go-sdk/design/design.md
Jonathan Amsterdam 92ee792363 internal/mcp: fix roots design
Minor changes to the roots part of the design.

Change-Id: I8125aecc5da297594f74d8d7dc5ef3d94dfa8519
Reviewed-on: https://go-review.googlesource.com/c/tools/+/671356
Reviewed-by: Robert Findley <rfindley@google.com>
TryBot-Bypass: Jonathan Amsterdam <jba@google.com>
2025-05-12 06:39:01 -07:00

930 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Go MCP SDK design
This file discusses the design of a Go SDK for the [model context
protocol](https://modelcontextprotocol.io/specification/2025-03-26). It is
intended to seed a GitHub discussion about the official Go MCP SDK.
The github.com/modelcontextprotocol/go-sdk/mcp package contains a prototype that we built
to explore the MCP design space. Many of the ideas there are present in this
document. However, we have diverged and expanded on the APIs of that prototype,
and this document should be considered canonical.
## Similarities and differences with mark3labs/mcp-go
The most popular unofficial MCP SDK for Go is
[mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go). As of this
writing, it is imported by over 400 packages that span over 200 modules.
We admire mcp-go, and seriously considered simply adopting it as a starting
point for this SDK. However, as we looked at doing so, we realized that a
significant amount of its API would probably need to change. In some cases,
mcp-go has older APIs that predated newer variations--an obvious opportunity
for cleanup. In others, it took a batteries-included approach that is probably
not viable for an official SDK. In yet others, we simply think there is room for
API refinement, and we should take this opportunity to consider our options.
Therefore, we wrote this document as though it were proposing a new
implementation. Nevertheless, much of the API discussed here originated from or
was inspired by mcp-go and other unofficial SDKs, and if the consensus of this
discussion is close enough to mcp-go or any other unofficial SDK, we can start
from a fork.
Since mcp-go is so influential and popular, we have noted significant
differences from its API in the sections below. Although the API here is not
compatible with mcp-go, translating between them should be straightforward in
most cases.
(Later, we will provide a detailed translation guide.)
# Requirements
These may be obvious, but it's worthwhile to define goals for an official MCP
SDK. An official SDK should aim to be:
- **complete**: it should be possible to implement every feature of the MCP
spec, and these features should conform to all of the semantics described by
the spec.
- **idiomatic**: as much as possible, MCP features should be modeled using
features of the Go language and its standard library. Additionally, the SDK
should repeat idioms from similar domains.
- **robust**: the SDK itself should be well tested and reliable, and should
enable easy testability for its users.
- **future-proof**: the SDK should allow for future evolution of the MCP spec,
in such a way that we can (as much as possible) avoid incompatible changes to
the SDK API.
- **extensible**: to best serve the previous four concerns, the SDK should be
minimal. However, it should admit extensibility using (for example) simple
interfaces, middleware, or hooks.
# Design considerations
In the sections below, we visit each aspect of the MCP spec, in approximately
the order they are presented by the [official spec](https://modelcontextprotocol.io/specification/2025-03-26)
For each, we discuss considerations for the Go implementation. In many cases an
API is suggested, though in some there may be open questions.
## Foundations
### Package layout
In the sections that follow, it is assumed that most of the MCP API lives in a
single shared package, the `mcp` package. This is inconsistent with other MCP
SDKs, but is consistent with Go packages like `net/http`, `net/rpc`, or
`google.golang.org/grpc`.
Functionality that is not directly related to MCP (like jsonschema or jsonrpc2)
belongs in a separate package.
Therefore, this is the package layout. `module.path` is a placeholder for the
final module path of the mcp module
- `module.path/mcp`: the bulk of the user facing API
- `module.path/jsonschema`: a jsonschema implementation, with validation
- `module.path/internal/jsonrpc2`: a fork of x/tools/internal/jsonrpc2_v2
The JSON-RPC implementation is hidden, to avoid tight coupling. As described in
the next section, the only aspects of JSON-RPC that need to be exposed in the
SDK are the message types, for the purposes of defining custom transports. We
can expose these types from the `mcp` package via aliases or wrappers.
**Difference from mcp-go**: Our `mcp` package includes all the functionality of
mcp-go's `mcp`, `client`, `server` and `transport` packages.
### jsonrpc2 and Transports
The MCP is defined in terms of client-server communication over bidirectional
JSON-RPC message streams. Specifically, version `2025-03-26` of the spec
defines two transports:
- **stdio**: communication with a subprocess over stdin/stdout.
- **streamable http**: communication over a relatively complicated series of
text/event-stream GET and HTTP POST requests.
Additionally, version `2024-11-05` of the spec defined a simpler HTTP transport:
- **sse**: client issues a hanging GET request and receives messages via
`text/event-stream`, and sends messages via POST to a session endpoint.
Furthermore, the spec [states](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#custom-transports) that it must be possible for users to define their own custom transports.
Given the diversity of the transport implementations, they can be challenging
to abstract. However, since JSON-RPC requires a bidirectional stream, we can
use this to model the MCP transport abstraction:
```go
// A Transport is used to create a bidirectional connection between MCP client
// and server.
type Transport interface {
Connect(ctx context.Context) (Stream, error)
}
// A Stream is a bidirectional jsonrpc2 Stream.
type Stream interface {
Read(ctx context.Context) (jsonrpc2.Message, error)
Write(ctx context.Context, jsonrpc2.Message) error
Close() error
}
```
Methods accept a Go `Context` and return an `error`,
as is idiomatic for APIs that do I/O.
A `Transport` is something that connects a logical JSON-RPC
stream, and nothing more. Streams must be closeable in order to
implement client and server shutdown, and therefore conform to the `io.Closer`
interface.
Other SDKs define higher-level transports, with, for example, methods to send a
notification or make a call. Those are jsonrpc2 operations on top of the
logical stream, and the lower-level interface is easier to implement in most
cases, which means it is easier to implement custom transports.
For our prototype, we've used an internal `jsonrpc2` package based on the Go
language server `gopls`, which we propose to fork for the MCP SDK. It already
handles concerns like client/server connection, request lifecycle,
cancellation, and shutdown.
In the MCP Spec, the **stdio** transport uses newline-delimited JSON to
communicate over stdin/stdout. It's possible to model both client side and
server side of this communication with a shared type that communicates over an
`io.ReadWriteCloser`. However, for the purposes of future-proofing, we should
use a distinct types for both client and server stdio transport.
The `CommandTransport` is the client side of the stdio transport, and
connects by starting a command and binding its jsonrpc2 stream to its
stdin/stdout.
```go
// A CommandTransport is a [Transport] that runs a command and communicates
// with it over stdin/stdout, using newline-delimited JSON.
type CommandTransport struct { /* unexported fields */ }
// NewCommandTransport returns a [CommandTransport] that runs the given command
// and communicates with it over stdin/stdout.
func NewCommandTransport(cmd *exec.Command) *CommandTransport
// Connect starts the command, and connects to it over stdin/stdout.
func (*CommandTransport) Connect(ctx context.Context) (Stream, error) {
```
The `StdIOTransport` is the server side of the stdio transport, and connects by
binding to `os.Stdin` and `os.Stdout`.
```go
// A StdIOTransport is a [Transport] that communicates using newline-delimited
// JSON over stdin/stdout.
type StdIOTransport struct { /* unexported fields */ }
func NewStdIOTransport() *StdIOTransport
func (t *StdIOTransport) Connect(context.Context) (Stream, error)
```
The HTTP transport APIs are even more asymmetrical. Since connections are initiated
via HTTP requests, the client developer will create a transport, but
the server developer will typically install an HTTP handler. Internally, the
HTTP handler will create a transport for each new client connection.
Importantly, since they serve many connections, the HTTP handlers must accept a
callback to get an MCP server for each new session.
```go
// SSEHTTPHandler is an http.Handler that serves SSE-based MCP sessions as defined by
// the 2024-11-05 version of the MCP protocol.
type SSEHTTPHandler struct { /* unexported fields */ }
// NewSSEHTTPHandler returns a new [SSEHTTPHandler] that is ready to serve HTTP.
//
// The getServer function is used to bind created servers for new sessions. It
// is OK for getServer to return the same server multiple times.
func NewSSEHTTPHandler(getServer func(request *http.Request) *Server) *SSEHTTPHandler
func (*SSEHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
// Close prevents the SSEHTTPHandler from accepting new sessions, closes active
// sessions, and awaits their graceful termination.
func (*SSEHTTPHandler) Close() error
```
Notably absent are options to hook into low-level request handling for the purposes
of authentication or context injection. These concerns are better handled using
standard HTTP middleware patterns. For middleware at the level of the MCP protocol,
see [Middleware](#Middleware) below.
By default, the SSE handler creates messages endpoints with the
`?sessionId=...` query parameter. Users that want more control over the
management of sessions and session endpoints may write their own handler, and
create `SSEServerTransport` instances themselves, for incoming GET requests.
```go
// A SSEServerTransport is a logical SSE session created through a hanging GET
// request.
//
// When connected, it it returns the following [Stream] implementation:
// - Writes are SSE 'message' events to the GET response.
// - Reads are received from POSTs to the session endpoint, via
// [SSEServerTransport.ServeHTTP].
// - Close terminates the hanging GET.
type SSEServerTransport struct { /* ... */ }
// NewSSEServerTransport creates a new SSE transport for the given messages
// endpoint, and hanging GET response.
//
// Use [SSEServerTransport.Connect] to initiate the flow of messages.
//
// The transport is itself an [http.Handler]. It is the caller's responsibility
// to ensure that the resulting transport serves HTTP requests on the given
// session endpoint.
func NewSSEServerTransport(endpoint string, w http.ResponseWriter) *SSEServerTransport
// ServeHTTP handles POST requests to the transport endpoint.
func (*SSEServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
// Connect sends the 'endpoint' event to the client.
// See [SSEServerTransport] for more details on the [Stream] implementation.
func (*SSEServerTransport) Connect(context.Context) (Stream, error)
```
The SSE client transport is simpler, and hopefully self-explanatory.
```go
type SSEClientTransport struct { /* ... */ }
// NewSSEClientTransport returns a new client transport that connects to the
// SSE server at the provided URL.
func NewSSEClientTransport(url string) (*SSEClientTransport, error) {
// Connect connects through the client endpoint.
func (*SSEClientTransport) Connect(ctx context.Context) (Stream, error)
```
The Streamable HTTP transports are similar to the SSE transport, albeit with a
more complicated implementation. For brevity, we summarize only the differences
from the equivalent SSE types:
```go
// The StreamableHTTPHandler interface is symmetrical to the SSEHTTPHandler.
type StreamableHTTPHandler struct { /* unexported fields */ }
func NewStreamableHTTPHandler(getServer func(request *http.Request) *Server) *StreamableHTTPHandler
func (*StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (*StreamableHTTPHandler) Close() error
// Unlike the SSE transport, the streamable transport constructor accepts a
// session ID, not an endpoint, along with the http response for the request
// that created the session. It is the caller's responsibility to delegate
// requests to this session.
type StreamableServerTransport struct { /* ... */ }
func NewStreamableServerTransport(sessionID string, w http.ResponseWriter) *StreamableServerTransport
func (*StreamableServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (*StreamableServerTransport) Connect(context.Context) (Stream, error)
// The streamable client handles reconnection transparently to the user.
type StreamableClientTransport struct { /* ... */ }
func NewStreamableClientTransport(url string) *StreamableClientTransport {
func (*StreamableClientTransport) Connect(context.Context) (Stream, error)
```
**Differences from mcp-go**: The Go team has a battle-tested JSON-RPC
implementation that we use for gopls, our Go LSP server. We are using the new
version of this library as part of our MCP SDK. It handles all JSON-RPC 2.0
features, including cancellation.
The `Transport` interface here is lower-level than that of mcp-go, but serves a
similar purpose. We believe the lower-level interface is easier to implement.
In mcp-go, server authors create an `MCPServer`, populate it with tools,
resources and so on, and then wrap it in an `SSEServer` or `StdioServer`. These
also use session IDs, which are exposed. Users can manage their own sessions
with `RegisterSession` and `UnregisterSession`.
We find the similarity in names among the three server types to be confusing,
and we could not discover any uses of the session methods in the open-source
ecosystem. In our design, server authors create a `Server`, and then
connect it to a `Transport`. An `SSEHTTPHandler` manages sessions for
incoming SSE connections, but does not expose them. HTTP handlers accept a
server constructor, rather than Server, to allow for stateful or "per-session"
servers.
Individual handlers and transports here have a minimal smaller API, and do not
expose internal details. Customization of things like handlers or session
management is intended to be implemented with middleware and/or compositional
patterns.
### Protocol types
Types needed for the protocol are generated from the
[JSON schema of the MCP spec](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json).
These types will be included in the `mcp` package, but will be unexported
unless they are needed for the user-facing API. Notably, JSON-RPC message types
are elided, since they are handled by the `jsonrpc2` package and should not be
observed by the user.
For user-provided data, we use `json.RawMessage`, so that
marshalling/unmarshalling can be delegated to the business logic of the client
or server.
For union types, which can't be represented in Go (specifically `Content` and
`Resource`), we prefer distinguished unions: struct types with fields
corresponding to the union of all properties for union elements.
For brevity, only a few examples are shown here:
```go
type CallToolParams struct {
Arguments map[string]json.RawMessage `json:"arguments,omitempty"`
Name string `json:"name"`
}
type CallToolResult struct {
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
Content []Content `json:"content"`
IsError bool `json:"isError,omitempty"`
}
// Content is the wire format for content.
//
// The Type field distinguishes the type of the content.
// At most one of Text, MIMEType, Data, and Resource is non-zero.
type Content struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
MIMEType string `json:"mimeType,omitempty"`
Data string `json:"data,omitempty"`
Resource *Resource `json:"resource,omitempty"`
}
// Resource is the wire format for embedded resources.
//
// The URI field describes the resource location. At most one of Text and Blob
// is non-zero.
type Resource struct {
URI string `json:"uri,"`
MIMEType string `json:"mimeType,omitempty"`
Text string `json:"text"`
Blob *string `json:"blob"`
}
```
**Differences from mcp-go**: these types are largely similar, but our type
generation flattens types rather than using struct embedding.
### Clients and Servers
Generally speaking, the SDK is used by creating a `Client` or `Server`
instance, adding features to it, and connecting it to a peer.
However, the SDK must make a non-obvious choice in these APIs: are clients 1:1
with their logical connections? What about servers? Both clients and servers
are stateful: users may add or remove roots from clients, and tools, prompts,
and resources from servers. Additionally, handlers for these features may
themselves be stateful, for example if a tool handler caches state from earlier
requests in the session.
We believe that in the common case, any change to a client or server,
such as adding a tool, is intended for all its peers.
It is therefore more useful to allow multiple connections from a client, and to
a server. This is similar to the `net/http` packages, in which an `http.Client`
and `http.Server` each may handle multiple unrelated connections. When users
add features to a client or server, all connected peers are notified of the
change.
Following the terminology of the
[spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management),
we call the logical connection between a client and server a "session". There
must necessarily be a `ClientSession` and a `ServerSession`, corresponding to
the APIs available from the client and server perspective, respectively.
```
Client Server
⇅ (jsonrpc2) ⇅
ClientSession ⇄ Client Transport ⇄ Server Transport ⇄ ServerSession
```
Sessions are created from either `Client` or `Server` using the `Connect`
method.
```go
type Client struct { /* ... */ }
func NewClient(name, version string, opts *ClientOptions) *Client
func (c *Client) Connect(context.Context, Transport) (*ClientSession, error)
// Methods for adding/removing client features are described below.
type ClientOptions struct { /* ... */ } // described below
type ClientSession struct { /* ... */ }
func (*ClientSession) Client() *Client
func (*ClientSession) Close() error
func (*ClientSession) Wait() error
// Methods for calling through the ClientSession are described below.
type Server struct { /* ... */ }
func NewServer(name, version string, opts *ServerOptions) *Server
func (s *Server) Connect(context.Context, Transport) (*ServerSession, error)
// Methods for adding/removing server features are described below.
type ServerOptions struct { /* ... */ } // described below
type ServerSession struct { /* ... */ }
func (*ServerSession) Server() *Server
func (*ServerSession) Close() error
func (*ServerSession) Wait() error
// Methods for calling through the ServerSession are described below.
```
Here's an example of these APIs from the client side:
```go
client := mcp.NewClient("mcp-client", "v1.0.0", nil)
// Connect to a server over stdin/stdout
transport := mcp.NewCommandTransport(exec.Command("myserver"))
session, err := client.Connect(ctx, transport)
if err != nil { ... }
// Call a tool on the server.
content, err := session.CallTool(ctx, "greet", map[string]any{"name": "you"})
...
return session.Close()
```
And here's an example from the server side:
```go
// Create a server with a single tool.
server := mcp.NewServer("greeter", "v1.0.0", nil)
server.AddTool(mcp.NewTool("greet", "say hi", SayHi))
// Run the server over stdin/stdout, until the client disconnects.
transport := mcp.NewStdIOTransport()
session, err := server.Connect(ctx, transport)
...
return session.Wait()
```
For convenience, we provide `Server.Run` to handle the common case of running a
session until the client disconnects:
```go
func (*Server) Run(context.Context, Transport)
```
**Differences from mcp-go**: the Server APIs are very similar to mcp-go,
though the association between servers and transports is different. In
mcp-go, a single server is bound to what we would call an `SSEHTTPHandler`,
and reused for all client sessions. As discussed above, the transport
abstraction here is differentiated from HTTP serving, and the `Server.Connect`
method provides a consistent API for binding to an arbitrary transport. Servers
here do not have methods for sending notifications or calls, because they are
logically distinct from the `ServerSession`. In mcp-go, servers are `n:1`,
but there is no abstraction of a server session: sessions are addressed in
Server APIs through their `sessionID`: `SendNotificationToAllClients`,
`SendNotificationToClient`, `SendNotificationToSpecificClient`.
The client API here is different, since clients and client sessions are
conceptually distinct. The `ClientSession` is closer to mcp-go's notion of
Client.
For both clients and servers, mcp-go uses variadic options to customize
behavior, whereas an options struct is used here. We felt that in this case, an
options struct would be more readable, and result in cleaner package
documentation.
### Middleware
We provide a mechanism to add MCP-level middleware, which runs after the
request has been parsed, but before any normal handling.
```go
// A Dispatcher dispatches an MCP message to the appropriate handler.
// The params argument will be an XXXParams struct pointer, such as *GetPromptParams.
// The response if err is non-nil should be an XXXResult struct pointer.
type Dispatcher func(ctx context.Context, s *ServerSession, method string, params any) (result any, err error)
// AddDispatchers calls each function from right to left on the previous result, beginning
// with the server's current dispatcher, and installs the result as the new handler.
func (*Server) AddDispatchers(middleware ...func(Handler) Handler))
```
As an example, this code adds server-side logging:
```go
func withLogging(h mcp.Handler) mcp.Handler {
return func(ctx context.Context, s *mcp.ServerSession, method string, params any) (res any, err error) {
log.Printf("request: %s %v", method, params)
defer func() { log.Printf("response: %v, %v", res, err) }()
return h(ctx, s , method, params)
}
}
server.AddDispatchers(withLogging)
```
**Differences from mcp-go**: Version 0.26.0 of mcp-go defines 24 server hooks.
Each hook consists of a field in the `Hooks` struct, a `Hooks.Add` method, and
a type for the hook function. These are rarely used. The most common is
`OnError`, which occurs fewer than ten times in open-source code.
### Errors
With the exception of tool handler errors, protocol errors are handled
transparently as Go errors: errors in server-side feature handlers are
propagated as errors from calls from the `ClientSession`, and vice-versa.
Protocol errors wrap a `JSONRPCError` type which exposes its underlying error
code.
```go
type JSONRPCError struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data,omitempty"`
}
```
As described by the
[spec](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#error-handling),
tool execution errors are reported in tool results.
**Differences from mcp-go**: the `JSONRPCError` type here does not expose
details that are irrelevant or can be inferred from the caller (ID and Method).
Otherwise, this behavior is similar.
### Cancellation
Cancellation is implemented transparently using context cancellation. The user
can cancel an operation by cancelling the associated context:
```go
ctx, cancel := context.WithCancel(ctx)
go session.CallTool(ctx, "slow", map[string]any{})
cancel()
```
When this client call is cancelled, a `"notifications/cancelled"` notification
is sent to the server. However, the client call returns immediately with
`ctx.Err()`: it does not wait for the result from the server.
The server observes a client cancellation as a cancelled context.
### Progress handling
A caller can request progress notifications by setting the `ProgressToken` field on any request.
```go
type ProgressToken any // string or int
type XXXParams struct { // where XXX is each type of call
...
ProgressToken ProgressToken
}
```
Handlers can notify their peer about progress by calling the `NotifyProgress`
method. The notification is only sent if the peer requested it by providing
a progress token.
```go
func (*ClientSession) NotifyProgress(context.Context, *ProgressNotification)
func (*ServerSession) NotifyProgress(context.Context, *ProgressNotification)
```
### Ping / KeepAlive
Both `ClientSession` and `ServerSession` expose a `Ping` method to call "ping"
on their peer.
```go
func (c *ClientSession) Ping(ctx context.Context) error
func (c *ServerSession) Ping(ctx context.Context) error
```
Additionally, client and server sessions can be configured with automatic
keepalive behavior. If the `KeepAlive` option is set to a non-zero duration,
it defines an
interval for regular "ping" requests. If the peer fails to respond to pings
originating from the keepalive check, the session is automatically closed.
```go
type ClientOptions struct {
...
KeepAlive time.Duration
}
type ServerOptions struct {
...
KeepAlive time.Duration
}
```
**Differences from mcp-go**: in mcp-go the `Ping` method is only provided for
client, not server, and the keepalive option is only provided for SSE servers
(as a variadic option).
## Client Features
### Roots
Clients support the MCP Roots feature out of the box, including roots-changed notifications.
Roots can be added and removed from a `Client` with `AddRoots` and `RemoveRoots`:
```go
// AddRoots adds the given roots to the client,
// replacing any with the same URIs,
// and notifies any connected servers.
func (*Client) AddRoots(roots ...Root)
// RemoveRoots removes the roots with the given URIs.
// and notifies any connected servers if the list has changed.
// It is not an error to remove a nonexistent root.
func (*Client) RemoveRoots(uris ...string)
```
Servers can call `ListRoots` to get the roots. If a server installs a
`RootsChangedHandler`, it will be called when the client sends a roots-changed
notification, which happens whenever the list of roots changes after a
connection has been established.
```go
func (*Server) ListRoots(context.Context, *ListRootsParams) (*ListRootsResult, error)
type ServerOptions {
...
// If non-nil, called when a client sends a roots-changed notification.
RootsChangedHandler func(context.Context, *ServerSession, *RootsChangedParams)
}
```
### Sampling
Clients that support sampling are created with a `CreateMessageHandler` option
for handling server calls. To perform sampling, a server calls `CreateMessage`.
```go
type ClientOptions struct {
...
CreateMessageHandler func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error)
}
func (*ServerSession) CreateMessage(context.Context, *CreateMessageParams) (*CreateMessageResult, error)
```
## Server Features
### Tools
A `Tool` is a logical MCP tool, generated from the MCP spec, and a `ServerTool`
is a tool bound to a tool handler.
```go
type Tool struct {
Annotations *ToolAnnotations `json:"annotations,omitempty"`
Description string `json:"description,omitempty"`
InputSchema *jsonschema.Schema `json:"inputSchema"`
Name string `json:"name"`
}
type ToolHandler func(context.Context, *ServerSession, map[string]json.RawMessage) (*CallToolResult, error)
type ServerTool struct {
Tool Tool
Handler ToolHandler
}
```
Add tools to a server with `AddTools`:
```go
server.AddTools(
mcp.NewTool("add", "add numbers", addHandler),
mcp.NewTools("subtract, subtract numbers", subHandler))
```
Remove them by name with `RemoveTools`:
```go
server.RemoveTools("add", "subtract")
```
A tool's input schema, expressed as a [JSON Schema](https://json-schema.org),
provides a way to validate the tool's input. One of the challenges in defining
tools is the need to associate them with a Go function, yet support the
arbitrary complexity of JSON Schema. To achieve this, we have seen two primary
approaches:
1. Use reflection to generate the tool's input schema from a Go type (à la
`metoro-io/mcp-golang`)
2. Explicitly build the input schema (à la `mark3labs/mcp-go`).
Both of these have their advantages and disadvantages. Reflection is nice,
because it allows you to bind directly to a Go API, and means that the JSON
schema of your API is compatible with your Go types by construction. It also
means that concerns like parsing and validation can be handled automatically.
However, it can become cumbersome to express the full breadth of JSON schema
using Go types or struct tags, and sometimes you want to express things that
aren’t naturally modeled by Go types, like unions. Explicit schemas are simple
and readable, and give the caller full control over their tool definition, but
involve significant boilerplate.
We have found that a hybrid model works well, where the _initial_ schema is
derived using reflection, but any customization on top of that schema is
applied using variadic options. We achieve this using a `NewTool` helper, which
generates the schema from the input type, and wraps the handler to provide
parsing and validation. The schema (and potentially other features) can be
customized using ToolOptions.
```go
// NewTool creates a Tool using reflection on the given handler.
func NewTool[TInput any](name, description string, handler func(context.Context, *ServerSession, TInput) ([]Content, error), opts …ToolOption) *ServerTool
type ToolOption interface { /* ... */ }
```
`NewTool` determines the input schema for a Tool from the struct used
in the handler. Each struct field that would be marshaled by `encoding/json.Marshal`
becomes a property of the schema. The property is required unless
the field's `json` tag specifies "omitempty" or "omitzero" (new in Go 1.24).
For example, given this struct:
```go
struct {
Name string `json:"name"`
Count int `json:"count,omitempty"`
Choices []string
Password []byte `json:"-"`
}
```
"name" and "Choices" are required, while "count" is optional.
As of this writing, the only `ToolOption` is `Input`, which allows customizing the
input schema of the tool using schema options. These schema options are
recursive, in the sense that they may also be applied to properties.
```go
func Input(...SchemaOption) ToolOption
type Property(name string, opts ...SchemaOption) SchemaOption
type Description(desc string) SchemaOption
// etc.
```
For example:
```go
NewTool(name, description, handler,
Input(Property("count", Description("size of the inventory"))))
```
The most recent JSON Schema spec defines over 40 keywords. Providing them all
as options would bloat the API despite the fact that most would be very rarely
used. For less common keywords, use the `Schema` option to set the schema
explicitly:
```go
NewTool(name, description, handler,
Input(Property("Choices", Schema(&jsonschema.Schema{UniqueItems: true}))))
```
Schemas are validated on the server before the tool handler is called.
Since all the fields of the Tool struct are exported, a Tool can also be created
directly with assignment or a struct literal.
**Differences from mcp-go**: using variadic options to configure tools was
signficantly inspired by mcp-go. However, the distinction between `ToolOption`
and `SchemaOption` allows for recursive application of schema options.
For example, that limitation is visible in [this
code](https://github.com/DCjanus/dida365-mcp-server/blob/master/cmd/mcp/tools.go#L315),
which must resort to untyped maps to express a nested schema.
Additionally, the `NewTool` helper provides a means for building a tool from a
Go function using reflection, that automatically handles parsing and validation
of inputs.
We provide a full JSON Schema implementation for validating tool input schemas
against incoming arguments. The `jsonschema.Schema` type provides exported
features for all keywords in the JSON Schema draft2020-12 spec. Tool definers
can use it to construct any schema they want, so there is no need to provide
options for all of them. When combined with schema inference from input
structs, we found that we needed only three options to cover the common cases,
instead of mcp-go's 23. For example, we will provide `Enum`, which occurs 125
times in open source code, but not MinItems, MinLength or MinProperties, which
each occur only once (and in an SDK that wraps mcp-go).
For registering tools, we provide only `AddTools`; mcp-go's `SetTools`,
`AddTool`, `AddSessionTool`, and `AddSessionTools` are deemed unnecessary.
(similarly for Delete/Remove).
### Prompts
Use `NewPrompt` to create a prompt.
As with tools, prompt argument schemas can be inferred from a struct, or obtained
from options.
```go
func NewPrompt[TReq any](name, description string,
handler func(context.Context, *ServerSession, TReq) (*GetPromptResult, error),
opts ...PromptOption) *ServerPrompt
```
Use `AddPrompts` to add prompts to the server, and `RemovePrompts`
to remove them by name.
```go
type codeReviewArgs struct {
Code string `json:"code"`
}
func codeReviewHandler(context.Context, *ServerSession, codeReviewArgs) {...}
server.AddPrompts(
NewPrompt("code_review", "review code", codeReviewHandler,
Argument("code", Description("the code to review"))))
server.RemovePrompts("code_review")
```
Clients can call `ListPrompts` to list the available prompts and `GetPrompt` to get one.
```go
func (*ClientSession) ListPrompts(context.Context, *ListPromptParams) (*ListPromptsResult, error)
func (*ClientSession) GetPrompt(context.Context, *GetPromptParams) (*GetPromptResult, error)
```
**Differences from mcp-go**: We provide a `NewPrompt` helper to bind a prompt
handler to a Go function using reflection to derive its arguments. We provide
`RemovePrompts` to remove prompts from the server.
### Resources and resource templates
Servers have Add and Remove methods for resources and resource templates:
```go
func (*Server) AddResources(resources ...*Resource)
func (*Server) RemoveResources(names ...string)
func (*Server) AddResourceTemplates(templates...*ResourceTemplate)
func (*Server) RemoveResourceTemplates(names ...string)
```
Clients call `ListResources` to list the available resources, `ReadResource` to read
one of them, and `ListResourceTemplates` to list the templates:
```go
func (*ClientSession) ListResources(context.Context, *ListResourcesParams) (*ListResourcesResult, error)
func (*ClientSession) ReadResource(context.Context, *ReadResourceParams) (*ReadResourceResult, error)
func (*ClientSession) ListResourceTemplates(context.Context, *ListResourceTemplatesParams) (*ListResourceTemplatesResult, error)
```
<!-- TODO: subscriptions -->
### ListChanged notifications
When a list of tools, prompts or resources changes as the result of an AddXXX
or RemoveXXX call, the server informs all its connected clients by sending the
corresponding type of notification.
A client will receive these notifications if it was created with the corresponding option:
```go
type ClientOptions struct {
...
ToolListChangedHandler func(context.Context, *ClientConnection, *ToolListChangedParams)
PromptListChangedHandler func(context.Context, *ClientConnection, *PromptListChangedParams)
ResourceListChangedHandler func(context.Context, *ClientConnection, *ResourceListChangedParams)
}
```
**Differences from mcp-go**: mcp-go instead provides a general `OnNotification`
handler. For type-safety, and to hide JSON RPC details, we provide
feature-specific handlers here.
### Completion
Clients call `Complete` to request completions.
```go
func (*ClientSession) Complete(context.Context, *CompleteParams) (*CompleteResult, error)
```
Servers automatically handle these requests based on their collections of
prompts and resources.
**Differences from mcp-go**: the client API is similar. mcp-go has not yet
defined its server-side behavior.
### Logging
ServerSessions have access to a `slog.Logger` that writes to the client. A call to
a log method like `Info`is translated to a `LoggingMessageNotification` as
follows:
- An attribute with key "logger" is used to populate the "logger" field of the notification.
- The remaining attributes and the message populate the "data" field with the
output of a `slog.JSONHandler`: The result is always a JSON object, with the
key "msg" for the message.
- The standard slog levels `Info`, `Debug`, `Warn` and `Error` map to the
corresponding levels in the MCP spec. The other spec levels will be mapped
to integers between the slog levels. For example, "notice" is level 2 because
it is between "warning" (slog value 4) and "info" (slog value 0).
The `mcp` package defines consts for these levels. To log at the "notice"
level, a handler would call `session.Log(ctx, mcp.LevelNotice, "message")`.
### Pagination
<!-- TODO: needs design -->