Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func main() {
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)

// Connect to a server over stdin/stdout
transport := mcp.NewCommandTransport(exec.Command("myserver"))
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -127,7 +127,7 @@ func main() {

mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
// Run the server over stdin/stdout, until the client disconnects
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
Expand Down
81 changes: 29 additions & 52 deletions design/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,7 @@ The `CommandTransport` is the client side of the stdio transport, and connects b
```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
type CommandTransport struct { Command *exec.Command }

// Connect starts the command, and connects to it over stdin/stdout.
func (*CommandTransport) Connect(ctx context.Context) (Connection, error) {
Expand All @@ -115,9 +111,7 @@ The `StdioTransport` is the server side of the stdio transport, and connects by
```go
// A StdioTransport is a [Transport] that communicates using newline-delimited
// JSON over stdin/stdout.
type StdioTransport struct { /* unexported fields */ }

func NewStdioTransport() *StdioTransport
type StdioTransport struct { }

func (t *StdioTransport) Connect(context.Context) (Connection, error)
```
Expand All @@ -128,6 +122,8 @@ The HTTP transport APIs are even more asymmetrical. Since connections are initia

Importantly, since they serve many connections, the HTTP handlers must accept a callback to get an MCP server for each new session. As described below, MCP servers can optionally connect to multiple clients. This allows customization of per-session servers: if the MCP server is stateless, the user can return the same MCP server for each connection. On the other hand, if any per-session customization is required, it is possible by returning a different `Server` instance for each connection.

Both the SSE and Streamable HTTP server transports are http.Handlers which serve messages to their associated connection. Consequently, they can be connected at most once.

```go
// SSEHTTPHandler is an http.Handler that serves SSE-based MCP sessions as defined by
// the 2024-11-05 version of the MCP protocol.
Expand All @@ -153,26 +149,10 @@ By default, the SSE handler creates messages endpoints with the `?sessionId=...`
```go
// A SSEServerTransport is a logical SSE session created through a hanging GET
// request.
//
// When connected, it returns the following [Connection] 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.
//
// Most callers should instead use an [SSEHandler], which transparently handles
// the delegation to SSEServerTransports.
func NewSSEServerTransport(endpoint string, w http.ResponseWriter) *SSEServerTransport
type SSEServerTransport struct {
Endpoint string
Response http.ResponseWriter
}

// ServeHTTP handles POST requests to the transport endpoint.
func (*SSEServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
Expand All @@ -185,20 +165,14 @@ func (*SSEServerTransport) Connect(context.Context) (Connection, error)
The SSE client transport is simpler, and hopefully self-explanatory.

```go
type SSEClientTransport struct { /* ... */ }

// SSEClientTransportOptions provides options for the [NewSSEClientTransport]
// constructor.
type SSEClientTransportOptions struct {
type SSEClientTransport struct {
// Endpoint is the SSE endpoint to connect to.
Endpoint string
// HTTPClient is the client to use for making HTTP requests. If nil,
// http.DefaultClient is used.
HTTPClient *http.Client
}

// NewSSEClientTransport returns a new client transport that connects to the
// SSE server at the provided URL.
func NewSSEClientTransport(url string, opts *SSEClientTransportOptions) (*SSEClientTransport, error)

// Connect connects through the client endpoint.
func (*SSEClientTransport) Connect(ctx context.Context) (Connection, error)
```
Expand All @@ -218,23 +192,22 @@ func (*StreamableHTTPHandler) Close() error
// 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) *StreamableServerTransport
type StreamableServerTransport struct {
// SessionID is the ID of this session.
SessionID string
// Storage for events, to enable stream resumption.
EventStore EventStore
}
func (*StreamableServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
func (*StreamableServerTransport) Connect(context.Context) (Connection, error)

// The streamable client handles reconnection transparently to the user.
type StreamableClientTransport struct { /* ... */ }

// StreamableClientTransportOptions provides options for the
// [NewStreamableClientTransport] constructor.
type StreamableClientTransportOptions struct {
// HTTPClient is the client to use for making HTTP requests. If nil,
// http.DefaultClient is used.
HTTPClient *http.Client
type StreamableClientTransport struct {
Endpoint string
HTTPClient *http.Client
ReconnectOptions *StreamableReconnectOptions
}

func NewStreamableClientTransport(url string, opts *StreamableClientTransportOptions) *StreamableClientTransport
func (*StreamableClientTransport) Connect(context.Context) (Connection, error)
```

Expand All @@ -257,8 +230,10 @@ func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport)

// A LoggingTransport is a [Transport] that delegates to another transport,
// writing RPC logs to an io.Writer.
type LoggingTransport struct { /* ... */ }
func NewLoggingTransport(delegate Transport, w io.Writer) *LoggingTransport
type LoggingTransport struct {
Delegate Transport
Writer io.Writer
}
```

### Protocol types
Expand Down Expand Up @@ -358,7 +333,9 @@ Here's an example of these APIs from the client side:
```go
client := mcp.NewClient(&mcp.Implementation{Name:"mcp-client", Version:"v1.0.0"}, nil)
// Connect to a server over stdin/stdout
transport := mcp.NewCommandTransport(exec.Command("myserver"))
transport := &mcp.CommandTransport{
Command: exec.Command("myserver"},
}
session, err := client.Connect(ctx, transport)
if err != nil { ... }
// Call a tool on the server.
Expand All @@ -374,7 +351,7 @@ A server that can handle that client call would look like this:
server := mcp.NewServer(&mcp.Implementation{Name:"greeter", Version:"v1.0.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
// Run the server over stdin/stdout, until the client disconnects.
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
```
Expand Down
2 changes: 1 addition & 1 deletion examples/client/listfeatures/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func main() {
ctx := context.Background()
cmd := exec.Command(args[0], args[1:]...)
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
cs, err := client.Connect(ctx, mcp.NewCommandTransport(cmd), nil)
cs, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
if err != nil {
log.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion examples/server/hello/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func main() {
log.Printf("MCP handler listening at %s", *httpAddr)
http.ListenAndServe(*httpAddr, handler)
} else {
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
if err := server.Run(context.Background(), t); err != nil {
log.Printf("Server failed: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion examples/server/memory/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func main() {
log.Printf("MCP handler listening at %s", *httpAddr)
http.ListenAndServe(*httpAddr, handler)
} else {
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
if err := server.Run(context.Background(), t); err != nil {
log.Printf("Server failed: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion examples/server/sequentialthinking/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ func main() {
log.Fatal(err)
}
} else {
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
if err := server.Run(context.Background(), t); err != nil {
log.Printf("Server failed: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/readme/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func main() {
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)

// Connect to a server over stdin/stdout
transport := mcp.NewCommandTransport(exec.Command("myserver"))
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion internal/readme/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func main() {

mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
// Run the server over stdin/stdout, until the client disconnects
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
Expand Down
16 changes: 10 additions & 6 deletions mcp/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,37 @@ import (
// A CommandTransport is a [Transport] that runs a command and communicates
// with it over stdin/stdout, using newline-delimited JSON.
type CommandTransport struct {
cmd *exec.Cmd
Command *exec.Cmd
}

// NewCommandTransport returns a [CommandTransport] that runs the given command
// and communicates with it over stdin/stdout.
//
// The resulting transport takes ownership of the command, starting it during
// [CommandTransport.Connect], and stopping it when the connection is closed.
//
// Deprecated: use a CommandTransport literal.
//
//go:fix inline
func NewCommandTransport(cmd *exec.Cmd) *CommandTransport {
return &CommandTransport{cmd}
return &CommandTransport{Command: cmd}
}

// Connect starts the command, and connects to it over stdin/stdout.
func (t *CommandTransport) Connect(ctx context.Context) (Connection, error) {
stdout, err := t.cmd.StdoutPipe()
stdout, err := t.Command.StdoutPipe()
if err != nil {
return nil, err
}
stdout = io.NopCloser(stdout) // close the connection by closing stdin, not stdout
stdin, err := t.cmd.StdinPipe()
stdin, err := t.Command.StdinPipe()
if err != nil {
return nil, err
}
if err := t.cmd.Start(); err != nil {
if err := t.Command.Start(); err != nil {
return nil, err
}
return newIOConn(&pipeRWC{t.cmd, stdout, stdin}), nil
return newIOConn(&pipeRWC{t.Command, stdout, stdin}), nil
}

// A pipeRWC is an io.ReadWriteCloser that communicates with a subprocess over
Expand Down
8 changes: 4 additions & 4 deletions mcp/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func runServer() {

server := mcp.NewServer(testImpl, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
if err := server.Run(ctx, mcp.NewStdioTransport()); err != nil {
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
Expand All @@ -59,7 +59,7 @@ func runCancelContextServer() {
defer done()

server := mcp.NewServer(testImpl, nil)
if err := server.Run(ctx, mcp.NewStdioTransport()); err != nil {
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
Expand Down Expand Up @@ -116,7 +116,7 @@ func TestServerInterrupt(t *testing.T) {
cmd := createServerCommand(t, "default")

client := mcp.NewClient(testImpl, nil)
_, err := client.Connect(ctx, mcp.NewCommandTransport(cmd), nil)
_, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -198,7 +198,7 @@ func TestCmdTransport(t *testing.T) {
cmd := createServerCommand(t, "default")

client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
session, err := client.Connect(ctx, mcp.NewCommandTransport(cmd), nil)
session, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ func TestNoJSONNull(t *testing.T) {

// Collect logs, to sanity check that we don't write JSON null anywhere.
var logbuf safeBuffer
ct = NewLoggingTransport(ct, &logbuf)
ct = &LoggingTransport{Transport: ct, Writer: &logbuf}

s := NewServer(testImpl, nil)
ss, err := s.Connect(ctx, st, nil)
Expand Down
Loading
Loading