Skip to content

mcp: ServerSession.Close deadlocks while a subscriptions/listen stream is in flight #1160

Description

@kfet

Server.subscriptionsListen is a long-lived handler that blocks until the session context is cancelled, but it runs as an ordinary in-flight incoming request. ServerSession.Closejsonrpc2.Connection.Close waits for in-flight requests to drain, so Close waits on the handler and the handler waits on the teardown Close was about to do.

ClientSession.Subscribe opens that stream implicitly, so any server whose client has subscribed can no longer be closed.

Repro'd on v1.7.0 and main (v1.7.1-0.20260810085250-4282916668c3). v1.6.1 predates SEP-2575 — a defect in the new feature, not a regression.

Repro (fails in ~10s)

func TestServerSessionCloseDeadlock(t *testing.T) {
	ctx := context.Background()
	s := mcp.NewServer(&mcp.Implementation{Name: "s", Version: "0"}, &mcp.ServerOptions{
		SubscribeHandler:   func(context.Context, *mcp.SubscribeRequest) error { return nil },
		UnsubscribeHandler: func(context.Context, *mcp.UnsubscribeRequest) error { return nil },
	})
	s.AddResource(&mcp.Resource{URI: "test://r", Name: "r"},
		func(context.Context, *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
			return &mcp.ReadResourceResult{}, nil
		})

	ct, st := mcp.NewInMemoryTransports()
	s.Connect(ctx, st, nil)
	cs, _ := mcp.NewClient(&mcp.Implementation{Name: "c", Version: "0"}, nil).Connect(ctx, ct, nil)

	cs.Subscribe(ctx, &mcp.SubscribeParams{URI: "test://r"}) // opens subscriptions/listen
	time.Sleep(200 * time.Millisecond)

	var ss *mcp.ServerSession
	for x := range s.Sessions() {
		ss = x
		break
	}

	done := make(chan error, 1)
	go func() { done <- ss.Close() }()
	select {
	case <-done:
	case <-time.After(10 * time.Second):
		t.Fatal("DEADLOCK: ServerSession.Close did not return within 10s")
	}
}

The cycle

mcp.(*ServerSession).Close                    mcp/server.go:2028
jsonrpc2.(*Connection).Close                  internal/jsonrpc2/conn.go:508
jsonrpc2.(*Connection).wait      [chan recv]  internal/jsonrpc2/conn.go:480
        ↕ waits for
jsonrpc2.(*Connection).handleAsync.func3      internal/jsonrpc2/conn.go:685
mcp.(*ServerSession).handle                   mcp/server.go:1925
mcp.(*Server).subscriptionsListen [chan recv] mcp/server.go:1247

Suggested fix

Exclude subscriptions/listen from the shutdown wait set, or cancel the session context before waiting in Close.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Significant bug affecting many users

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions