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

View file

@ -0,0 +1,146 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// The toolschemas example demonstrates how to create tools using both the
// low-level [ToolHandler] and high level [ToolHandlerFor], as well as how to
// customize schemas in both cases.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// Input is the input into all the tools handlers below.
type Input struct {
Name string `json:"name" jsonschema:"the person to greet"`
}
// Output is the structured output of the tool.
//
// Not every tool needs to have structured output.
type Output struct {
Greeting string `json:"greeting" jsonschema:"the greeting to send to the user"`
}
// simpleGreeting is an [mcp.ToolHandlerFor] that only cares about input and output.
func simpleGreeting(_ context.Context, _ *mcp.CallToolRequest, input Input) (*mcp.CallToolResult, Output, error) {
return nil, Output{"Hi " + input.Name}, nil
}
// manualGreeter handles the parsing and validation of input and output manually.
//
// Therfore, it needs to close over its resolved schemas, to use them in
// validation.
type manualGreeter struct {
inputSchema *jsonschema.Resolved
outputSchema *jsonschema.Resolved
}
func (t *manualGreeter) greet(_ context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// errf produces a 'tool error', embedding the error in a CallToolResult.
errf := func(format string, args ...any) *mcp.CallToolResult {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf(format, args...)}},
IsError: true,
}
}
// Handle the parsing and validation of input and output.
//
// Note that errors here are treated as tool errors, not protocol errors.
var input Input
if err := json.Unmarshal(req.Params.Arguments, &input); err != nil {
return errf("failed to unmarshal arguments: %v", err), nil
}
if err := t.inputSchema.Validate(input); err != nil {
return errf("invalid input: %v", err), nil
}
output := Output{Greeting: "Hi " + input.Name}
if err := t.outputSchema.Validate(output); err != nil {
return errf("tool produced invalid output: %v", err), nil
}
outputJSON, err := json.Marshal(output)
if err != nil {
return errf("output failed to marshal: %v", err), nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: string(outputJSON)}},
StructuredContent: output,
}, nil
}
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil)
// Add the 'greeting' tool in a few different ways.
// First, we can just use [mcp.AddTool], and get the out-of-the-box handling
// it provides:
mcp.AddTool(server, &mcp.Tool{Name: "simple greeting"}, simpleGreeting)
// Next, we can create our schemas entirely manually, and add them using
// [mcp.Server.AddTool]. Since we're working manually, we can add some
// constraints on the length of the name.
//
// We don't need to do all this work: below, we use jsonschema.For to start
// from the default schema.
var (
manual manualGreeter
err error
)
inputSchema := &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"name": {Type: "string", MaxLength: jsonschema.Ptr(10)},
},
}
manual.inputSchema, err = inputSchema.Resolve(nil)
if err != nil {
log.Fatal(err)
}
outputSchema := &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"greeting": {Type: "string"},
},
}
manual.outputSchema, err = outputSchema.Resolve(nil)
if err != nil {
log.Fatal(err)
}
server.AddTool(&mcp.Tool{
Name: "manual greeting",
InputSchema: inputSchema,
OutputSchema: outputSchema,
}, manual.greet)
// Finally, note that we can also use custom schemas with a ToolHandlerFor.
// We can do this in two ways: by using one of the schema values constructed
// above, or by using jsonschema.For and adjusting the resulting schema.
mcp.AddTool(server, &mcp.Tool{
Name: "customized greeting 1",
InputSchema: inputSchema,
// OutputSchema will still be derived from Output.
}, simpleGreeting)
customSchema, err := jsonschema.For[Input](nil)
if err != nil {
log.Fatal(err)
}
customSchema.Properties["name"].MaxLength = jsonschema.Ptr(10)
mcp.AddTool(server, &mcp.Tool{
Name: "customized greeting 2",
InputSchema: customSchema,
}, simpleGreeting)
// Now run the server.
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Printf("Server failed: %v", err)
}
}