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.Close → jsonrpc2.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.
Server.subscriptionsListenis a long-lived handler that blocks until the session context is cancelled, but it runs as an ordinary in-flight incoming request.ServerSession.Close→jsonrpc2.Connection.Closewaits 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.Subscribeopens 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)
The cycle
Suggested fix
Exclude
subscriptions/listenfrom the shutdown wait set, or cancel the session context before waiting inClose.