all: add examples of customizing tool schemas, and clarify documentation (#421)

Add example/server/toolschemas, which demonstrates various ways to
customize tool schemas, and illustrates the differences between
ToolHandler and ToolHandlerFor.

Fixes #385
Fixes #386
For #368
This commit is contained in:
Robert Findley 2025-09-05 14:11:11 -04:00 • committed by GitHub
commit 40b6bd30f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 309 additions and 100 deletions

View file

@ -49,7 +49,9 @@ type CallToolParams struct {
Arguments any `json:"arguments,omitempty"`
}
// CallToolParamsRaw is passed to tool handlers on the server.
// CallToolParamsRaw is passed to tool handlers on the server. Its arguments
// are not yet unmarshaled (hence "raw"), so that the handlers can perform
// unmarshaling themselves.
type CallToolParamsRaw struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.
@ -58,29 +60,48 @@ type CallToolParamsRaw struct {
Arguments json.RawMessage `json:"arguments,omitempty"`
}
// The server's response to a tool call.
// A CallToolResult is the server's response to a tool call.
//
// The [ToolHandler] and [ToolHandlerFor] handler functions return this result,
// though [ToolHandlerFor] populates much of it automatically as documented at
// each field.
type CallToolResult struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.
Meta `json:"_meta,omitempty"`
// A list of content objects that represent the unstructured result of the tool
// call.
//
// When using a [ToolHandlerFor] with structured output, if Content is unset
// it will be populated with JSON text content corresponding to the
// structured output value.
Content []Content `json:"content"`
// An optional JSON object that represents the structured result of the tool
// call.
// StructuredContent is an optional value that represents the structured
// result of the tool call. It must marshal to a JSON object.
//
// When using a [ToolHandlerFor] with structured output, you should not
// populate this field. It will be automatically populated with the typed Out
// value.
StructuredContent any `json:"structuredContent,omitempty"`
// Whether the tool call ended in an error.
// IsError reports whether the tool call ended in an error.
//
// If not set, this is assumed to be false (the call was successful).
//
// Any errors that originate from the tool should be reported inside the result
// object, with isError set to true, not as an MCP protocol-level error
// response. Otherwise, the LLM would not be able to see that an error occurred
// and self-correct.
// Any errors that originate from the tool should be reported inside the
// Content field, with IsError set to true, not as an MCP protocol-level
// error response. Otherwise, the LLM would not be able to see that an error
// occurred and self-correct.
//
// However, any errors in finding the tool, an error indicating that the
// server does not support tool calls, or any other exceptional conditions,
// should be reported as an MCP error response.
//
// When using a [ToolHandlerFor], this field is automatically set when the
// tool handler returns an error, and the error string is included as text in
// the Content field.
IsError bool `json:"isError,omitempty"`
}

View file

@ -321,22 +321,18 @@ func setSchema[T any](sfield **jsonschema.Schema, rfield **jsonschema.Resolved)
}
// AddTool adds a tool and typed tool handler to the server.
//
// If the tool's input schema is nil, it is set to the schema inferred from the
// In type parameter, using [jsonschema.For]. The In type parameter must be a
// In type parameter, using [jsonschema.For]. The In type argument must be a
// map or a struct, so that its inferred JSON Schema has type "object".
//
// For tools that don't return structured output, Out should be 'any'.
// Otherwise, if the tool's output schema is nil the output schema is set to
// the schema inferred from Out, which must be a map or a struct.
// If the tool's output schema is nil, and the Out type is not 'any', the
// output schema is set to the schema inferred from the Out type argument,
// which also must be a map or struct.
//
// The In argument to the handler will contain the unmarshaled arguments from
// CallToolRequest.Params.Arguments. Most users can ignore the [CallToolRequest]
// argument to the handler.
//
// The handler's Out return value will be used to populate [CallToolResult.StructuredContent].
// If the handler returns a non-nil error, [CallToolResult.IsError] will be set to true,
// and [CallToolResult.Content] will be set to the text of the error.
// Most users can ignore the [CallToolResult] return value.
// Unlike [Server.AddTool], AddTool does a lot automatically, and forces tools
// to conform to the MCP spec. See [ToolHandlerFor] for a detailed description
// of this automatic behavior.
func AddTool[In, Out any](s *Server, t *Tool, h ToolHandlerFor[In, Out]) {
tt, hh, err := toolForErr(t, h)
if err != nil {

View file

@ -14,14 +14,46 @@ import (
)
// A ToolHandler handles a call to tools/call.
// This is a low-level API, for use with [Server.AddTool].
// Most users will write a [ToolHandlerFor] and install it with [AddTool].
//
// This is a low-level API, for use with [Server.AddTool]. It does not do any
// pre- or post-processing of the request or result: the params contain raw
// arguments, no input validation is performed, and the result is returned to
// the user as-is, without any validation of the output.
//
// Most users will write a [ToolHandlerFor] and install it with the generic
// [AddTool] function.
//
// If ToolHandler returns an error, it is treated as a protocol error. By
// contrast, [ToolHandlerFor] automatically populates [CallToolResult.IsError]
// and [CallToolResult.Content] accordingly.
type ToolHandler func(context.Context, *CallToolRequest) (*CallToolResult, error)
// A ToolHandlerFor handles a call to tools/call with typed arguments and results.
//
// Use [AddTool] to add a ToolHandlerFor to a server.
// Most users can ignore the [CallToolRequest] argument and [CallToolResult] return value.
type ToolHandlerFor[In, Out any] func(context.Context, *CallToolRequest, In) (*CallToolResult, Out, error)
//
// Unlike [ToolHandler], [ToolHandlerFor] provides significant functionality
// out of the box, and enforces that the tool conforms to the MCP spec:
// - The In type provides a default input schema for the tool, though it may
// be overridden in [AddTool].
// - The input value is automatically unmarshaled from req.Params.Arguments.
// - The input value is automatically validated against its input schema.
// Invalid input is rejected before getting to the handler.
// - If the Out type is not the empty interface [any], it provides the
// default output schema for the tool (which again may be overridden in
// [AddTool]).
// - The Out value is used to populate result.StructuredOutput.
// - If [CallToolResult.Content] is unset, it is populated with the JSON
// content of the output.
// - An error result is treated as a tool error, rather than a protocol
// error, and is therefore packed into CallToolResult.Content, with
// [IsError] set.
//
// For these reasons, most users can ignore the [CallToolRequest] argument and
// [CallToolResult] return values entirely. In fact, it is permissible to
// return a nil CallToolResult, if you only care about returning a output value
// or error. The effective result will be populated as described above.
type ToolHandlerFor[In, Out any] func(_ context.Context, request *CallToolRequest, input In) (result *CallToolResult, output Out, _ error)
// A serverTool is a tool definition that is bound to a tool handler.
type serverTool struct {