Skip to content

Commit a38032f

Browse files
committed
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 ./...
1 parent deecf8a commit a38032f

4 files changed

Lines changed: 145 additions & 3 deletions

File tree

mcp/server.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1901,7 +1901,46 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
19011901
if validatedMeta.usesNewProtocol {
19021902
ss.setLevel(ctx, &SetLoggingLevelParams{Level: validatedMeta.logLevel})
19031903
}
1904-
return handleReceive(ctx, ss, req)
1904+
res, err := handleReceive(ctx, ss, req)
1905+
if err != nil {
1906+
return nil, err
1907+
}
1908+
if validatedMeta.usesNewProtocol {
1909+
return withCompleteResultType(res), nil
1910+
}
1911+
return res, nil
1912+
}
1913+
1914+
func withCompleteResultType(res Result) any {
1915+
switch res.(type) {
1916+
case *CompleteResult, *DiscoverResult, *ListPromptsResult, *ListResourceTemplatesResult, *ListResourcesResult, *ListToolsResult:
1917+
return completeResult{Result: res}
1918+
default:
1919+
return res
1920+
}
1921+
}
1922+
1923+
type completeResult struct {
1924+
Result
1925+
}
1926+
1927+
func (r completeResult) MarshalJSON() ([]byte, error) {
1928+
data, err := json.Marshal(r.Result)
1929+
if err != nil {
1930+
return nil, err
1931+
}
1932+
data = bytes.TrimSpace(data)
1933+
if len(data) == 2 && data[0] == '{' && data[1] == '}' {
1934+
return []byte(`{"resultType":"complete"}`), nil
1935+
}
1936+
if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' {
1937+
return data, nil
1938+
}
1939+
out := make([]byte, 0, len(data)+len(`,"resultType":"complete"`))
1940+
out = append(out, data[:len(data)-1]...)
1941+
out = append(out, `,"resultType":"complete"`...)
1942+
out = append(out, '}')
1943+
return out, nil
19051944
}
19061945

19071946
// InitializeParams returns the InitializeParams provided during the client's

mcp/server_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,108 @@ func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) {
13991399
})
14001400
}
14011401

1402+
func TestServerSessionHandle_AddsResultTypeOnNewProtocol(t *testing.T) {
1403+
server := NewServer(testImpl, &ServerOptions{
1404+
CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) {
1405+
return &CompleteResult{
1406+
Completion: CompletionResultDetails{
1407+
Values: []string{"go"},
1408+
},
1409+
}, nil
1410+
},
1411+
})
1412+
newProtocolParams := func(fields map[string]any) map[string]any {
1413+
params := map[string]any{
1414+
"_meta": map[string]any{
1415+
MetaKeyProtocolVersion: protocolVersion20260728,
1416+
MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"},
1417+
MetaKeyClientCapabilities: map[string]any{},
1418+
},
1419+
}
1420+
for k, v := range fields {
1421+
params[k] = v
1422+
}
1423+
return params
1424+
}
1425+
1426+
tests := []struct {
1427+
name string
1428+
method string
1429+
params map[string]any
1430+
}{
1431+
{
1432+
name: "discover",
1433+
method: methodDiscover,
1434+
params: newProtocolParams(nil),
1435+
},
1436+
{
1437+
name: "tools list",
1438+
method: methodListTools,
1439+
params: newProtocolParams(nil),
1440+
},
1441+
{
1442+
name: "prompts list",
1443+
method: methodListPrompts,
1444+
params: newProtocolParams(nil),
1445+
},
1446+
{
1447+
name: "resources list",
1448+
method: methodListResources,
1449+
params: newProtocolParams(nil),
1450+
},
1451+
{
1452+
name: "resource templates list",
1453+
method: methodListResourceTemplates,
1454+
params: newProtocolParams(nil),
1455+
},
1456+
{
1457+
name: "complete",
1458+
method: methodComplete,
1459+
params: newProtocolParams(map[string]any{
1460+
"argument": map[string]any{
1461+
"name": "language",
1462+
"value": "g",
1463+
},
1464+
"ref": map[string]any{
1465+
"type": "ref/prompt",
1466+
"name": "code_review",
1467+
},
1468+
}),
1469+
},
1470+
}
1471+
1472+
for _, tc := range tests {
1473+
t.Run(tc.name, func(t *testing.T) {
1474+
ss := &ServerSession{server: server}
1475+
id, err := jsonrpc.MakeID("test")
1476+
if err != nil {
1477+
t.Fatal(err)
1478+
}
1479+
result, err := ss.handle(context.Background(), &jsonrpc.Request{
1480+
ID: id,
1481+
Method: tc.method,
1482+
Params: mustMarshal(tc.params),
1483+
})
1484+
if err != nil {
1485+
t.Fatal(err)
1486+
}
1487+
data, err := json.Marshal(result)
1488+
if err != nil {
1489+
t.Fatal(err)
1490+
}
1491+
var got struct {
1492+
ResultType string `json:"resultType"`
1493+
}
1494+
if err := json.Unmarshal(data, &got); err != nil {
1495+
t.Fatal(err)
1496+
}
1497+
if got.ResultType != string(resultTypeComplete) {
1498+
t.Fatalf("resultType = %q, want %q; response = %s", got.ResultType, resultTypeComplete, data)
1499+
}
1500+
})
1501+
}
1502+
}
1503+
14021504
// TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol verifies that
14031505
// the methods removed by SEP-2575 (`initialize`, `notifications/initialized`,
14041506
// `ping`) all return Method not found when the request opts into the new

mcp/testdata/conformance/server/discover.txtar

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ supports, its capabilities, and its identity.
3636
"serverInfo": {
3737
"name": "testServer",
3838
"version": "v1.0.0"
39-
}
39+
},
40+
"resultType": "complete"
4041
}
4142
}

mcp/transport_example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func ExampleLoggingTransport() {
4242
}
4343

4444
// Output:
45-
// 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"}}}
45+
// 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"}}
4646
// 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"}}}
4747
}
4848

0 commit comments

Comments
 (0)