Skip to content

Commit 79dca41

Browse files
committed
Merge branch 'main' into output-validation
2 parents 470edfd + 1c20560 commit 79dca41

File tree

5 files changed

+58
-45
lines changed

5 files changed

+58
-45
lines changed

examples/server/distributed/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ func parent() {
8080
var wg sync.WaitGroup
8181
childURLs := make([]*url.URL, len(ports))
8282
for i, port := range ports {
83-
wg.Add(1)
8483
childURL := fmt.Sprintf("http://localhost:%s", port)
8584
childURLs[i], err = url.Parse(childURL)
8685
if err != nil {
@@ -89,6 +88,8 @@ func parent() {
8988
cmd := exec.CommandContext(ctx, exe, os.Args[1:]...)
9089
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", childPortVar, port))
9190
cmd.Stderr = os.Stderr
91+
92+
wg.Add(1)
9293
go func() {
9394
defer wg.Done()
9495
log.Printf("starting child %d at %s", i, childURL)

examples/server/middleware/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ func main() {
6464
"duration_ms", duration.Milliseconds(),
6565
"has_result", result != nil,
6666
)
67+
// Log more for tool results.
68+
if ctr, ok := result.(*mcp.CallToolResult); ok {
69+
logger.Info("tool result",
70+
"isError", ctr.IsError,
71+
"structuredContent", ctr.StructuredContent)
72+
}
6773
}
6874
return result, err
6975
}
@@ -103,7 +109,7 @@ func main() {
103109
Content: []mcp.Content{
104110
&mcp.TextContent{Text: message},
105111
},
106-
}, nil, nil
112+
}, message, nil
107113
},
108114
)
109115

mcp/server.go

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"encoding/base64"
1111
"encoding/gob"
12+
"encoding/json"
1213
"fmt"
1314
"iter"
1415
"maps"
@@ -153,10 +154,18 @@ func (s *Server) RemovePrompts(names ...string) {
153154
// If present, the output schema must also have type "object".
154155
//
155156
// When the handler is invoked as part of a CallTool request, req.Params.Arguments
156-
// will be a json.RawMessage. Unmarshaling the arguments and validating them against the
157-
// input schema are the handler author's responsibility.
157+
// will be a json.RawMessage.
158158
//
159-
// Most users should use the top-level function [AddTool].
159+
// Unmarshaling the arguments and validating them against the input schema are the
160+
// caller's responsibility.
161+
//
162+
// Validating the result against the output schema, if any, is the caller's responsibility.
163+
//
164+
// Setting the result's Content, StructuredContent and IsError fields are the caller's
165+
// responsibility.
166+
//
167+
// Most users should use the top-level function [AddTool], which handles all these
168+
// responsibilities.
160169
func (s *Server) AddTool(t *Tool, h ToolHandler) {
161170
if t.InputSchema == nil {
162171
// This prevents the tool author from forgetting to write a schema where
@@ -180,26 +189,6 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
180189
func() bool { s.tools.add(st); return true })
181190
}
182191

183-
// ToolFor returns a shallow copy of t and a [ToolHandler] that wraps h.
184-
//
185-
// If the tool's input schema is nil, it is set to the schema inferred from the In
186-
// type parameter, using [jsonschema.For]. The In type parameter must be a map
187-
// or a struct, so that its inferred JSON Schema has type "object".
188-
//
189-
// For tools that don't return structured output, Out should be 'any'.
190-
// Otherwise, if the tool's output schema is nil the output schema is set to
191-
// the schema inferred from Out, which must be a map or a struct.
192-
//
193-
// Most users will call [AddTool]. Use [ToolFor] if you wish to modify the
194-
// tool's schemas or wrap the ToolHandler before calling [Server.AddTool].
195-
func ToolFor[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler) {
196-
tt, hh, err := toolForErr(t, h)
197-
if err != nil {
198-
panic(fmt.Sprintf("ToolFor: tool %q: %v", t.Name, err))
199-
}
200-
return tt, hh
201-
}
202-
203192
// TODO(v0.3.0): test
204193
func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler, error) {
205194
tt := *t
@@ -265,26 +254,35 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
265254
return nil, fmt.Errorf("tool output: %w", err)
266255
}
267256

268-
// TODO: return the serialized JSON in a TextContent block, as per spec?
269-
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
270-
// But people may use res.Content for other things.
271257
if res == nil {
272258
res = &CallToolResult{}
273259
}
274-
if res.Content == nil {
275-
res.Content = []Content{} // avoid returning 'null'
276-
}
277-
res.StructuredContent = out
260+
// Marshal the output and put the RawMessage in the StructuredContent field.
261+
var outval any = out
278262
if elemZero != nil {
279263
// Avoid typed nil, which will serialize as JSON null.
280-
// Instead, use the zero value of the non-zero
264+
// Instead, use the zero value of the unpointered type.
281265
var z Out
282266
if any(out) == any(z) { // zero is only non-nil if Out is a pointer type
283-
res.StructuredContent = elemZero
267+
outval = elemZero
284268
}
285269
}
270+
outbytes, err := json.Marshal(outval)
271+
if err != nil {
272+
return nil, fmt.Errorf("marshaling output: %w", err)
273+
}
274+
res.StructuredContent = json.RawMessage(outbytes) // avoid a second marshal over the wire
275+
276+
// If the Content field isn't being used, return the serialized JSON in a
277+
// TextContent block, as the spec suggests:
278+
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content.
279+
if res.Content == nil {
280+
res.Content = []Content{&TextContent{
281+
Text: string(outbytes),
282+
}}
283+
}
286284
return res, nil
287-
}
285+
} // end of handler
288286

289287
return &tt, th, nil
290288
}
@@ -329,10 +327,12 @@ func setSchema[T any](sfield **jsonschema.Schema, rfield **jsonschema.Resolved)
329327
// For tools that don't return structured output, Out should be 'any'.
330328
// Otherwise, if the tool's output schema is nil the output schema is set to
331329
// the schema inferred from Out, which must be a map or a struct.
332-
//
333-
// It is a convenience for s.AddTool(ToolFor(t, h)).
334330
func AddTool[In, Out any](s *Server, t *Tool, h ToolHandlerFor[In, Out]) {
335-
s.AddTool(ToolFor(t, h))
331+
tt, hh, err := toolForErr(t, h)
332+
if err != nil {
333+
panic(fmt.Sprintf("AddTool: tool %q: %v", t.Name, err))
334+
}
335+
s.AddTool(tt, hh)
336336
}
337337

338338
// RemoveTools removes the tools with the given names.

mcp/streamable.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ type StreamableHTTPOptions struct {
7272

7373
// TODO: support session retention (?)
7474

75-
// jsonResponse is forwarded to StreamableServerTransport.jsonResponse.
76-
jsonResponse bool
75+
// JSONResponse is forwarded to StreamableServerTransport.jsonResponse.
76+
JSONResponse bool
7777
}
7878

7979
// NewStreamableHTTPHandler returns a new [StreamableHTTPHandler].
@@ -233,7 +233,7 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
233233
transport = &StreamableServerTransport{
234234
SessionID: sessionID,
235235
Stateless: h.opts.Stateless,
236-
jsonResponse: h.opts.jsonResponse,
236+
jsonResponse: h.opts.JSONResponse,
237237
}
238238

239239
// To support stateless mode, we initialize the session with a default
@@ -487,7 +487,7 @@ type stream struct {
487487
// jsonResponse records whether this stream should respond with application/json
488488
// instead of text/event-stream.
489489
//
490-
// See [StreamableServerTransportOptions.jsonResponse].
490+
// See [StreamableServerTransportOptions.JSONResponse].
491491
jsonResponse bool
492492

493493
// signal is a 1-buffered channel, owned by an incoming HTTP request, that signals

mcp/streamable_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func TestStreamableTransports(t *testing.T) {
8484
// Start an httptest.Server with the StreamableHTTPHandler, wrapped in a
8585
// cookie-checking middleware.
8686
handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, &StreamableHTTPOptions{
87-
jsonResponse: useJSON,
87+
JSONResponse: useJSON,
8888
})
8989

9090
var (
@@ -1175,7 +1175,10 @@ func TestStreamableStateless(t *testing.T) {
11751175
req(2, "tools/call", &CallToolParams{Name: "greet", Arguments: hiParams{Name: "World"}}),
11761176
},
11771177
wantMessages: []jsonrpc.Message{
1178-
resp(2, &CallToolResult{Content: []Content{&TextContent{Text: "hi World"}}}, nil),
1178+
resp(2, &CallToolResult{
1179+
Content: []Content{&TextContent{Text: "hi World"}},
1180+
StructuredContent: json.RawMessage("null"),
1181+
}, nil),
11791182
},
11801183
wantSessionID: false,
11811184
},
@@ -1186,7 +1189,10 @@ func TestStreamableStateless(t *testing.T) {
11861189
req(2, "tools/call", &CallToolParams{Name: "greet", Arguments: hiParams{Name: "foo"}}),
11871190
},
11881191
wantMessages: []jsonrpc.Message{
1189-
resp(2, &CallToolResult{Content: []Content{&TextContent{Text: "hi foo"}}}, nil),
1192+
resp(2, &CallToolResult{
1193+
Content: []Content{&TextContent{Text: "hi foo"}},
1194+
StructuredContent: json.RawMessage("null"),
1195+
}, nil),
11901196
},
11911197
wantSessionID: false,
11921198
},

0 commit comments

Comments
 (0)