diff --git a/.github/workflows/readme-check.yml b/.github/workflows/readme-check.yml index bed3ff44..8709be11 100644 --- a/.github/workflows/readme-check.yml +++ b/.github/workflows/readme-check.yml @@ -1,11 +1,13 @@ name: README Check on: - workflow_dispatch: + workflow_dispatch: pull_request: paths: - 'internal/readme/**' - 'README.md' - + - 'internal/docs/**' + - 'docs/**' + permissions: contents: read @@ -17,15 +19,15 @@ jobs: uses: actions/setup-go@v5 - name: Check out code uses: actions/checkout@v4 - - name: Check README is up-to-date + - name: Check docs is up-to-date run: | - go generate ./internal/readme + go generate ./... if [ -n "$(git status --porcelain)" ]; then - echo "ERROR: README.md is not up-to-date!" + echo "ERROR: docs are not up-to-date!" echo "" - echo "The README.md file differs from what would be generated by `go generate ./internal/readme`." - echo "Please update internal/readme/README.src.md instead of README.md directly," - echo "then run `go generate ./internal/readme` to regenerate README.md." + echo "The docs differ from what would be generated by `go generate ./...`." + echo "Please update internal/**/*.src.md instead of directly editing README.md or docs/ files," + echo "then run `go generate ./...` to regenerate docs." echo "" echo "Changes:" git status --porcelain @@ -34,4 +36,4 @@ jobs: git diff exit 1 fi - echo "README.md is up-to-date" + echo "Docs are up-to-date." diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..81e7f5f7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,40 @@ + +These docs are a work-in-progress. + +# Features + +These docs mirror the [official MCP spec](https://modelcontextprotocol.io/specification/2025-06-18). + +## Base Protocol + +1. [Lifecycle (Clients, Servers, and Sessions)](protocol.md#lifecycle). +1. [Transports](protocol.md#transports) + 1. [Stdio transport](protocol.md#stdio-transport) + 1. [Streamable transport](protocol.md#streamable-transport) + 1. [Custom transports](protocol.md#stateless-mode) +1. [Authorization](protocol.md#authorization) +1. [Security](protocol.md#security) +1. [Utilities](protocol.md#utilities) + 1. [Cancellation](utilities.md#cancellation) + 1. [Ping](utilities.md#ping) + 1. [Progress](utilities.md#progress) + +## Client Features + +1. [Roots](client.md#roots) +1. [Sampling](client.md#sampling) +1. [Elicitation](clients.md#elicitation) + +## Server Features + +1. [Prompts](server.md#prompts) +1. [Resources](server.md#resources) +1. [Tools](tools.md) +1. [Utilities](server.md#utilities) + 1. [Completion](server.md#completion) + 1. [Logging](server.md#logging) + 1. [Pagination](server.md#pagination) + +# TroubleShooting + +See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide. diff --git a/docs/client.md b/docs/client.md new file mode 100644 index 00000000..13c12d57 --- /dev/null +++ b/docs/client.md @@ -0,0 +1,168 @@ + +# Support for MCP client features + +1. [Roots](#roots) +1. [Sampling](#sampling) +1. [Elicitation](#elicitation) + +## Roots + +MCP allows clients to specify a set of filesystem +["roots"](https://modelcontextprotocol.io/specification/2025-06-18/client/roots). +The SDK supports this as follows: + +**Client-side**: The SDK client always has the `roots.listChanged` capability. +To add roots to a client, use the +[`Client.AddRoots`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AddRoots) +and +[`Client.RemoveRoots`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Client.RemoveRoots) +methods. If any servers are already [connected](protocol.md#lifecycle) to the +client, a call to `AddRoot` or `RemoveRoots` will result in a +`notifications/roots/list_changed` notification to each connected server. + +**Server-side**: To query roots from the server, use the +[`ServerSession.ListRoots`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.ListRoots) +method. To receive notifications about root changes, set +[`ServerOptions.RootsListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.RootsListChangedHandler). + +```go +func Example_roots() { + ctx := context.Background() + + // Create a client with a single root. + c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + c.AddRoots(&mcp.Root{URI: "file://a"}) + + // Now create a server with a handler to receive notifications about roots. + rootsChanged := make(chan struct{}) + handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) { + rootList, err := req.Session.ListRoots(ctx, nil) + if err != nil { + log.Fatal(err) + } + var roots []string + for _, root := range rootList.Roots { + roots = append(roots, root.URI) + } + fmt.Println(roots) + close(rootsChanged) + } + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{ + RootsListChangedHandler: handleRootsChanged, + }) + + // Connect the server and client... + t1, t2 := mcp.NewInMemoryTransports() + if _, err := s.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + if _, err := c.Connect(ctx, t2, nil); err != nil { + log.Fatal(err) + } + + // ...and add a root. The server is notified about the change. + c.AddRoots(&mcp.Root{URI: "file://b"}) + <-rootsChanged + // Output: [file://a file://b] +} +``` + +## Sampling + +[Sampling](https://modelcontextprotocol.io/specification/2025-06-18/client/sampling) +is a way for servers to leverage the client's AI capabilities. It is +implemented in the SDK as follows: + +**Client-side**: To add the `sampling` capability to a client, set +[`ClientOptions.CreateMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.CreateMessageHandler). +This function is invoked whenever the server requests sampling. + +**Server-side**: To use sampling from the server, call +[`ServerSession.CreateMessage`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.CreateMessage). + +```go +func Example_sampling() { + ctx := context.Background() + + // Create a client with a sampling handler. + c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{ + CreateMessageHandler: func(_ context.Context, req *mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) { + return &mcp.CreateMessageResult{ + Content: &mcp.TextContent{ + Text: "would have created a message", + }, + }, nil + }, + }) + + // Connect the server and client... + ct, st := mcp.NewInMemoryTransports() + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + session, err := s.Connect(ctx, st, nil) + if err != nil { + log.Fatal(err) + } + defer session.Close() + + if _, err := c.Connect(ctx, ct, nil); err != nil { + log.Fatal(err) + } + + msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{}) + if err != nil { + log.Fatal(err) + } + fmt.Println(msg.Content.(*mcp.TextContent).Text) + // Output: would have created a message +} +``` + +## Elicitation + +[Elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) +allows servers to request user inputs. It is implemented in the SDK as follows: + +**Client-side**: To add the `elicitation` capability to a client, set +[`ClientOptions.ElicitationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ElicitationHandler). +The elicitation handler must return a result that matches the requested schema; +otherwise, elicitation returns an error. + +**Server-side**: To use elicitation from the server, call +[`ServerSession.Elicit`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Elicit). + +```go +func Example_elicitation() { + ctx := context.Background() + ct, st := mcp.NewInMemoryTransports() + + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + ss, err := s.Connect(ctx, st, nil) + if err != nil { + log.Fatal(err) + } + defer ss.Close() + + c := mcp.NewClient(testImpl, &mcp.ClientOptions{ + ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) { + return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil + }, + }) + if _, err := c.Connect(ctx, ct, nil); err != nil { + log.Fatal(err) + } + res, err := ss.Elicit(ctx, &mcp.ElicitParams{ + Message: "This should fail", + RequestedSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "test": {Type: "string"}, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + fmt.Println(res.Content["test"]) + // Output: value +} +``` diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 00000000..dbc4c1cb --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,368 @@ + +# Support for the MCP base protocol + +1. [Lifecycle](#lifecycle) +1. [Transports](#transports) + 1. [Stdio Transport](#stdio-transport) + 1. [Streamable Transport](#streamable-transport) + 1. [Custom transports](#custom-transports) + 1. [Concurrency](#concurrency) +1. [Authorization](#authorization) +1. [Security](#security) +1. [Utilities](#utilities) + 1. [Cancellation](#cancellation) + 1. [Ping](#ping) + 1. [Progress](#progress) + +## Lifecycle + +The SDK provides an API for defining both MCP clients and servers, and +connecting them over various transports. When a client and server are +connected, it creates a logical session, which follows the MCP spec's +[lifecycle](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle). + +In this SDK, both a +[`Client`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Client) +and +[`Server`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server) +can handle multiple peers. Every time a new peer is connected, it creates a new +session. + +- A `Client` is a logical MCP client, configured with various + [`ClientOptions`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions). +- When a client is connected to a server using + [`Client.Connect`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Client.Connect), + it creates a + [`ClientSession`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession). + This session is initialized during the `Connect` method, and provides methods + to communicate with the server peer. +- A `Server` is a logical MCP server, configured with various + [`ServerOptions`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions). +- When a server is connected to a client using + [`Server.Connect`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.Connect), + it creates a + [`ServerSession`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession). + This session is not initialized until the client sends the + `notifications/initialized` message. Use `ServerOptions.InitializedHandler` + to listen for this event, or just use the session through various feature + handlers (such as a `ToolHandler`). Requests to the server are rejected until + the client has initialized the session. + +Both `ClientSession` and `ServerSession` have a `Close` method to terminate the +session, and a `Wait` method to await session termination by the peer. Typically, +it is the client's responsibility to end the session. + +```go +func Example_lifecycle() { + ctx := context.Background() + + // Create a client and server. + // Wait for the client to initialize the session. + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{ + InitializedHandler: func(context.Context, *mcp.InitializedRequest) { + fmt.Println("initialized!") + }, + }) + + // Connect the server and client using in-memory transports. + // + // Connect the server first so that it's ready to receive initialization + // messages from the client. + t1, t2 := mcp.NewInMemoryTransports() + serverSession, err := server.Connect(ctx, t1, nil) + if err != nil { + log.Fatal(err) + } + clientSession, err := client.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + + // Now shut down the session by closing the client, and waiting for the + // server session to end. + if err := clientSession.Close(); err != nil { + log.Fatal(err) + } + if err := serverSession.Wait(); err != nil { + log.Fatal(err) + } + // Output: initialized! +} +``` + +## Transports + +A +[transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) +can be used to send JSON-RPC messages from client to server, or vice-versa. + +In the SDK, this is achieved by implementing the +[`Transport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Transport) +interface, which creates a (logical) bidirectional stream of JSON-RPC messages. +Most transport implementations described below are specific to either the +client or server: a "client transport" is something that can be used to connect +a client to a server, and a "server transport" is something that can be used to +connect a server to a client. However, it's possible for a transport to be both +a client and server transport, such as the `InMemoryTransport` used in the +lifecycle example above. + +Transports should not be reused for multiple connections: if you need to create +multiple connections, use different transports. + +### Stdio Transport + +In the +[`stdio`](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) +transport clients communicate with an MCP server running in a subprocess using +newline-delimited JSON over its stdin/stdout. + +**Client-side**: the client side of the `stdio` transport is implemented by +[`CommandTransport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#CommandTransport), +which starts the a `exec.Cmd` as a subprocess and communicates over its +stdin/stdout. + +**Server-side**: the server side of the `stdio` transport is implemented by +`StdioTransport`, which connects over the current processes `os.Stdin` and +`os.Stdout`. + +### Streamable Transport + +The [streamable +transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) +API is implemented across three types: + +- `StreamableHTTPHandler`: an`http.Handler` that serves streamable MCP + sessions. +- `StreamableServerTransport`: a `Transport` that implements the server side of + the streamable transport. +- `StreamableClientTransport`: a `Transport` that implements the client side of + the streamable transport. + +To create a streamable MCP server, you create a `StreamableHTTPHandler` and +pass it an `mcp.Server`: + +```go +func ExampleStreamableHTTPHandler() { + // Create a new streamable handler, using the same MCP server for every request. + // + // Here, we configure it to serves application/json responses rather than + // text/event-stream, just so the output below doesn't use random event ids. + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil) + handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { + return server + }, &mcp.StreamableHTTPOptions{JSONResponse: true}) + httpServer := httptest.NewServer(handler) + defer httpServer.Close() + + // The SDK is currently permissive of some missing keys in "params". + resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL) + fmt.Println(resp) + // Output: + // {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.1.0"}}} +} +``` + +The `StreamableHTTPHandler` handles the HTTP requests and creates a new +`StreamableServerTransport` for each new session. The transport is then used to +communicate with the client. + +On the client side, you create a `StreamableClientTransport` and use it to +connect to the server: + +```go +transport := &mcp.StreamableClientTransport{ + Endpoint: "http://localhost:8080/mcp", +} +client, err := mcp.Connect(ctx, transport, &mcp.ClientOptions{...}) +``` + +The `StreamableClientTransport` handles the HTTP requests and communicates with +the server using the streamable transport protocol. + +#### Stateless Mode + + + +#### Sessionless mode + + + +### Custom transports + + + +### Concurrency + +In general, MCP offers no guarantees about concurrency semantics: if a client +or server sends a notification, the spec says nothing about when the peer +observes that notification relative to other request. However, the Go SDK +implements the following heuristics: + +- If a notifying method (such as `notifications/progress` or + `notifications/initialized`) returns, then it is guaranteed that the peer + observes that notification before other notifications or calls from the same + client goroutine. +- Calls (such as `tools/call`) are handled asynchronously with respect to + each other. + +See +[modelcontextprotocol/go-sdk#26](https://github.com/modelcontextprotocol/go-sdk/issues/26) +for more background. + +## Authorization + + + +## Security + + + +## Utilities + +### Cancellation + +Cancellation is implemented with context cancellation. Cancelling a context +used in a method on `ClientSession` or `ServerSession` will terminate the RPC +and send a "notifications/cancelled" message to the peer. + +When an RPC exits due to a cancellation error, there's a guarantee that the +cancellation notification has been sent, but there's no guarantee that the +server has observed it (see [concurrency](#concurrency)). + +```go +func Example_cancellation() { + // For this example, we're going to be collecting observations from the + // server and client. + var clientResult, serverResult string + var wg sync.WaitGroup + wg.Add(2) + + // Create a server with a single slow tool. + // When the client cancels its request, the server should observe + // cancellation. + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + started := make(chan struct{}, 1) // signals that the server started handling the tool call + mcp.AddTool(server, &mcp.Tool{Name: "slow"}, func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + started <- struct{}{} + defer wg.Done() + select { + case <-time.After(5 * time.Second): + serverResult = "tool done" + case <-ctx.Done(): + serverResult = "tool canceled" + } + return &mcp.CallToolResult{}, nil, nil + }) + + // Connect a client to the server. + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + ctx := context.Background() + t1, t2 := mcp.NewInMemoryTransports() + if _, err := server.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + session, err := client.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + defer session.Close() + + // Make a tool call, asynchronously. + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defer wg.Done() + _, err = session.CallTool(ctx, &mcp.CallToolParams{Name: "slow"}) + clientResult = fmt.Sprintf("%v", err) + }() + + // As soon as the server has started handling the call, cancel it from the + // client side. + <-started + cancel() + wg.Wait() + + fmt.Println(clientResult) + fmt.Println(serverResult) + // Output: + // context canceled + // tool canceled +} +``` + +### Ping + +[Ping](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping) +support is symmetrical for client and server. + +To initiate a ping, call +[`ClientSession.Ping`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Ping) +or +[`ServerSession.Ping`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Ping). + +To have the client or server session automatically ping its peer, and close the +session if the ping fails, set +[`ClientOptions.KeepAlive`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.KeepAlive) +or +[`ServerOptions.KeepAlive`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.KeepAlive). + +### Progress + +[Progress](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress) +reporting is possible by reading the progress token from request metadata and +calling either +[`ClientSession.NotifyProgress`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.NotifyProgress) +or +[`ServerSession.NotifyProgress`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.NotifyProgress). +To listen to progress notifications, set +[`ClientOptions.ProgressNotificationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ProgressNotificationHandler) +or +[`ServerOptions.ProgressNotificationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.ProgressNotificationHandler). + +Issue #460 discusses some potential ergonomic improvements to this API. + +```go +func Example_progress() { + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + mcp.AddTool(server, &mcp.Tool{Name: "makeProgress"}, func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + if token := req.Params.GetProgressToken(); token != nil { + for i := range 3 { + params := &mcp.ProgressNotificationParams{ + Message: "frobbing widgets", + ProgressToken: token, + Progress: float64(i), + Total: 2, + } + req.Session.NotifyProgress(ctx, params) // ignore error + } + } + return &mcp.CallToolResult{}, nil, nil + }) + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{ + ProgressNotificationHandler: func(_ context.Context, req *mcp.ProgressNotificationClientRequest) { + fmt.Printf("%s %.0f/%.0f\n", req.Params.Message, req.Params.Progress, req.Params.Total) + }, + }) + ctx := context.Background() + t1, t2 := mcp.NewInMemoryTransports() + if _, err := server.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + + session, err := client.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + defer session.Close() + if _, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "makeProgress", + Meta: mcp.Meta{"progressToken": "abc123"}, + }); err != nil { + log.Fatal(err) + } + // Output: + // frobbing widgets 0/2 + // frobbing widgets 1/2 + // frobbing widgets 2/2 +} +``` diff --git a/docs/server.md b/docs/server.md new file mode 100644 index 00000000..5b17f2e5 --- /dev/null +++ b/docs/server.md @@ -0,0 +1,38 @@ + +# Support for MCP server features + +1. [Prompts](#prompts) +1. [Resources](#resources) +1. [Tools](#tools) +1. [Utilities](#utilities) + 1. [Completion](#completion) + 1. [Logging](#logging) + 1. [Pagination](#pagination) + +## Prompts + + + +## Resources + + + +## Tools + + + +## Utilities + + + +### Completion + + + +### Logging + + + +### Pagination + + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..c0f021b6 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,90 @@ + +# Troubleshooting + +The Model Context Protocol is a complicated spec that leaves some room for +interpretation. Client and server SDKs can behave differently, or can be more +or less strict about their inputs. And of course, bugs happen. + +When you encounter a problem using the Go SDK, these instructions can help +collect information that will be useful in debugging. Please try to provide +this information in a bug report, so that maintainers can more quickly +understand what's going wrong. + +And most of all, please do [file bugs](https://github.com/modelcontextprotocol/go-sdk/issues/new?template=bug_report.md). + +## Using the MCP inspector + +To debug an MCP server, you can use the [MCP +inspector](https://modelcontextprotocol.io/legacy/tools/inspector). This is +useful for testing your server and verifying that it works with the typescript +SDK, as well as inspecting MCP traffic. + +## Collecting MCP logs + +For [stdio](protocol.md#stdio-transport) transport connections, you can also +inspect MCP traffic using a `LoggingTransport`: + +```go +func ExampleLoggingTransport() { + ctx := context.Background() + t1, t2 := mcp.NewInMemoryTransports() + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + if _, err := server.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + var b bytes.Buffer + logTransport := &mcp.LoggingTransport{Transport: t2, Writer: &b} + if _, err := client.Connect(ctx, logTransport, nil); err != nil { + log.Fatal(err) + } + fmt.Println(b.String()) + // Output: + // write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18"}} + // read: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.0.1"}}} + // write: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} + +} +``` + +That example uses a `bytes.Buffer`, but you can also log to a file, or to +`os.Stderr`. + +## Inspecting HTTP traffic + +There are a couple different ways to investigate traffic to an HTTP transport +([streamable](protocol.md#streamable-transport) or legacy SSE). + +The first is to use an HTTP middleware: + +```go +func ExampleStreamableHTTPHandler_middleware() { + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil) + handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { + return server + }, nil) + loggingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Example debugging; you could also capture the response. + body, err := io.ReadAll(req.Body) + if err != nil { + log.Fatal(err) + } + req.Body.Close() // ignore error + req.Body = io.NopCloser(bytes.NewBuffer(body)) + fmt.Println(req.Method, string(body)) + handler.ServeHTTP(w, req) + }) + httpServer := httptest.NewServer(loggingHandler) + defer httpServer.Close() + + // The SDK is currently permissive of some missing keys in "params". + mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL) + // Output: + // POST {"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}} +} +``` + +The second is to use a general purpose tool to inspect http traffic, such as +[wireshark](https://www.wireshark.org/) or +[tcpdump](https://linux.die.net/man/8/tcpdump). diff --git a/internal/docs/README.src.md b/internal/docs/README.src.md new file mode 100644 index 00000000..fb600df3 --- /dev/null +++ b/internal/docs/README.src.md @@ -0,0 +1,39 @@ +These docs are a work-in-progress. + +# Features + +These docs mirror the [official MCP spec](https://modelcontextprotocol.io/specification/2025-06-18). + +## Base Protocol + +1. [Lifecycle (Clients, Servers, and Sessions)](protocol.md#lifecycle). +1. [Transports](protocol.md#transports) + 1. [Stdio transport](protocol.md#stdio-transport) + 1. [Streamable transport](protocol.md#streamable-transport) + 1. [Custom transports](protocol.md#stateless-mode) +1. [Authorization](protocol.md#authorization) +1. [Security](protocol.md#security) +1. [Utilities](protocol.md#utilities) + 1. [Cancellation](utilities.md#cancellation) + 1. [Ping](utilities.md#ping) + 1. [Progress](utilities.md#progress) + +## Client Features + +1. [Roots](client.md#roots) +1. [Sampling](client.md#sampling) +1. [Elicitation](clients.md#elicitation) + +## Server Features + +1. [Prompts](server.md#prompts) +1. [Resources](server.md#resources) +1. [Tools](tools.md) +1. [Utilities](server.md#utilities) + 1. [Completion](server.md#completion) + 1. [Logging](server.md#logging) + 1. [Pagination](server.md#pagination) + +# TroubleShooting + +See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide. diff --git a/internal/docs/client.src.md b/internal/docs/client.src.md new file mode 100644 index 00000000..f342719e --- /dev/null +++ b/internal/docs/client.src.md @@ -0,0 +1,55 @@ +# Support for MCP client features + +%toc + +## Roots + +MCP allows clients to specify a set of filesystem +["roots"](https://modelcontextprotocol.io/specification/2025-06-18/client/roots). +The SDK supports this as follows: + +**Client-side**: The SDK client always has the `roots.listChanged` capability. +To add roots to a client, use the +[`Client.AddRoots`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AddRoots) +and +[`Client.RemoveRoots`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Client.RemoveRoots) +methods. If any servers are already [connected](protocol.md#lifecycle) to the +client, a call to `AddRoot` or `RemoveRoots` will result in a +`notifications/roots/list_changed` notification to each connected server. + +**Server-side**: To query roots from the server, use the +[`ServerSession.ListRoots`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.ListRoots) +method. To receive notifications about root changes, set +[`ServerOptions.RootsListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.RootsListChangedHandler). + +%include ../../mcp/client_example_test.go roots - + +## Sampling + +[Sampling](https://modelcontextprotocol.io/specification/2025-06-18/client/sampling) +is a way for servers to leverage the client's AI capabilities. It is +implemented in the SDK as follows: + +**Client-side**: To add the `sampling` capability to a client, set +[`ClientOptions.CreateMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.CreateMessageHandler). +This function is invoked whenever the server requests sampling. + +**Server-side**: To use sampling from the server, call +[`ServerSession.CreateMessage`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.CreateMessage). + +%include ../../mcp/client_example_test.go sampling - + +## Elicitation + +[Elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) +allows servers to request user inputs. It is implemented in the SDK as follows: + +**Client-side**: To add the `elicitation` capability to a client, set +[`ClientOptions.ElicitationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ElicitationHandler). +The elicitation handler must return a result that matches the requested schema; +otherwise, elicitation returns an error. + +**Server-side**: To use elicitation from the server, call +[`ServerSession.Elicit`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Elicit). + +%include ../../mcp/client_example_test.go elicitation - diff --git a/internal/docs/doc.go b/internal/docs/doc.go new file mode 100644 index 00000000..7b23ad63 --- /dev/null +++ b/internal/docs/doc.go @@ -0,0 +1,15 @@ +// 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. + +//go:generate -command weave go run golang.org/x/example/internal/cmd/weave@latest +//go:generate weave -o ../../docs/README.md ./README.src.md +//go:generate weave -o ../../docs/protocol.md ./protocol.src.md +//go:generate weave -o ../../docs/client.md ./client.src.md +//go:generate weave -o ../../docs/server.md ./server.src.md +//go:generate weave -o ../../docs/troubleshooting.md ./troubleshooting.src.md + +// The doc package generates the documentation at /doc, via go:generate. +// +// Tests in this package are used for examples. +package docs diff --git a/internal/docs/protocol.src.md b/internal/docs/protocol.src.md new file mode 100644 index 00000000..79b72418 --- /dev/null +++ b/internal/docs/protocol.src.md @@ -0,0 +1,198 @@ +# Support for the MCP base protocol + +%toc + +## Lifecycle + +The SDK provides an API for defining both MCP clients and servers, and +connecting them over various transports. When a client and server are +connected, it creates a logical session, which follows the MCP spec's +[lifecycle](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle). + +In this SDK, both a +[`Client`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Client) +and +[`Server`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server) +can handle multiple peers. Every time a new peer is connected, it creates a new +session. + +- A `Client` is a logical MCP client, configured with various + [`ClientOptions`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions). +- When a client is connected to a server using + [`Client.Connect`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Client.Connect), + it creates a + [`ClientSession`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession). + This session is initialized during the `Connect` method, and provides methods + to communicate with the server peer. +- A `Server` is a logical MCP server, configured with various + [`ServerOptions`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions). +- When a server is connected to a client using + [`Server.Connect`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.Connect), + it creates a + [`ServerSession`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession). + This session is not initialized until the client sends the + `notifications/initialized` message. Use `ServerOptions.InitializedHandler` + to listen for this event, or just use the session through various feature + handlers (such as a `ToolHandler`). Requests to the server are rejected until + the client has initialized the session. + +Both `ClientSession` and `ServerSession` have a `Close` method to terminate the +session, and a `Wait` method to await session termination by the peer. Typically, +it is the client's responsibility to end the session. + +%include ../../mcp/mcp_example_test.go lifecycle - + +## Transports + +A +[transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) +can be used to send JSON-RPC messages from client to server, or vice-versa. + +In the SDK, this is achieved by implementing the +[`Transport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Transport) +interface, which creates a (logical) bidirectional stream of JSON-RPC messages. +Most transport implementations described below are specific to either the +client or server: a "client transport" is something that can be used to connect +a client to a server, and a "server transport" is something that can be used to +connect a server to a client. However, it's possible for a transport to be both +a client and server transport, such as the `InMemoryTransport` used in the +lifecycle example above. + +Transports should not be reused for multiple connections: if you need to create +multiple connections, use different transports. + +### Stdio Transport + +In the +[`stdio`](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio) +transport clients communicate with an MCP server running in a subprocess using +newline-delimited JSON over its stdin/stdout. + +**Client-side**: the client side of the `stdio` transport is implemented by +[`CommandTransport`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#CommandTransport), +which starts the a `exec.Cmd` as a subprocess and communicates over its +stdin/stdout. + +**Server-side**: the server side of the `stdio` transport is implemented by +`StdioTransport`, which connects over the current processes `os.Stdin` and +`os.Stdout`. + +### Streamable Transport + +The [streamable +transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) +API is implemented across three types: + +- `StreamableHTTPHandler`: an`http.Handler` that serves streamable MCP + sessions. +- `StreamableServerTransport`: a `Transport` that implements the server side of + the streamable transport. +- `StreamableClientTransport`: a `Transport` that implements the client side of + the streamable transport. + +To create a streamable MCP server, you create a `StreamableHTTPHandler` and +pass it an `mcp.Server`: + +%include ../../mcp/streamable_example_test.go streamablehandler - + +The `StreamableHTTPHandler` handles the HTTP requests and creates a new +`StreamableServerTransport` for each new session. The transport is then used to +communicate with the client. + +On the client side, you create a `StreamableClientTransport` and use it to +connect to the server: + +```go +transport := &mcp.StreamableClientTransport{ + Endpoint: "http://localhost:8080/mcp", +} +client, err := mcp.Connect(ctx, transport, &mcp.ClientOptions{...}) +``` + +The `StreamableClientTransport` handles the HTTP requests and communicates with +the server using the streamable transport protocol. + +#### Stateless Mode + + + +#### Sessionless mode + + + +### Custom transports + + + +### Concurrency + +In general, MCP offers no guarantees about concurrency semantics: if a client +or server sends a notification, the spec says nothing about when the peer +observes that notification relative to other request. However, the Go SDK +implements the following heuristics: + +- If a notifying method (such as `notifications/progress` or + `notifications/initialized`) returns, then it is guaranteed that the peer + observes that notification before other notifications or calls from the same + client goroutine. +- Calls (such as `tools/call`) are handled asynchronously with respect to + each other. + +See +[modelcontextprotocol/go-sdk#26](https://github.com/modelcontextprotocol/go-sdk/issues/26) +for more background. + +## Authorization + + + +## Security + + + +## Utilities + +### Cancellation + +Cancellation is implemented with context cancellation. Cancelling a context +used in a method on `ClientSession` or `ServerSession` will terminate the RPC +and send a "notifications/cancelled" message to the peer. + +When an RPC exits due to a cancellation error, there's a guarantee that the +cancellation notification has been sent, but there's no guarantee that the +server has observed it (see [concurrency](#concurrency)). + +%include ../../mcp/mcp_example_test.go cancellation - + +### Ping + +[Ping](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping) +support is symmetrical for client and server. + +To initiate a ping, call +[`ClientSession.Ping`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Ping) +or +[`ServerSession.Ping`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Ping). + +To have the client or server session automatically ping its peer, and close the +session if the ping fails, set +[`ClientOptions.KeepAlive`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.KeepAlive) +or +[`ServerOptions.KeepAlive`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.KeepAlive). + +### Progress + +[Progress](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress) +reporting is possible by reading the progress token from request metadata and +calling either +[`ClientSession.NotifyProgress`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.NotifyProgress) +or +[`ServerSession.NotifyProgress`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.NotifyProgress). +To listen to progress notifications, set +[`ClientOptions.ProgressNotificationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ProgressNotificationHandler) +or +[`ServerOptions.ProgressNotificationHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.ProgressNotificationHandler). + +Issue #460 discusses some potential ergonomic improvements to this API. + +%include ../../mcp/mcp_example_test.go progress - diff --git a/internal/docs/server.src.md b/internal/docs/server.src.md new file mode 100644 index 00000000..a131bcd3 --- /dev/null +++ b/internal/docs/server.src.md @@ -0,0 +1,31 @@ +# Support for MCP server features + +%toc + +## Prompts + + + +## Resources + + + +## Tools + + + +## Utilities + + + +### Completion + + + +### Logging + + + +### Pagination + + diff --git a/internal/docs/troubleshooting.src.md b/internal/docs/troubleshooting.src.md new file mode 100644 index 00000000..83342032 --- /dev/null +++ b/internal/docs/troubleshooting.src.md @@ -0,0 +1,42 @@ +# Troubleshooting + +The Model Context Protocol is a complicated spec that leaves some room for +interpretation. Client and server SDKs can behave differently, or can be more +or less strict about their inputs. And of course, bugs happen. + +When you encounter a problem using the Go SDK, these instructions can help +collect information that will be useful in debugging. Please try to provide +this information in a bug report, so that maintainers can more quickly +understand what's going wrong. + +And most of all, please do [file bugs](https://github.com/modelcontextprotocol/go-sdk/issues/new?template=bug_report.md). + +## Using the MCP inspector + +To debug an MCP server, you can use the [MCP +inspector](https://modelcontextprotocol.io/legacy/tools/inspector). This is +useful for testing your server and verifying that it works with the typescript +SDK, as well as inspecting MCP traffic. + +## Collecting MCP logs + +For [stdio](protocol.md#stdio-transport) transport connections, you can also +inspect MCP traffic using a `LoggingTransport`: + +%include ../../mcp/transport_example_test.go loggingtransport - + +That example uses a `bytes.Buffer`, but you can also log to a file, or to +`os.Stderr`. + +## Inspecting HTTP traffic + +There are a couple different ways to investigate traffic to an HTTP transport +([streamable](protocol.md#streamable-transport) or legacy SSE). + +The first is to use an HTTP middleware: + +%include ../../mcp/streamable_example_test.go httpmiddleware - + +The second is to use a general purpose tool to inspect http traffic, such as +[wireshark](https://www.wireshark.org/) or +[tcpdump](https://linux.die.net/man/8/tcpdump). diff --git a/internal/readme/client/client.go b/internal/readme/client/client.go index e2794f8b..9f357964 100644 --- a/internal/readme/client/client.go +++ b/internal/readme/client/client.go @@ -44,4 +44,4 @@ func main() { } } -//!- +// !- diff --git a/mcp/client.go b/mcp/client.go index 1ed3b048..822566de 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -55,11 +55,15 @@ func NewClient(impl *Implementation, opts *ClientOptions) *Client { // ClientOptions configures the behavior of the client. type ClientOptions struct { - // Handler for sampling. - // Called when a server calls CreateMessage. + // CreateMessageHandler handles incoming requests for sampling/createMessage. + // + // Setting CreateMessageHandler to a non-nil value causes the client to + // advertise the sampling capability. CreateMessageHandler func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error) - // Handler for elicitation. - // Called when a server requests user input via Elicit. + // ElicitationHandler handles incoming requests for elicitation/create. + // + // Setting ElicitationHandler to a non-nil value causes the client to + // advertise the elicitation capability. ElicitationHandler func(context.Context, *ElicitRequest) (*ElicitResult, error) // Handlers for notifications from the server. ToolListChangedHandler func(context.Context, *ToolListChangedRequest) @@ -123,7 +127,7 @@ func (c *Client) capabilities() *ClientCapabilities { } // Connect begins an MCP session by connecting to a server over the given -// transport, and initializing the session. +// transport. The resulting session is initialized, and ready to use. // // Typically, it is the responsibility of the client to close the connection // when it is no longer needed. However, if the connection is closed by the @@ -302,6 +306,9 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult, // Validate elicitation result content against requested schema if req.Params.RequestedSchema != nil && res.Content != nil { + // TODO: is this the correct behavior if validation fails? + // It isn't the *server's* params that are invalid, so why would we return + // this code to the server? resolved, err := req.Params.RequestedSchema.Resolve(nil) if err != nil { return nil, jsonrpc2.NewError(CodeInvalidParams, fmt.Sprintf("failed to resolve requested schema: %v", err)) diff --git a/mcp/client_example_test.go b/mcp/client_example_test.go new file mode 100644 index 00000000..3c3c3837 --- /dev/null +++ b/mcp/client_example_test.go @@ -0,0 +1,136 @@ +// 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. + +package mcp_test + +import ( + "context" + "fmt" + "log" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// !+roots + +func Example_roots() { + ctx := context.Background() + + // Create a client with a single root. + c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + c.AddRoots(&mcp.Root{URI: "file://a"}) + + // Now create a server with a handler to receive notifications about roots. + rootsChanged := make(chan struct{}) + handleRootsChanged := func(ctx context.Context, req *mcp.RootsListChangedRequest) { + rootList, err := req.Session.ListRoots(ctx, nil) + if err != nil { + log.Fatal(err) + } + var roots []string + for _, root := range rootList.Roots { + roots = append(roots, root.URI) + } + fmt.Println(roots) + close(rootsChanged) + } + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{ + RootsListChangedHandler: handleRootsChanged, + }) + + // Connect the server and client... + t1, t2 := mcp.NewInMemoryTransports() + if _, err := s.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + if _, err := c.Connect(ctx, t2, nil); err != nil { + log.Fatal(err) + } + + // ...and add a root. The server is notified about the change. + c.AddRoots(&mcp.Root{URI: "file://b"}) + <-rootsChanged + // Output: [file://a file://b] +} + +// !-roots + +// !+sampling + +func Example_sampling() { + ctx := context.Background() + + // Create a client with a sampling handler. + c := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{ + CreateMessageHandler: func(_ context.Context, req *mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) { + return &mcp.CreateMessageResult{ + Content: &mcp.TextContent{ + Text: "would have created a message", + }, + }, nil + }, + }) + + // Connect the server and client... + ct, st := mcp.NewInMemoryTransports() + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + session, err := s.Connect(ctx, st, nil) + if err != nil { + log.Fatal(err) + } + defer session.Close() + + if _, err := c.Connect(ctx, ct, nil); err != nil { + log.Fatal(err) + } + + msg, err := session.CreateMessage(ctx, &mcp.CreateMessageParams{}) + if err != nil { + log.Fatal(err) + } + fmt.Println(msg.Content.(*mcp.TextContent).Text) + // Output: would have created a message +} + +// !-sampling + +// !+elicitation + +func Example_elicitation() { + ctx := context.Background() + ct, st := mcp.NewInMemoryTransports() + + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + ss, err := s.Connect(ctx, st, nil) + if err != nil { + log.Fatal(err) + } + defer ss.Close() + + c := mcp.NewClient(testImpl, &mcp.ClientOptions{ + ElicitationHandler: func(context.Context, *mcp.ElicitRequest) (*mcp.ElicitResult, error) { + return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"test": "value"}}, nil + }, + }) + if _, err := c.Connect(ctx, ct, nil); err != nil { + log.Fatal(err) + } + res, err := ss.Elicit(ctx, &mcp.ElicitParams{ + Message: "This should fail", + RequestedSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "test": {Type: "string"}, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + fmt.Println(res.Content["test"]) + // Output: value +} + +// !-elicitation diff --git a/mcp/mcp_example_test.go b/mcp/mcp_example_test.go new file mode 100644 index 00000000..25f39fb8 --- /dev/null +++ b/mcp/mcp_example_test.go @@ -0,0 +1,166 @@ +// 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. + +package mcp_test + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// !+lifecycle + +func Example_lifecycle() { + ctx := context.Background() + + // Create a client and server. + // Wait for the client to initialize the session. + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, &mcp.ServerOptions{ + InitializedHandler: func(context.Context, *mcp.InitializedRequest) { + fmt.Println("initialized!") + }, + }) + + // Connect the server and client using in-memory transports. + // + // Connect the server first so that it's ready to receive initialization + // messages from the client. + t1, t2 := mcp.NewInMemoryTransports() + serverSession, err := server.Connect(ctx, t1, nil) + if err != nil { + log.Fatal(err) + } + clientSession, err := client.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + + // Now shut down the session by closing the client, and waiting for the + // server session to end. + if err := clientSession.Close(); err != nil { + log.Fatal(err) + } + if err := serverSession.Wait(); err != nil { + log.Fatal(err) + } + // Output: initialized! +} + +// !-lifecycle + +// !+progress + +func Example_progress() { + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + mcp.AddTool(server, &mcp.Tool{Name: "makeProgress"}, func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + if token := req.Params.GetProgressToken(); token != nil { + for i := range 3 { + params := &mcp.ProgressNotificationParams{ + Message: "frobbing widgets", + ProgressToken: token, + Progress: float64(i), + Total: 2, + } + req.Session.NotifyProgress(ctx, params) // ignore error + } + } + return &mcp.CallToolResult{}, nil, nil + }) + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{ + ProgressNotificationHandler: func(_ context.Context, req *mcp.ProgressNotificationClientRequest) { + fmt.Printf("%s %.0f/%.0f\n", req.Params.Message, req.Params.Progress, req.Params.Total) + }, + }) + ctx := context.Background() + t1, t2 := mcp.NewInMemoryTransports() + if _, err := server.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + + session, err := client.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + defer session.Close() + if _, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "makeProgress", + Meta: mcp.Meta{"progressToken": "abc123"}, + }); err != nil { + log.Fatal(err) + } + // Output: + // frobbing widgets 0/2 + // frobbing widgets 1/2 + // frobbing widgets 2/2 +} + +// !-progress + +// !+cancellation + +func Example_cancellation() { + // For this example, we're going to be collecting observations from the + // server and client. + var clientResult, serverResult string + var wg sync.WaitGroup + wg.Add(2) + + // Create a server with a single slow tool. + // When the client cancels its request, the server should observe + // cancellation. + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + started := make(chan struct{}, 1) // signals that the server started handling the tool call + mcp.AddTool(server, &mcp.Tool{Name: "slow"}, func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + started <- struct{}{} + defer wg.Done() + select { + case <-time.After(5 * time.Second): + serverResult = "tool done" + case <-ctx.Done(): + serverResult = "tool canceled" + } + return &mcp.CallToolResult{}, nil, nil + }) + + // Connect a client to the server. + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + ctx := context.Background() + t1, t2 := mcp.NewInMemoryTransports() + if _, err := server.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + session, err := client.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + defer session.Close() + + // Make a tool call, asynchronously. + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defer wg.Done() + _, err = session.CallTool(ctx, &mcp.CallToolParams{Name: "slow"}) + clientResult = fmt.Sprintf("%v", err) + }() + + // As soon as the server has started handling the call, cancel it from the + // client side. + <-started + cancel() + wg.Wait() + + fmt.Println(clientResult) + fmt.Println(serverResult) + // Output: + // context canceled + // tool canceled +} + +// !-cancellation diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 6191954c..dd542d3d 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -705,7 +705,7 @@ func TestCancellation(t *testing.T) { start = make(chan struct{}) cancelled = make(chan struct{}, 1) // don't block the request ) - slowRequest := func(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) { + slowTool := func(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) { start <- struct{}{} select { case <-ctx.Done(): @@ -716,7 +716,7 @@ func TestCancellation(t *testing.T) { return nil, nil, nil } cs, _ := basicConnection(t, func(s *Server) { - AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{Type: "object"}}, slowRequest) + AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{Type: "object"}}, slowTool) }) defer cs.Close() @@ -1109,7 +1109,7 @@ func TestElicitationSchemaValidation(t *testing.T) { "low", }, Extra: map[string]any{ - "enumNames": []interface{}{"High Priority", "Medium Priority", "Low Priority"}, + "enumNames": []any{"High Priority", "Medium Priority", "Low Priority"}, }, }, }, @@ -1270,7 +1270,7 @@ func TestElicitationSchemaValidation(t *testing.T) { "low", }, Extra: map[string]any{ - "enumNames": []interface{}{"High Priority", "Medium Priority"}, // Only 2 names for 3 values + "enumNames": []any{"High Priority", "Medium Priority"}, // Only 2 names for 3 values }, }, }, diff --git a/mcp/root.go b/mcp/root.go deleted file mode 100644 index b56ad991..00000000 --- a/mcp/root.go +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -package mcp diff --git a/mcp/streamable.go b/mcp/streamable.go index bfaccae4..8ac6f59a 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -70,9 +70,12 @@ type StreamableHTTPOptions struct { // documentation for [StreamableServerTransport]. Stateless bool - // TODO: support session retention (?) + // TODO(#148): support session retention (?) - // JSONResponse is forwarded to StreamableServerTransport.jsonResponse. + // JSONResponse causes streamable responses to return application/json rather + // than text/event-stream ([§2.1.5] of the spec). + // + // [§2.1.5]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server JSONResponse bool } @@ -181,7 +184,7 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque return } - // Section 2.7 of the spec (2025-06-18) states: + // [§2.7] of the spec (2025-06-18) states: // // "If using HTTP, the client MUST include the MCP-Protocol-Version: // HTTP header on all subsequent requests to the MCP @@ -209,6 +212,8 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque // assume 2025-03-26 if the client doesn't say anything). // // This logic matches the typescript SDK. + // + // [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header protocolVersion := req.Header.Get(protocolVersionHeader) if protocolVersion == "" { protocolVersion = protocolVersion20250326 @@ -370,6 +375,9 @@ type StreamableServerTransport struct { // request contain only a single message. In this case, notifications or // requests made within the context of a server request will be sent to the // hanging GET request, if any. + // + // TODO(rfindley): jsonResponse should be exported, since + // StreamableHTTPOptions.JSONResponse is exported. jsonResponse bool // connection is non-nil if and only if the transport has been connected. @@ -1188,7 +1196,7 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e return fmt.Errorf("%s: %v", requestSummary, err) } - // Section 2.5.3: "The server MAY terminate the session at any time, after + // §2.5.3: "The server MAY terminate the session at any time, after // which it MUST respond to requests containing that session ID with HTTP // 404 Not Found." if resp.StatusCode == http.StatusNotFound { diff --git a/mcp/streamable_example_test.go b/mcp/streamable_example_test.go new file mode 100644 index 00000000..430f2745 --- /dev/null +++ b/mcp/streamable_example_test.go @@ -0,0 +1,86 @@ +// 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. + +package mcp_test + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// !+streamablehandler + +func ExampleStreamableHTTPHandler() { + // Create a new streamable handler, using the same MCP server for every request. + // + // Here, we configure it to serves application/json responses rather than + // text/event-stream, just so the output below doesn't use random event ids. + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil) + handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { + return server + }, &mcp.StreamableHTTPOptions{JSONResponse: true}) + httpServer := httptest.NewServer(handler) + defer httpServer.Close() + + // The SDK is currently permissive of some missing keys in "params". + resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL) + fmt.Println(resp) + // Output: + // {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.1.0"}}} +} + +// !-streamablehandler + +// !+httpmiddleware + +func ExampleStreamableHTTPHandler_middleware() { + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil) + handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { + return server + }, nil) + loggingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Example debugging; you could also capture the response. + body, err := io.ReadAll(req.Body) + if err != nil { + log.Fatal(err) + } + req.Body.Close() // ignore error + req.Body = io.NopCloser(bytes.NewBuffer(body)) + fmt.Println(req.Method, string(body)) + handler.ServeHTTP(w, req) + }) + httpServer := httptest.NewServer(loggingHandler) + defer httpServer.Close() + + // The SDK is currently permissive of some missing keys in "params". + mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL) + // Output: + // POST {"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}} +} + +// !-httpmiddleware + +func mustPostMessage(msg, url string) string { + req := orFatal(http.NewRequest("POST", url, strings.NewReader(msg))) + req.Header["Content-Type"] = []string{"application/json"} + req.Header["Accept"] = []string{"application/json", "text/event-stream"} + resp := orFatal(http.DefaultClient.Do(req)) + defer resp.Body.Close() + body := orFatal(io.ReadAll(resp.Body)) + return string(body) +} + +func orFatal[T any](t T, err error) T { + if err != nil { + log.Fatal(err) + } + return t +} diff --git a/mcp/transport.go b/mcp/transport.go index 024863de..d2109e7d 100644 --- a/mcp/transport.go +++ b/mcp/transport.go @@ -212,12 +212,14 @@ func (t *LoggingTransport) Connect(ctx context.Context) (Connection, error) { if err != nil { return nil, err } - return &loggingConn{delegate, t.Writer}, nil + return &loggingConn{delegate: delegate, w: t.Writer}, nil } type loggingConn struct { delegate Connection - w io.Writer + + mu sync.Mutex + w io.Writer } func (c *loggingConn) SessionID() string { return c.delegate.SessionID() } @@ -225,15 +227,21 @@ func (c *loggingConn) SessionID() string { return c.delegate.SessionID() } // Read is a stream middleware that logs incoming messages. func (s *loggingConn) Read(ctx context.Context) (jsonrpc.Message, error) { msg, err := s.delegate.Read(ctx) + if err != nil { + s.mu.Lock() fmt.Fprintf(s.w, "read error: %v", err) + s.mu.Unlock() } else { data, err := jsonrpc2.EncodeMessage(msg) + s.mu.Lock() if err != nil { fmt.Fprintf(s.w, "LoggingTransport: failed to marshal: %v", err) } fmt.Fprintf(s.w, "read: %s\n", string(data)) + s.mu.Unlock() } + return msg, err } @@ -241,13 +249,17 @@ func (s *loggingConn) Read(ctx context.Context) (jsonrpc.Message, error) { func (s *loggingConn) Write(ctx context.Context, msg jsonrpc.Message) error { err := s.delegate.Write(ctx, msg) if err != nil { + s.mu.Lock() fmt.Fprintf(s.w, "write error: %v", err) + s.mu.Unlock() } else { data, err := jsonrpc2.EncodeMessage(msg) + s.mu.Lock() if err != nil { fmt.Fprintf(s.w, "LoggingTransport: failed to marshal: %v", err) } fmt.Fprintf(s.w, "write: %s\n", string(data)) + s.mu.Unlock() } return err } diff --git a/mcp/transport_example_test.go b/mcp/transport_example_test.go new file mode 100644 index 00000000..dcf1a8ba --- /dev/null +++ b/mcp/transport_example_test.go @@ -0,0 +1,40 @@ +// 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. + +package mcp_test + +import ( + "bytes" + "context" + "fmt" + "log" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// !+loggingtransport + +func ExampleLoggingTransport() { + ctx := context.Background() + t1, t2 := mcp.NewInMemoryTransports() + server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + if _, err := server.Connect(ctx, t1, nil); err != nil { + log.Fatal(err) + } + + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) + var b bytes.Buffer + logTransport := &mcp.LoggingTransport{Transport: t2, Writer: &b} + if _, err := client.Connect(ctx, logTransport, nil); err != nil { + log.Fatal(err) + } + fmt.Println(b.String()) + // Output: + // write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18"}} + // read: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.0.1"}}} + // write: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} + +} + +// !-loggingtransport