Skip to content

Commit a6679c3

Browse files
mcp: fix race condition in ServerSession.startKeepalive
1 parent 755b9ed commit a6679c3

2 files changed

Lines changed: 36 additions & 4 deletions

File tree

mcp/server.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,13 @@ func (s *Server) Connect(ctx context.Context, t Transport, opts *ServerSessionOp
10281028
s.opts.Logger.Error("server connect error", "error", err)
10291029
return nil, err
10301030
}
1031+
1032+
// Start keepalive before returning the session to avoid race conditions with Close.
1033+
// This is safe because the spec allows sending pings before initialization (see ServerSession.handle for details).
1034+
if s.opts.KeepAlive > 0 {
1035+
ss.startKeepalive(ss.server.opts.KeepAlive)
1036+
}
1037+
10311038
return ss, nil
10321039
}
10331040

@@ -1055,9 +1062,6 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar
10551062
ss.server.opts.Logger.Error("duplicate initialized notification")
10561063
return nil, fmt.Errorf("duplicate %q received", notificationInitialized)
10571064
}
1058-
if ss.server.opts.KeepAlive > 0 {
1059-
ss.startKeepalive(ss.server.opts.KeepAlive)
1060-
}
10611065
if h := ss.server.opts.InitializedHandler; h != nil {
10621066
h(ctx, serverRequestFor(ss, params))
10631067
}
@@ -1107,7 +1111,7 @@ type ServerSession struct {
11071111
server *Server
11081112
conn *jsonrpc2.Connection
11091113
mcpConn Connection
1110-
keepaliveCancel context.CancelFunc // TODO: theory around why keepaliveCancel need not be guarded
1114+
keepaliveCancel context.CancelFunc
11111115

11121116
mu sync.Mutex
11131117
state ServerSessionState

mcp/streamable_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,34 @@ func TestStreamableServerShutdown(t *testing.T) {
367367
}
368368
}
369369

370+
// TestStreamableStatelessKeepaliveRace verifies that there is no data race between
371+
// ServerSession.startKeepalive and ServerSession.Close in stateless servers.
372+
func TestStreamableStatelessKeepaliveRace(t *testing.T) {
373+
ctx := context.Background()
374+
server := NewServer(testImpl, &ServerOptions{KeepAlive: time.Hour})
375+
AddTool(server, &Tool{Name: "greet"}, sayHi)
376+
handler := NewStreamableHTTPHandler(
377+
func(*http.Request) *Server { return server },
378+
&StreamableHTTPOptions{Stateless: true},
379+
)
380+
httpServer := httptest.NewServer(mustNotPanic(t, handler))
381+
defer httpServer.Close()
382+
383+
for range 50 {
384+
cs, err := NewClient(testImpl, nil).Connect(ctx, &StreamableClientTransport{
385+
Endpoint: httpServer.URL,
386+
}, nil)
387+
if err != nil {
388+
t.Fatalf("NewClient() failed: %v", err)
389+
}
390+
_, _ = cs.CallTool(ctx, &CallToolParams{
391+
Name: "greet",
392+
Arguments: map[string]any{"Name": "world"},
393+
})
394+
_ = cs.Close()
395+
}
396+
}
397+
370398
// TestClientReplay verifies that the client can recover from a mid-stream
371399
// network failure and receive replayed messages (if replay is configured). It
372400
// uses a proxy that is killed and restarted to simulate a recoverable network

0 commit comments

Comments
 (0)