Skip to content

Commit 5ab63fe

Browse files
committed
mcp: don't mark the server sesion as initialized prematurely
Update ServerSession so that the session is not marked as initialized until 'notifications/initialized' is actually received from the client. Include a new test of this lifecycle strictness. Fixes #225
1 parent 4608401 commit 5ab63fe

File tree

6 files changed

+90
-22
lines changed

6 files changed

+90
-22
lines changed

mcp/server.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -567,11 +567,25 @@ func (s *Server) Connect(ctx context.Context, t Transport) (*ServerSession, erro
567567
return connect(ctx, t, s)
568568
}
569569

570-
func (s *Server) callInitializedHandler(ctx context.Context, ss *ServerSession, params *InitializedParams) (Result, error) {
571-
if s.opts.KeepAlive > 0 {
572-
ss.startKeepalive(s.opts.KeepAlive)
570+
func (ss *ServerSession) initialized(ctx context.Context, params *InitializedParams) (Result, error) {
571+
if ss.server.opts.KeepAlive > 0 {
572+
ss.startKeepalive(ss.server.opts.KeepAlive)
573573
}
574-
return callNotificationHandler(ctx, s.opts.InitializedHandler, ss, params)
574+
ss.mu.Lock()
575+
hasParams := ss.initializeParams != nil
576+
wasInitialized := ss._initialized
577+
if hasParams {
578+
ss._initialized = true
579+
}
580+
ss.mu.Unlock()
581+
582+
if !hasParams {
583+
return nil, fmt.Errorf("%q before %q", notificationInitialized, methodInitialize)
584+
}
585+
if wasInitialized {
586+
return nil, fmt.Errorf("duplicate %q received", notificationInitialized)
587+
}
588+
return callNotificationHandler(ctx, ss.server.opts.InitializedHandler, ss, params)
575589
}
576590

577591
func (s *Server) callRootsListChangedHandler(ctx context.Context, ss *ServerSession, params *RootsListChangedParams) (Result, error) {
@@ -603,7 +617,7 @@ type ServerSession struct {
603617
mu sync.Mutex
604618
logLevel LoggingLevel
605619
initializeParams *InitializeParams
606-
initialized bool
620+
_initialized bool
607621
keepaliveCancel context.CancelFunc
608622
}
609623

@@ -702,7 +716,7 @@ var serverMethodInfos = map[string]methodInfo{
702716
methodSetLevel: newMethodInfo(sessionMethod((*ServerSession).setLevel), 0),
703717
methodSubscribe: newMethodInfo(serverMethod((*Server).subscribe), 0),
704718
methodUnsubscribe: newMethodInfo(serverMethod((*Server).unsubscribe), 0),
705-
notificationInitialized: newMethodInfo(serverMethod((*Server).callInitializedHandler), notification|missingParamsOK),
719+
notificationInitialized: newMethodInfo(sessionMethod((*ServerSession).initialized), notification|missingParamsOK),
706720
notificationRootsListChanged: newMethodInfo(serverMethod((*Server).callRootsListChangedHandler), notification|missingParamsOK),
707721
notificationProgress: newMethodInfo(sessionMethod((*ServerSession).callProgressNotificationHandler), notification),
708722
}
@@ -729,13 +743,13 @@ func (ss *ServerSession) getConn() *jsonrpc2.Connection { return ss.conn }
729743
// handle invokes the method described by the given JSON RPC request.
730744
func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, error) {
731745
ss.mu.Lock()
732-
initialized := ss.initialized
746+
initialized := ss._initialized
733747
ss.mu.Unlock()
734748
// From the spec:
735749
// "The client SHOULD NOT send requests other than pings before the server
736750
// has responded to the initialize request."
737751
switch req.Method {
738-
case "initialize", "ping":
752+
case methodInitialize, methodPing, notificationInitialized:
739753
default:
740754
if !initialized {
741755
return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method)
@@ -753,16 +767,8 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam
753767
return nil, fmt.Errorf("%w: \"params\" must be be provided", jsonrpc2.ErrInvalidParams)
754768
}
755769
ss.mu.Lock()
756-
defer ss.mu.Unlock()
757770
ss.initializeParams = params
758-
759-
// Mark the connection as initialized when this method exits.
760-
// TODO(#26): Technically, the server should not be considered initialized
761-
// until it has *responded*, but since jsonrpc2 is currently serialized we
762-
// can mark the session as initialized here. If we ever implement a
763-
// concurrency model (#26), we need to guarantee that initialize is not
764-
// handled concurrently to other requests.
765-
ss.initialized = true
771+
ss.mu.Unlock()
766772

767773
// If we support the client's version, reply with it. Otherwise, reply with our
768774
// latest version.

mcp/testdata/conformance/server/bad_requests.txtar

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ code_review
3838
"clientInfo": { "name": "ExampleClient", "version": "1.0.0" }
3939
}
4040
}
41-
{"jsonrpc":"2.0", "id": 3, "method": "notifications/initialized"}
42-
{"jsonrpc":"2.0", "method":"ping"}
43-
{"jsonrpc":"2.0", "id": 4, "method": "logging/setLevel"}
44-
{"jsonrpc":"2.0", "id": 5, "method": "completion/complete"}
45-
{"jsonrpc":"2.0", "id": 4, "method": "logging/setLevel", "params": null}
41+
{ "jsonrpc":"2.0", "id": 3, "method": "notifications/initialized" }
42+
{ "jsonrpc":"2.0", "method": "notifications/initialized" }
43+
{ "jsonrpc":"2.0", "method":"ping" }
44+
{ "jsonrpc":"2.0", "id": 4, "method": "logging/setLevel" }
45+
{ "jsonrpc":"2.0", "id": 5, "method": "completion/complete" }
46+
{ "jsonrpc":"2.0", "id": 4, "method": "logging/setLevel", "params": null }
4647

4748
-- server --
4849
{
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
This test checks that the server obeys the rules for initialization lifecycle,
2+
and rejects non-ping requests until 'initialized' is received.
3+
4+
See also modelcontextprotocol/go-sdk#225.
5+
6+
-- client --
7+
{ "jsonrpc":"2.0", "method": "notifications/initialized" }
8+
{
9+
"jsonrpc": "2.0",
10+
"id": 1,
11+
"method": "initialize",
12+
"params": {
13+
"protocolVersion": "2024-11-05",
14+
"capabilities": {},
15+
"clientInfo": { "name": "ExampleClient", "version": "1.0.0" }
16+
}
17+
}
18+
{ "jsonrpc":"2.0", "id": 1, "method":"ping" }
19+
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
20+
{ "jsonrpc":"2.0", "method": "notifications/initialized" }
21+
{ "jsonrpc": "2.0", "id": 3, "method": "tools/list" }
22+
23+
-- server --
24+
{
25+
"jsonrpc": "2.0",
26+
"id": 1,
27+
"result": {
28+
"capabilities": {
29+
"logging": {}
30+
},
31+
"protocolVersion": "2024-11-05",
32+
"serverInfo": {
33+
"name": "testServer",
34+
"version": "v1.0.0"
35+
}
36+
}
37+
}
38+
{
39+
"jsonrpc": "2.0",
40+
"id": 1,
41+
"result": {}
42+
}
43+
{
44+
"jsonrpc": "2.0",
45+
"id": 2,
46+
"error": {
47+
"code": 0,
48+
"message": "method \"tools/list\" is invalid during session initialization"
49+
}
50+
}
51+
{
52+
"jsonrpc": "2.0",
53+
"id": 3,
54+
"result": {
55+
"tools": []
56+
}
57+
}

mcp/testdata/conformance/server/prompts.txtar

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ code_review
1818
"clientInfo": { "name": "ExampleClient", "version": "1.0.0" }
1919
}
2020
}
21+
{ "jsonrpc":"2.0", "method": "notifications/initialized" }
2122
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
2223
{ "jsonrpc": "2.0", "id": 4, "method": "prompts/list" }
2324
{ "jsonrpc": "2.0", "id": 5, "method": "prompts/get" }
25+
2426
-- server --
2527
{
2628
"jsonrpc": "2.0",

mcp/testdata/conformance/server/resources.txtar

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ info.txt
2121
"clientInfo": { "name": "ExampleClient", "version": "1.0.0" }
2222
}
2323
}
24+
{ "jsonrpc":"2.0", "method": "notifications/initialized" }
2425
{ "jsonrpc": "2.0", "id": 2, "method": "resources/list" }
2526
{
2627
"jsonrpc": "2.0", "id": 3,

mcp/testdata/conformance/server/tools.txtar

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ greet
2020
"clientInfo": { "name": "ExampleClient", "version": "1.0.0" }
2121
}
2222
}
23+
{"jsonrpc":"2.0", "method": "notifications/initialized"}
2324
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
2425
{ "jsonrpc": "2.0", "id": 3, "method": "resources/list" }
2526
{ "jsonrpc": "2.0", "id": 4, "method": "prompts/list" }

0 commit comments

Comments
 (0)