From a38032fe5760766814cfdd84e15843f8df631bf3 Mon Sep 17 00:00:00 2001 From: ychampion Date: Thu, 9 Jul 2026 02:53:29 +0000 Subject: [PATCH 1/3] mcp: include resultType on new-protocol responses Wrap non-MRTR server result objects for 2026-07-28 requests so the wire response includes resultType without changing direct marshaling of public result structs. Fixes #1032 Constraint: Preserve existing public result struct shape and direct JSON output for callers. Tested: go test ./mcp -run 'Test(ServerSessionHandle|CompleteResult|ClientConnectDiscover|InMemory_E2E_Discover)' -count=1; go test ./mcp -count=1; go test ./... --- mcp/server.go | 41 ++++++- mcp/server_test.go | 102 ++++++++++++++++++ .../conformance/server/discover.txtar | 3 +- mcp/transport_example_test.go | 2 +- 4 files changed, 145 insertions(+), 3 deletions(-) diff --git a/mcp/server.go b/mcp/server.go index c59f7930..2d9aa34b 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -1901,7 +1901,46 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, if validatedMeta.usesNewProtocol { ss.setLevel(ctx, &SetLoggingLevelParams{Level: validatedMeta.logLevel}) } - return handleReceive(ctx, ss, req) + res, err := handleReceive(ctx, ss, req) + if err != nil { + return nil, err + } + if validatedMeta.usesNewProtocol { + return withCompleteResultType(res), nil + } + return res, nil +} + +func withCompleteResultType(res Result) any { + switch res.(type) { + case *CompleteResult, *DiscoverResult, *ListPromptsResult, *ListResourceTemplatesResult, *ListResourcesResult, *ListToolsResult: + return completeResult{Result: res} + default: + return res + } +} + +type completeResult struct { + Result +} + +func (r completeResult) MarshalJSON() ([]byte, error) { + data, err := json.Marshal(r.Result) + if err != nil { + return nil, err + } + data = bytes.TrimSpace(data) + if len(data) == 2 && data[0] == '{' && data[1] == '}' { + return []byte(`{"resultType":"complete"}`), nil + } + if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' { + return data, nil + } + out := make([]byte, 0, len(data)+len(`,"resultType":"complete"`)) + out = append(out, data[:len(data)-1]...) + out = append(out, `,"resultType":"complete"`...) + out = append(out, '}') + return out, nil } // InitializeParams returns the InitializeParams provided during the client's diff --git a/mcp/server_test.go b/mcp/server_test.go index ac303be1..8fe2de79 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -1399,6 +1399,108 @@ func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) { }) } +func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) { + server := NewServer(testImpl, &ServerOptions{ + CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) { + return &CompleteResult{ + Completion: CompletionResultDetails{ + Values: []string{"go"}, + }, + }, nil + }, + }) + newProtocolParams := func(fields map[string]any) map[string]any { + params := map[string]any{ + "_meta": map[string]any{ + MetaKeyProtocolVersion: protocolVersion20260728, + MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"}, + MetaKeyClientCapabilities: map[string]any{}, + }, + } + for k, v := range fields { + params[k] = v + } + return params + } + + tests := []struct { + name string + method string + params map[string]any + }{ + { + name: "discover", + method: methodDiscover, + params: newProtocolParams(nil), + }, + { + name: "tools list", + method: methodListTools, + params: newProtocolParams(nil), + }, + { + name: "prompts list", + method: methodListPrompts, + params: newProtocolParams(nil), + }, + { + name: "resources list", + method: methodListResources, + params: newProtocolParams(nil), + }, + { + name: "resource templates list", + method: methodListResourceTemplates, + params: newProtocolParams(nil), + }, + { + name: "complete", + method: methodComplete, + params: newProtocolParams(map[string]any{ + "argument": map[string]any{ + "name": "language", + "value": "g", + }, + "ref": map[string]any{ + "type": "ref/prompt", + "name": "code_review", + }, + }), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ss := &ServerSession{server: server} + id, err := jsonrpc.MakeID("test") + if err != nil { + t.Fatal(err) + } + result, err := ss.handle(context.Background(), &jsonrpc.Request{ + ID: id, + Method: tc.method, + Params: mustMarshal(tc.params), + }) + if err != nil { + t.Fatal(err) + } + data, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + var got struct { + ResultType string `json:"resultType"` + } + if err := json.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + if got.ResultType != string(resultTypeComplete) { + t.Fatalf("resultType = %q, want %q; response = %s", got.ResultType, resultTypeComplete, data) + } + }) + } +} + // TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol verifies that // the methods removed by SEP-2575 (`initialize`, `notifications/initialized`, // `ping`) all return Method not found when the request opts into the new diff --git a/mcp/testdata/conformance/server/discover.txtar b/mcp/testdata/conformance/server/discover.txtar index 9b8276c9..ad156d2e 100644 --- a/mcp/testdata/conformance/server/discover.txtar +++ b/mcp/testdata/conformance/server/discover.txtar @@ -36,6 +36,7 @@ supports, its capabilities, and its identity. "serverInfo": { "name": "testServer", "version": "v1.0.0" - } + }, + "resultType": "complete" } } diff --git a/mcp/transport_example_test.go b/mcp/transport_example_test.go index 1920233d..6fc3dd78 100644 --- a/mcp/transport_example_test.go +++ b/mcp/transport_example_test.go @@ -42,7 +42,7 @@ func ExampleLoggingTransport() { } // Output: - // read: {"jsonrpc":"2.0","id":1,"result":{"ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"}}} + // read: {"jsonrpc":"2.0","id":1,"result":{"ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"},"resultType":"complete"}} // write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}} } From 95e8252479f0189a1b3803883d667c786dc587c0 Mon Sep 17 00:00:00 2001 From: ychampion Date: Thu, 9 Jul 2026 09:18:24 +0000 Subject: [PATCH 2/3] mcp: move complete resultType into protocol types --- mcp/protocol.go | 24 +++++++++++++ mcp/protocol_test.go | 3 +- mcp/server.go | 34 +------------------ .../conformance/server/discover.txtar | 4 +-- mcp/transport_example_test.go | 2 +- 5 files changed, 30 insertions(+), 37 deletions(-) diff --git a/mcp/protocol.go b/mcp/protocol.go index a68ef429..2336482f 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -28,6 +28,24 @@ const ( resultTypeInputRequired resultType = "input_required" ) +type completeResultWithType struct { + ResultType resultType `json:"resultType,omitempty"` +} + +func (r *completeResultWithType) setResultType(rt resultType) { r.ResultType = rt } +func (*completeResultWithType) isCompleteResult() {} + +type completeResultResponse interface { + setResultType(resultType) + isCompleteResult() +} + +func setCompleteResultType(res Result) { + if r, ok := res.(completeResultResponse); ok { + r.setResultType(resultTypeComplete) + } +} + // InputRequest is a type for parameters that a server can include in the response // to request input from client (SEP-2322). Implementations are [*ElicitParams], // [*CreateMessageParams], and [*ListRootsParams]. @@ -637,6 +655,7 @@ type CompletionResultDetails struct { // The server's response to a completion/complete request type CompleteResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` @@ -1114,6 +1133,7 @@ func (x *DiscoverParams) GetProgressToken() any { return getProgressToken(x) } func (x *DiscoverParams) SetProgressToken(t any) { setProgressToken(x, t) } type DiscoverResult struct { + completeResultWithType Meta `json:"_meta,omitempty"` Cacheable // The versions of the Model Context Protocol that the server supports. @@ -1177,6 +1197,7 @@ func (c *Cacheable) setDefaultCacheableValues() { // The server's response to a prompts/list request from the client. type ListPromptsResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` @@ -1207,6 +1228,7 @@ func (x *ListResourceTemplatesParams) cursorPtr() *string { return &x.Cursor // The server's response to a resources/templates/list request from the client. type ListResourceTemplatesResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` @@ -1237,6 +1259,7 @@ func (x *ListResourcesParams) cursorPtr() *string { return &x.Cursor } // The server's response to a resources/list request from the client. type ListResourcesResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` @@ -1303,6 +1326,7 @@ func (x *ListToolsParams) cursorPtr() *string { return &x.Cursor } // The server's response to a tools/list request from the client. type ListToolsResult struct { + completeResultWithType // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` diff --git a/mcp/protocol_test.go b/mcp/protocol_test.go index 38fd48f4..fe496dec 100644 --- a/mcp/protocol_test.go +++ b/mcp/protocol_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" ) func TestParamsMeta(t *testing.T) { @@ -491,7 +492,7 @@ func TestCompleteResult(t *testing.T) { if err := json.Unmarshal([]byte(test.in), &got); err != nil { t.Fatalf("json.Unmarshal(CompleteResult) failed: %v", err) } - if diff := cmp.Diff(test.want, got); diff != "" { + if diff := cmp.Diff(test.want, got, cmpopts.IgnoreUnexported(CompleteResult{})); diff != "" { t.Errorf("CompleteResult unmarshal mismatch (-want +got):\n%s", diff) } }) diff --git a/mcp/server.go b/mcp/server.go index 2d9aa34b..593a3fcd 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -1906,43 +1906,11 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, return nil, err } if validatedMeta.usesNewProtocol { - return withCompleteResultType(res), nil + setCompleteResultType(res) } return res, nil } -func withCompleteResultType(res Result) any { - switch res.(type) { - case *CompleteResult, *DiscoverResult, *ListPromptsResult, *ListResourceTemplatesResult, *ListResourcesResult, *ListToolsResult: - return completeResult{Result: res} - default: - return res - } -} - -type completeResult struct { - Result -} - -func (r completeResult) MarshalJSON() ([]byte, error) { - data, err := json.Marshal(r.Result) - if err != nil { - return nil, err - } - data = bytes.TrimSpace(data) - if len(data) == 2 && data[0] == '{' && data[1] == '}' { - return []byte(`{"resultType":"complete"}`), nil - } - if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' { - return data, nil - } - out := make([]byte, 0, len(data)+len(`,"resultType":"complete"`)) - out = append(out, data[:len(data)-1]...) - out = append(out, `,"resultType":"complete"`...) - out = append(out, '}') - return out, nil -} - // InitializeParams returns the InitializeParams provided during the client's // initial connection. func (ss *ServerSession) InitializeParams() *InitializeParams { diff --git a/mcp/testdata/conformance/server/discover.txtar b/mcp/testdata/conformance/server/discover.txtar index ad156d2e..9552edaa 100644 --- a/mcp/testdata/conformance/server/discover.txtar +++ b/mcp/testdata/conformance/server/discover.txtar @@ -21,6 +21,7 @@ supports, its capabilities, and its identity. "jsonrpc": "2.0", "id": 1, "result": { + "resultType": "complete", "ttlMs": 0, "cacheScope": "public", "supportedVersions": [ @@ -36,7 +37,6 @@ supports, its capabilities, and its identity. "serverInfo": { "name": "testServer", "version": "v1.0.0" - }, - "resultType": "complete" + } } } diff --git a/mcp/transport_example_test.go b/mcp/transport_example_test.go index 6fc3dd78..1d97a812 100644 --- a/mcp/transport_example_test.go +++ b/mcp/transport_example_test.go @@ -42,7 +42,7 @@ func ExampleLoggingTransport() { } // Output: - // read: {"jsonrpc":"2.0","id":1,"result":{"ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"},"resultType":"complete"}} + // read: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"}}} // write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}} } From 1645edff9e1d1ee275458ec2497d2d1f0b4580e9 Mon Sep 17 00:00:00 2001 From: ychampion Date: Fri, 10 Jul 2026 13:04:33 +0000 Subject: [PATCH 3/3] mcp: test MRTR resultType preservation Constraint: MRTR responses must retain input_required instead of receiving the generic complete discriminator. Tested: go test ./mcp -run TestServerSessionHandle_SetsResultTypeOnNewProtocol -count=1; go test ./...; go vet ./... --- mcp/server_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/mcp/server_test.go b/mcp/server_test.go index 8fe2de79..d6ecfed4 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -1399,7 +1399,7 @@ func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) { }) } -func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) { +func TestServerSessionHandle_SetsResultTypeOnNewProtocol(t *testing.T) { server := NewServer(testImpl, &ServerOptions{ CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) { return &CompleteResult{ @@ -1409,6 +1409,21 @@ func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) { }, nil }, }) + AddTool(server, &Tool{Name: "tool"}, func(context.Context, *CallToolRequest, struct{}) (*CallToolResult, any, error) { + return &CallToolResult{ + InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}}, + }, nil, nil + }) + server.AddPrompt(&Prompt{Name: "prompt"}, func(context.Context, *GetPromptRequest) (*GetPromptResult, error) { + return &GetPromptResult{ + InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}}, + }, nil + }) + server.AddResource(&Resource{URI: "test://resource", Name: "resource"}, func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error) { + return &ReadResourceResult{ + InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}}, + }, nil + }) newProtocolParams := func(fields map[string]any) map[string]any { params := map[string]any{ "_meta": map[string]any{ @@ -1427,31 +1442,37 @@ func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) { name string method string params map[string]any + want resultType }{ { name: "discover", method: methodDiscover, params: newProtocolParams(nil), + want: resultTypeComplete, }, { name: "tools list", method: methodListTools, params: newProtocolParams(nil), + want: resultTypeComplete, }, { name: "prompts list", method: methodListPrompts, params: newProtocolParams(nil), + want: resultTypeComplete, }, { name: "resources list", method: methodListResources, params: newProtocolParams(nil), + want: resultTypeComplete, }, { name: "resource templates list", method: methodListResourceTemplates, params: newProtocolParams(nil), + want: resultTypeComplete, }, { name: "complete", @@ -1466,6 +1487,25 @@ func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) { "name": "code_review", }, }), + want: resultTypeComplete, + }, + { + name: "tool input required", + method: methodCallTool, + params: newProtocolParams(map[string]any{"name": "tool", "arguments": map[string]any{}}), + want: resultTypeInputRequired, + }, + { + name: "prompt input required", + method: methodGetPrompt, + params: newProtocolParams(map[string]any{"name": "prompt"}), + want: resultTypeInputRequired, + }, + { + name: "resource input required", + method: methodReadResource, + params: newProtocolParams(map[string]any{"uri": "test://resource"}), + want: resultTypeInputRequired, }, } @@ -1494,8 +1534,8 @@ func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) { if err := json.Unmarshal(data, &got); err != nil { t.Fatal(err) } - if got.ResultType != string(resultTypeComplete) { - t.Fatalf("resultType = %q, want %q; response = %s", got.ResultType, resultTypeComplete, data) + if got.ResultType != string(tc.want) { + t.Fatalf("resultType = %q, want %q; response = %s", got.ResultType, tc.want, data) } }) }