Skip to content

Commit 7c10890

Browse files
committed
mcp: make transports open structs
As described in #272, there is really no reason for transports to be closed structs with constructors, since their state must be established by a call to Connect. Making them open structs simplifies their APIs, and means that all transports can be extended in the future: we don't have to create empty Options structs just for the purpose of future compatibility. For now, the related constructors and options structs are simply deprecated (with go:fix directives where possible). A future CL will remove them prior to the v1.0.0 release. For #272
1 parent dae8853 commit 7c10890

File tree

7 files changed

+285
-229
lines changed

7 files changed

+285
-229
lines changed

design/design.md

Lines changed: 29 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,7 @@ The `CommandTransport` is the client side of the stdio transport, and connects b
100100
```go
101101
// A CommandTransport is a [Transport] that runs a command and communicates
102102
// with it over stdin/stdout, using newline-delimited JSON.
103-
type CommandTransport struct { /* unexported fields */ }
104-
105-
// NewCommandTransport returns a [CommandTransport] that runs the given command
106-
// and communicates with it over stdin/stdout.
107-
func NewCommandTransport(cmd *exec.Command) *CommandTransport
103+
type CommandTransport struct { Command *exec.Command }
108104

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

122116
func (t *StdioTransport) Connect(context.Context) (Connection, error)
123117
```
@@ -128,6 +122,8 @@ The HTTP transport APIs are even more asymmetrical. Since connections are initia
128122
129123
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.
130124
125+
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.
126+
131127
```go
132128
// SSEHTTPHandler is an http.Handler that serves SSE-based MCP sessions as defined by
133129
// the 2024-11-05 version of the MCP protocol.
@@ -153,26 +149,10 @@ By default, the SSE handler creates messages endpoints with the `?sessionId=...`
153149
```go
154150
// A SSEServerTransport is a logical SSE session created through a hanging GET
155151
// request.
156-
//
157-
// When connected, it returns the following [Connection] implementation:
158-
// - Writes are SSE 'message' events to the GET response.
159-
// - Reads are received from POSTs to the session endpoint, via
160-
// [SSEServerTransport.ServeHTTP].
161-
// - Close terminates the hanging GET.
162-
type SSEServerTransport struct { /* ... */ }
163-
164-
// NewSSEServerTransport creates a new SSE transport for the given messages
165-
// endpoint, and hanging GET response.
166-
//
167-
// Use [SSEServerTransport.Connect] to initiate the flow of messages.
168-
//
169-
// The transport is itself an [http.Handler]. It is the caller's responsibility
170-
// to ensure that the resulting transport serves HTTP requests on the given
171-
// session endpoint.
172-
//
173-
// Most callers should instead use an [SSEHandler], which transparently handles
174-
// the delegation to SSEServerTransports.
175-
func NewSSEServerTransport(endpoint string, w http.ResponseWriter) *SSEServerTransport
152+
type SSEServerTransport struct {
153+
Endpoint string
154+
Response http.ResponseWriter
155+
}
176156

177157
// ServeHTTP handles POST requests to the transport endpoint.
178158
func (*SSEServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
@@ -185,20 +165,14 @@ func (*SSEServerTransport) Connect(context.Context) (Connection, error)
185165
The SSE client transport is simpler, and hopefully self-explanatory.
186166
187167
```go
188-
type SSEClientTransport struct { /* ... */ }
189-
190-
// SSEClientTransportOptions provides options for the [NewSSEClientTransport]
191-
// constructor.
192-
type SSEClientTransportOptions struct {
168+
type SSEClientTransport struct {
169+
// Endpoint is the SSE endpoint to connect to.
170+
Endpoint string
193171
// HTTPClient is the client to use for making HTTP requests. If nil,
194172
// http.DefaultClient is used.
195173
HTTPClient *http.Client
196174
}
197175

198-
// NewSSEClientTransport returns a new client transport that connects to the
199-
// SSE server at the provided URL.
200-
func NewSSEClientTransport(url string, opts *SSEClientTransportOptions) (*SSEClientTransport, error)
201-
202176
// Connect connects through the client endpoint.
203177
func (*SSEClientTransport) Connect(ctx context.Context) (Connection, error)
204178
```
@@ -218,23 +192,22 @@ func (*StreamableHTTPHandler) Close() error
218192
// session ID, not an endpoint, along with the HTTP response for the request
219193
// that created the session. It is the caller's responsibility to delegate
220194
// requests to this session.
221-
type StreamableServerTransport struct { /* ... */ }
222-
func NewStreamableServerTransport(sessionID string) *StreamableServerTransport
195+
type StreamableServerTransport struct {
196+
// SessionID is the ID of this session.
197+
SessionID string
198+
// Storage for events, to enable stream resumption.
199+
EventStore EventStore
200+
}
223201
func (*StreamableServerTransport) ServeHTTP(w http.ResponseWriter, req *http.Request)
224202
func (*StreamableServerTransport) Connect(context.Context) (Connection, error)
225203

226204
// The streamable client handles reconnection transparently to the user.
227-
type StreamableClientTransport struct { /* ... */ }
228-
229-
// StreamableClientTransportOptions provides options for the
230-
// [NewStreamableClientTransport] constructor.
231-
type StreamableClientTransportOptions struct {
232-
// HTTPClient is the client to use for making HTTP requests. If nil,
233-
// http.DefaultClient is used.
234-
HTTPClient *http.Client
205+
type StreamableClientTransport struct {
206+
Endpoint string
207+
HTTPClient *http.Client
208+
ReconnectOptions *StreamableReconnectOptions
235209
}
236210

237-
func NewStreamableClientTransport(url string, opts *StreamableClientTransportOptions) *StreamableClientTransport
238211
func (*StreamableClientTransport) Connect(context.Context) (Connection, error)
239212
```
240213
@@ -257,8 +230,10 @@ func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport)
257230

258231
// A LoggingTransport is a [Transport] that delegates to another transport,
259232
// writing RPC logs to an io.Writer.
260-
type LoggingTransport struct { /* ... */ }
261-
func NewLoggingTransport(delegate Transport, w io.Writer) *LoggingTransport
233+
type LoggingTransport struct {
234+
Delegate Transport
235+
Writer io.Writer
236+
}
262237
```
263238
264239
### Protocol types
@@ -358,7 +333,9 @@ Here's an example of these APIs from the client side:
358333
```go
359334
client := mcp.NewClient(&mcp.Implementation{Name:"mcp-client", Version:"v1.0.0"}, nil)
360335
// Connect to a server over stdin/stdout
361-
transport := mcp.NewCommandTransport(exec.Command("myserver"))
336+
transport := &mcp.CommandTransport{
337+
Command: exec.Command("myserver"},
338+
}
362339
session, err := client.Connect(ctx, transport)
363340
if err != nil { ... }
364341
// Call a tool on the server.
@@ -374,7 +351,7 @@ A server that can handle that client call would look like this:
374351
server := mcp.NewServer(&mcp.Implementation{Name:"greeter", Version:"v1.0.0"}, nil)
375352
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
376353
// Run the server over stdin/stdout, until the client disconnects.
377-
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
354+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
378355
log.Fatal(err)
379356
}
380357
```

examples/server/hello/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func main() {
5858
log.Printf("MCP handler listening at %s", *httpAddr)
5959
http.ListenAndServe(*httpAddr, handler)
6060
} else {
61-
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
61+
t := &mcp.LoggingTransport{Delegate: &mcp.StdioTransport{}, Writer: os.Stderr}
6262
if err := server.Run(context.Background(), t); err != nil {
6363
log.Printf("Server failed: %v", err)
6464
}

mcp/cmd.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,37 @@ import (
1616
// A CommandTransport is a [Transport] that runs a command and communicates
1717
// with it over stdin/stdout, using newline-delimited JSON.
1818
type CommandTransport struct {
19-
cmd *exec.Cmd
19+
Command *exec.Cmd
2020
}
2121

2222
// NewCommandTransport returns a [CommandTransport] that runs the given command
2323
// and communicates with it over stdin/stdout.
2424
//
2525
// The resulting transport takes ownership of the command, starting it during
2626
// [CommandTransport.Connect], and stopping it when the connection is closed.
27+
//
28+
// Deprecated: use a CommandTransport literal.
29+
//
30+
//go:fix inline
2731
func NewCommandTransport(cmd *exec.Cmd) *CommandTransport {
2832
return &CommandTransport{cmd}
2933
}
3034

3135
// Connect starts the command, and connects to it over stdin/stdout.
3236
func (t *CommandTransport) Connect(ctx context.Context) (Connection, error) {
33-
stdout, err := t.cmd.StdoutPipe()
37+
stdout, err := t.Command.StdoutPipe()
3438
if err != nil {
3539
return nil, err
3640
}
3741
stdout = io.NopCloser(stdout) // close the connection by closing stdin, not stdout
38-
stdin, err := t.cmd.StdinPipe()
42+
stdin, err := t.Command.StdinPipe()
3943
if err != nil {
4044
return nil, err
4145
}
42-
if err := t.cmd.Start(); err != nil {
46+
if err := t.Command.Start(); err != nil {
4347
return nil, err
4448
}
45-
return newIOConn(&pipeRWC{t.cmd, stdout, stdin}), nil
49+
return newIOConn(&pipeRWC{t.Command, stdout, stdin}), nil
4650
}
4751

4852
// A pipeRWC is an io.ReadWriteCloser that communicates with a subprocess over

0 commit comments

Comments
 (0)