diff --git a/docs/mcpgodebug.md b/docs/mcpgodebug.md index 696aa9f92..01e96285c 100644 --- a/docs/mcpgodebug.md +++ b/docs/mcpgodebug.md @@ -42,6 +42,13 @@ Options listed below were added and will be removed in the 1.11.0 version of the behavior was changed to reject such messages as invalid requests, as the specification lists `params` as required for all three. +- `nopreinitinvalidrequest` added. If set to `1`, a legacy server session that + has not been initialized refuses a request other than `initialize` or `ping` + with JSON-RPC error code `0`, restoring the previous behavior. The default + behavior was changed to refuse it with `-32600` (Invalid Request): `0` + tells a client nothing and is indistinguishable from an unset code. See + issue #1367. + ### 1.8.0 Options listed below were added and will be removed in the 1.9.0 version of the SDK. diff --git a/internal/docs/mcpgodebug.src.md b/internal/docs/mcpgodebug.src.md index b1b5016b5..0a75d81de 100644 --- a/internal/docs/mcpgodebug.src.md +++ b/internal/docs/mcpgodebug.src.md @@ -41,6 +41,13 @@ Options listed below were added and will be removed in the 1.11.0 version of the behavior was changed to reject such messages as invalid requests, as the specification lists `params` as required for all three. +- `nopreinitinvalidrequest` added. If set to `1`, a legacy server session that + has not been initialized refuses a request other than `initialize` or `ping` + with JSON-RPC error code `0`, restoring the previous behavior. The default + behavior was changed to refuse it with `-32600` (Invalid Request): `0` + tells a client nothing and is indistinguishable from an unset code. See + issue #1367. + ### 1.8.0 Options listed below were added and will be removed in the 1.9.0 version of the SDK. diff --git a/mcp/server.go b/mcp/server.go index 3efa3b834..a6bbdea72 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -2010,6 +2010,14 @@ func (ss *ServerSession) receivingMethodHandler() MethodHandler { // getConn implements [session.getConn]. func (ss *ServerSession) getConn() *jsonrpc2.Connection { return ss.conn } +// nopreinitinvalidrequest is a compatibility parameter that restores the +// previous behavior of a legacy server session that has not been initialized, +// where a request other than initialize or ping was refused with JSON-RPC +// error code 0 instead of -32600 (Invalid Request). See the documentation for +// the mcpgodebug package for instructions how to enable it. The option will be +// removed in a future version of the SDK. +var nopreinitinvalidrequest = mcpgodebug.Value("nopreinitinvalidrequest") + // handle invokes the method described by the given JSON RPC request. func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, error) { ss.mu.Lock() @@ -2076,7 +2084,11 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, if !initialized && !validatedMeta.usesNewProtocol && req.IsCall() && req.Method != methodInitialize && req.Method != methodPing { ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method) - return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method) + err := fmt.Errorf("method %q is invalid during session initialization", req.Method) + if nopreinitinvalidrequest == "1" { + return nil, err + } + return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidRequest, Message: err.Error()} } // modelcontextprotocol/go-sdk#26: handle calls asynchronously, and diff --git a/mcp/server_test.go b/mcp/server_test.go index 314afefa1..c3bfcee8f 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -2445,6 +2445,26 @@ func TestServerHandle_LegacyCallBeforeInitialize(t *testing.T) { if want := fmt.Sprintf("method %q is invalid during session initialization", tc.method); err.Error() != want { t.Errorf("handle(%q) error = %q, want %q", tc.method, err.Error(), want) } + var rpcErr *jsonrpc.Error + if !errors.As(err, &rpcErr) || rpcErr.Code != jsonrpc.CodeInvalidRequest { + t.Errorf("handle(%q) error = %#v, want a JSON-RPC error with code %d", tc.method, err, jsonrpc.CodeInvalidRequest) + } + // MCPGODEBUG=nopreinitinvalidrequest=1 restores the old code, 0. + func() { + defer func(old string) { nopreinitinvalidrequest = old }(nopreinitinvalidrequest) + nopreinitinvalidrequest = "1" + _, err := ss.handle(ctx, &jsonrpc.Request{ + ID: jsonrpc2.Int64ID(2), + Method: tc.method, + Params: json.RawMessage(tc.params), + }) + if err == nil { + t.Fatalf("handle(%q), old error code restored, served the call before initialize", tc.method) + } + if errors.As(err, &rpcErr) { + t.Errorf("handle(%q), old error code restored, error = %#v, want a plain error (code 0 on the wire)", tc.method, err) + } + }() if handlerRan { t.Errorf("handle(%q) ran the server's handler for a call it refused", tc.method) } diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index a481e7b7c..a5d987ab8 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -1291,6 +1291,7 @@ func TestStreamableServerTransport(t *testing.T) { messages: []jsonrpc.Message{req(2, "tools/call", &CallToolParams{Name: "tool"})}, wantStatusCode: http.StatusOK, wantMessages: []jsonrpc.Message{resp(2, nil, &jsonrpc.Error{ + Code: jsonrpc.CodeInvalidRequest, Message: `method "tools/call" is invalid during session initialization`, })}, }, diff --git a/mcp/testdata/conformance/server/lifecycle.txtar b/mcp/testdata/conformance/server/lifecycle.txtar index 652aa7f32..edaf27f3a 100644 --- a/mcp/testdata/conformance/server/lifecycle.txtar +++ b/mcp/testdata/conformance/server/lifecycle.txtar @@ -26,7 +26,7 @@ See also modelcontextprotocol/go-sdk#225. "jsonrpc": "2.0", "id": 2, "error": { - "code": 0, + "code": -32600, "message": "method \"tools/list\" is invalid during session initialization" } }