diff --git a/mcp/server.go b/mcp/server.go index 19d902ff..508552e5 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -267,19 +267,21 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan outval = elemZero } } - outbytes, err := json.Marshal(outval) - if err != nil { - return nil, fmt.Errorf("marshaling output: %w", err) - } - res.StructuredContent = json.RawMessage(outbytes) // avoid a second marshal over the wire - - // If the Content field isn't being used, return the serialized JSON in a - // TextContent block, as the spec suggests: - // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content. - if res.Content == nil { - res.Content = []Content{&TextContent{ - Text: string(outbytes), - }} + if outval != nil { + outbytes, err := json.Marshal(outval) + if err != nil { + return nil, fmt.Errorf("marshaling output: %w", err) + } + res.StructuredContent = json.RawMessage(outbytes) // avoid a second marshal over the wire + + // If the Content field isn't being used, return the serialized JSON in a + // TextContent block, as the spec suggests: + // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content. + if res.Content == nil { + res.Content = []Content{&TextContent{ + Text: string(outbytes), + }} + } } return res, nil } // end of handler diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index 3f897ba0..1e7a63ce 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -1115,8 +1115,7 @@ func TestStreamableStateless(t *testing.T) { }, wantMessages: []jsonrpc.Message{ resp(2, &CallToolResult{ - Content: []Content{&TextContent{Text: "hi World"}}, - StructuredContent: json.RawMessage("null"), + Content: []Content{&TextContent{Text: "hi World"}}, }, nil), }, }, @@ -1128,8 +1127,7 @@ func TestStreamableStateless(t *testing.T) { }, wantMessages: []jsonrpc.Message{ resp(2, &CallToolResult{ - Content: []Content{&TextContent{Text: "hi foo"}}, - StructuredContent: json.RawMessage("null"), + Content: []Content{&TextContent{Text: "hi foo"}}, }, nil), }, }, diff --git a/mcp/testdata/conformance/server/tools.txtar b/mcp/testdata/conformance/server/tools.txtar index 870e9ea5..c39e3ec9 100644 --- a/mcp/testdata/conformance/server/tools.txtar +++ b/mcp/testdata/conformance/server/tools.txtar @@ -21,11 +21,15 @@ structured "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } } -{"jsonrpc":"2.0", "method": "notifications/initialized"} +{ "jsonrpc":"2.0", "method": "notifications/initialized" } { "jsonrpc": "2.0", "id": 2, "method": "tools/list" } { "jsonrpc": "2.0", "id": 3, "method": "resources/list" } { "jsonrpc": "2.0", "id": 4, "method": "prompts/list" } { "jsonrpc": "2.0", "id": 5, "method": "tools/call" } +{ "jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "greet", "arguments": {"name": "you"} } } +{ "jsonrpc": "2.0", "id": 1, "result": {} } +{ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {"name": "structured", "arguments": {"In": "input"} } } + -- server -- { "jsonrpc": "2.0", @@ -119,3 +123,35 @@ structured "message": "invalid request: missing required \"params\"" } } +{ + "jsonrpc": "2.0", + "id": 1, + "method": "ping" +} +{ + "jsonrpc": "2.0", + "id": 6, + "result": { + "content": [ + { + "type": "text", + "text": "hi you" + } + ] + } +} +{ + "jsonrpc": "2.0", + "id": 7, + "result": { + "content": [ + { + "type": "text", + "text": "{\"Out\":\"Ack input\"}" + } + ], + "structuredContent": { + "Out": "Ack input" + } + } +}