diff --git a/README.md b/README.md index 126aa52e..606f579a 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,6 @@ software development kit (SDK) for the Model Context Protocol (MCP). > changes. We aim to tag v1.0.0 in September, 2025. See > https://github.com/modelcontextprotocol/go-sdk/issues/328 for details. -## Design - -The design doc for this SDK is at [design.md](./design/design.md), which was -initially reviewed at -[modelcontextprotocol/discussions/364](https://github.com/orgs/modelcontextprotocol/discussions/364). - -Further design discussion should occur in -[issues](https://github.com/modelcontextprotocol/go-sdk/issues) (for concrete -proposals) or -[discussions](https://github.com/modelcontextprotocol/go-sdk/discussions) for -open-ended discussion. See [CONTRIBUTING.md](/CONTRIBUTING.md) for details. - ## Package documentation The SDK consists of two importable packages: @@ -42,12 +30,53 @@ The SDK consists of two importable packages: - The [`github.com/modelcontextprotocol/go-sdk/jsonrpc`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/jsonrpc) package is for users implementing their own transports. - +- The + [`github.com/modelcontextprotocol/go-sdk/auth`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth) + package provides some primitives for supporting oauth. + +## Getting started + +To get started creating an MCP server, create an `mcp.Server` instance, add +features to it, and then run it over an `mcp.Transport`. For example, this +server adds a single simple tool, and then connects it to clients over +stdin/stdout: + +```go +package main + +import ( + "context" + "log" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) -## Example +type Input struct { + Name string `json:"name" jsonschema:"the name of the person to greet"` +} + +type Output struct { + Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"` +} + +func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (*mcp.CallToolResult, Output, error) { + return nil, Output{Greeting: "Hi " + input.Name}, nil +} + +func main() { + // Create a server with a single tool. + server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil) + mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) + // Run the server over stdin/stdout, until the client disconnects + if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil { + log.Fatal(err) + } +} +``` -In this example, an MCP client communicates with an MCP server running in a -sidecar process: +To communicate with that server, we can similarly create an `mcp.Client` and +connect it to the corresponding server, by running the server command and +communicating over its stdin/stdout: ```go package main @@ -92,42 +121,20 @@ func main() { } ``` -Here's an example of the corresponding server component, which communicates -with its client over stdin/stdout: - -```go -package main - -import ( - "context" - "log" - - "github.com/modelcontextprotocol/go-sdk/mcp" -) - -type HiParams struct { - Name string `json:"name" jsonschema:"the name of the person to greet"` -} - -func SayHi(ctx context.Context, req *mcp.CallToolRequest, args HiParams) (*mcp.CallToolResult, any, error) { - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{Text: "Hi " + args.Name}}, - }, nil, nil -} +The [`examples/`](/examples/) directory contains more example clients and +servers. -func main() { - // Create a server with a single tool. - server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil) +## Design - mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) - // Run the server over stdin/stdout, until the client disconnects - if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil { - log.Fatal(err) - } -} -``` +The design doc for this SDK is at [design.md](./design/design.md), which was +initially reviewed at +[modelcontextprotocol/discussions/364](https://github.com/orgs/modelcontextprotocol/discussions/364). -The [`examples/`](/examples/) directory contains more example clients and servers. +Further design discussion should occur in +[issues](https://github.com/modelcontextprotocol/go-sdk/issues) (for concrete +proposals) or +[discussions](https://github.com/modelcontextprotocol/go-sdk/discussions) for +open-ended discussion. See [CONTRIBUTING.md](/CONTRIBUTING.md) for details. ## Acknowledgements diff --git a/examples/server/toolschemas/main.go b/examples/server/toolschemas/main.go new file mode 100644 index 00000000..36ee0495 --- /dev/null +++ b/examples/server/toolschemas/main.go @@ -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) + } +} diff --git a/internal/readme/README.src.md b/internal/readme/README.src.md index 40e0aa19..d128c935 100644 --- a/internal/readme/README.src.md +++ b/internal/readme/README.src.md @@ -18,18 +18,6 @@ software development kit (SDK) for the Model Context Protocol (MCP). > changes. We aim to tag v1.0.0 in September, 2025. See > https://github.com/modelcontextprotocol/go-sdk/issues/328 for details. -## Design - -The design doc for this SDK is at [design.md](./design/design.md), which was -initially reviewed at -[modelcontextprotocol/discussions/364](https://github.com/orgs/modelcontextprotocol/discussions/364). - -Further design discussion should occur in -[issues](https://github.com/modelcontextprotocol/go-sdk/issues) (for concrete -proposals) or -[discussions](https://github.com/modelcontextprotocol/go-sdk/discussions) for -open-ended discussion. See [CONTRIBUTING.md](/CONTRIBUTING.md) for details. - ## Package documentation The SDK consists of two importable packages: @@ -41,21 +29,39 @@ The SDK consists of two importable packages: - The [`github.com/modelcontextprotocol/go-sdk/jsonrpc`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/jsonrpc) package is for users implementing their own transports. - +- The + [`github.com/modelcontextprotocol/go-sdk/auth`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth) + package provides some primitives for supporting oauth. -## Example +## Getting started -In this example, an MCP client communicates with an MCP server running in a -sidecar process: +To get started creating an MCP server, create an `mcp.Server` instance, add +features to it, and then run it over an `mcp.Transport`. For example, this +server adds a single simple tool, and then connects it to clients over +stdin/stdout: + +%include server/server.go - + +To communicate with that server, we can similarly create an `mcp.Client` and +connect it to the corresponding server, by running the server command and +communicating over its stdin/stdout: %include client/client.go - -Here's an example of the corresponding server component, which communicates -with its client over stdin/stdout: +The [`examples/`](/examples/) directory contains more example clients and +servers. -%include server/server.go - +## Design -The [`examples/`](/examples/) directory contains more example clients and servers. +The design doc for this SDK is at [design.md](./design/design.md), which was +initially reviewed at +[modelcontextprotocol/discussions/364](https://github.com/orgs/modelcontextprotocol/discussions/364). + +Further design discussion should occur in +[issues](https://github.com/modelcontextprotocol/go-sdk/issues) (for concrete +proposals) or +[discussions](https://github.com/modelcontextprotocol/go-sdk/discussions) for +open-ended discussion. See [CONTRIBUTING.md](/CONTRIBUTING.md) for details. ## Acknowledgements diff --git a/internal/readme/server/server.go b/internal/readme/server/server.go index aff5fcd0..e9996027 100644 --- a/internal/readme/server/server.go +++ b/internal/readme/server/server.go @@ -12,20 +12,21 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -type HiParams struct { +type Input struct { Name string `json:"name" jsonschema:"the name of the person to greet"` } -func SayHi(ctx context.Context, req *mcp.CallToolRequest, args HiParams) (*mcp.CallToolResult, any, error) { - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{Text: "Hi " + args.Name}}, - }, nil, nil +type Output struct { + Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"` +} + +func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (*mcp.CallToolResult, Output, error) { + return nil, Output{Greeting: "Hi " + input.Name}, nil } func main() { // Create a server with a single tool. server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil) - mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) // Run the server over stdin/stdout, until the client disconnects if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil { diff --git a/mcp/protocol.go b/mcp/protocol.go index 27860659..a8e4817d 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -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"` } diff --git a/mcp/server.go b/mcp/server.go index 508552e5..3e3b55e3 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -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. -// -// 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. +// 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 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 { diff --git a/mcp/tool.go b/mcp/tool.go index 0797700f..ffccbf30 100644 --- a/mcp/tool.go +++ b/mcp/tool.go @@ -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 {