mcp: set content to marshaled output (#398)
The ToolHandler constructed by ToolFor sets the result's Content to the marshaled output, following the spec's suggestion. See https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content. Also, set StructuredContent to the marshaled RawMessage to avoid a double marshal. Fixes #391.
This commit is contained in:
parent
07b65d76a7
commit
7769b2a959
2 changed files with 28 additions and 12 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
|
|
@ -261,26 +262,35 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
|
|||
// TODO(v0.3.0): Validate out.
|
||||
_ = outputResolved
|
||||
|
||||
// TODO: return the serialized JSON in a TextContent block, as per spec?
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
|
||||
// But people may use res.Content for other things.
|
||||
if res == nil {
|
||||
res = &CallToolResult{}
|
||||
}
|
||||
if res.Content == nil {
|
||||
res.Content = []Content{} // avoid returning 'null'
|
||||
}
|
||||
res.StructuredContent = out
|
||||
// Marshal the output and put the RawMessage in the StructuredContent field.
|
||||
var outval any = out
|
||||
if elemZero != nil {
|
||||
// Avoid typed nil, which will serialize as JSON null.
|
||||
// Instead, use the zero value of the non-zero
|
||||
// Instead, use the zero value of the unpointered type.
|
||||
var z Out
|
||||
if any(out) == any(z) { // zero is only non-nil if Out is a pointer type
|
||||
res.StructuredContent = elemZero
|
||||
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),
|
||||
}}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
} // end of handler
|
||||
|
||||
return &tt, th, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1175,7 +1175,10 @@ func TestStreamableStateless(t *testing.T) {
|
|||
req(2, "tools/call", &CallToolParams{Name: "greet", Arguments: hiParams{Name: "World"}}),
|
||||
},
|
||||
wantMessages: []jsonrpc.Message{
|
||||
resp(2, &CallToolResult{Content: []Content{&TextContent{Text: "hi World"}}}, nil),
|
||||
resp(2, &CallToolResult{
|
||||
Content: []Content{&TextContent{Text: "hi World"}},
|
||||
StructuredContent: json.RawMessage("null"),
|
||||
}, nil),
|
||||
},
|
||||
wantSessionID: false,
|
||||
},
|
||||
|
|
@ -1186,7 +1189,10 @@ func TestStreamableStateless(t *testing.T) {
|
|||
req(2, "tools/call", &CallToolParams{Name: "greet", Arguments: hiParams{Name: "foo"}}),
|
||||
},
|
||||
wantMessages: []jsonrpc.Message{
|
||||
resp(2, &CallToolResult{Content: []Content{&TextContent{Text: "hi foo"}}}, nil),
|
||||
resp(2, &CallToolResult{
|
||||
Content: []Content{&TextContent{Text: "hi foo"}},
|
||||
StructuredContent: json.RawMessage("null"),
|
||||
}, nil),
|
||||
},
|
||||
wantSessionID: false,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue