Skip to content

Commit 112ca4e

Browse files
cryo-zdfindleyr
authored andcommitted
mcp: Ensure keepalive goroutine is started exactly once
1 parent 52734fd commit 112ca4e

File tree

2 files changed

+52
-3
lines changed

2 files changed

+52
-3
lines changed

mcp/server.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -602,9 +602,6 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar
602602
// params are non-nil.
603603
params = new(InitializedParams)
604604
}
605-
if ss.server.opts.KeepAlive > 0 {
606-
ss.startKeepalive(ss.server.opts.KeepAlive)
607-
}
608605
var wasInit, wasInitd bool
609606
ss.updateState(func(state *ServerSessionState) {
610607
wasInit = state.InitializeParams != nil
@@ -620,6 +617,9 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar
620617
if wasInitd {
621618
return nil, fmt.Errorf("duplicate %q received", notificationInitialized)
622619
}
620+
if ss.server.opts.KeepAlive > 0 {
621+
ss.startKeepalive(ss.server.opts.KeepAlive)
622+
}
623623
if h := ss.server.opts.InitializedHandler; h != nil {
624624
h(ctx, serverRequestFor(ss, params))
625625
}

mcp/server_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log"
1010
"slices"
1111
"testing"
12+
"time"
1213

1314
"github.com/google/go-cmp/cmp"
1415
"github.com/google/jsonschema-go/jsonschema"
@@ -371,3 +372,51 @@ func TestServerCapabilities(t *testing.T) {
371372
})
372373
}
373374
}
375+
376+
// TestServerSessionkeepaliveCancelOverwritten is to verify that `ServerSession.keepaliveCancel` is assigned exactly once,
377+
// ensuring that only a single goroutine is responsible for the session's keepalive ping mechanism.
378+
func TestServerSessionkeepaliveCancelOverwritten(t *testing.T) {
379+
// Set KeepAlive to a long duration to ensure the keepalive
380+
// goroutine stays alive for the duration of the test without actually sending
381+
// ping requests, since we don't have a real client connection established.
382+
server := NewServer(testImpl, &ServerOptions{KeepAlive: 5 * time.Second})
383+
ss := &ServerSession{server: server}
384+
385+
// 1. Initialize the session.
386+
_, err := ss.initialize(context.Background(), &InitializeParams{})
387+
if err != nil {
388+
t.Fatalf("ServerSession initialize failed: %v", err)
389+
}
390+
391+
// 2. Call 'initialized' for the first time. This should start the keepalive mechanism.
392+
_, err = ss.initialized(context.Background(), &InitializedParams{})
393+
if err != nil {
394+
t.Fatalf("First initialized call failed: %v", err)
395+
}
396+
if ss.keepaliveCancel == nil {
397+
t.Fatalf("expected ServerSession.keepaliveCancel to be set after the first call of initialized")
398+
}
399+
400+
// Save the cancel function and use defer to ensure resources are cleaned up.
401+
firstCancel := ss.keepaliveCancel
402+
defer firstCancel()
403+
404+
// 3. Manually set the field to nil.
405+
// Do this to facilitate the test's core assertion. The goal is to verify that
406+
// 'ss.keepaliveCancel' is not assigned a second time. By setting it to nil,
407+
// we can easily check after the next call if a new keepalive goroutine was started.
408+
ss.keepaliveCancel = nil
409+
410+
// 4. Call 'initialized' for the second time. This should return an error.
411+
_, err = ss.initialized(context.Background(), &InitializedParams{})
412+
if err == nil {
413+
t.Fatalf("Expected 'duplicate initialized received' error on second call, got nil")
414+
}
415+
416+
// 5. Re-check the field to ensure it remains nil.
417+
// Since 'initialized' correctly returned an error and did not call
418+
// 'startKeepalive', the field should remain unchanged.
419+
if ss.keepaliveCancel != nil {
420+
t.Fatal("expected ServerSession.keepaliveCancel to be nil after we manually niled it and re-initialized")
421+
}
422+
}

0 commit comments

Comments
 (0)