From 390d437d4f4b37f88dc5039b95122f89c1f39876 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 6 May 2025 19:58:27 +0000 Subject: [PATCH] internal/mcp/design: start a full design doc, with stubs Start writing up a formal design doc for an MCP SDK based on what we've learned from the prototype. Include discussion of package layout, jsonrpc2, and transport. Lay out the rest of the design sections, leaving stub comments describing what they should cover. Change-Id: Id0e5b408eac8c0acb5ef90a32d0d69473d8e83f0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/670536 Reviewed-by: Jonathan Amsterdam LUCI-TryBot-Result: Go LUCI Reviewed-by: Sam Thanawalla Auto-Submit: Robert Findley --- design/design.md | 282 +++++++++++++++++++++++++++++++++++++++++++++++ mcp/transport.go | 3 +- 2 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 design/design.md diff --git a/design/design.md b/design/design.md new file mode 100644 index 0000000..31ed720 --- /dev/null +++ b/design/design.md @@ -0,0 +1,282 @@ +# 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, and so +approaches each design aspect from first principles. Of course, there is +significant prior art in various unofficial SDKs, along with other practical +constraints. Nevertheless, if we can first agree on the most natural way to +model the MCP spec, we can then discuss the shortest path to get there. + +# 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 many 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. + +### 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 +} +``` + +Specifically, a `Transport` is something that connects a logical JSON-RPC +stream, and nothing more (methods accept a Go `Context` and return an `error`, +as is idiomatic for APIs that do I/O). 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 or middleware. + +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 (t *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 +// SSEHandler is an http.Handler that serves SSE-based MCP sessions as defined by +// the 2024-11-05 version of the MCP protocol. +type SSEHandler struct { /* unexported fields */ } + +// NewSSEHandler returns a new [SSEHandler] 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 NewSSEHandler(getServer func(request *http.Request) *Server) *SSEHandler + +func (*SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) + +// Close prevents the SSEHandler from accepting new sessions, closes active +// sessions, and awaits their graceful termination. +func (*SSEHandler) Close() error +``` + +Notably absent are options to hook into the request handling for the purposes +of authentication or context injection. These concerns are better handled using +standard HTTP middleware patterns. + + + + + +### Protocol types + + + +### Clients and Servers + + + +### Errors + + + +### Cancellation + + + +### Progress handling + + + +### Ping / Keepalive + + + +## Client Features + +### Roots + + + +### Sampling + + + +## Server Features + +### Tools + + + +#### JSON Schema + + + +### Prompts + + + +### Resources + + + +### Completion + + + +### Logging + + + +### Pagination + + + +## Compatibility with existing SDKs + + diff --git a/mcp/transport.go b/mcp/transport.go index 15dc09e..6bbe5a5 100644 --- a/mcp/transport.go +++ b/mcp/transport.go @@ -35,8 +35,7 @@ type Transport interface { Connect(ctx context.Context) (Stream, error) } -// A Stream is an abstract bidirectional jsonrpc2 Stream. -// It is used by [connect] to establish a [jsonrpc2.Connection]. +// A Stream is a bidirectional jsonrpc2 Stream. type Stream interface { jsonrpc2.Reader jsonrpc2.Writer