Skip to content

Commit e097918

Browse files
authored
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 679f777 commit e097918

File tree

17 files changed

+307
-251
lines changed

17 files changed

+307
-251
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func main() {
7373
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
7474

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

128128
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
129129
// Run the server over stdin/stdout, until the client disconnects
130-
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
130+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
131131
log.Fatal(err)
132132
}
133133
}

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/client/listfeatures/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func main() {
4141
ctx := context.Background()
4242
cmd := exec.Command(args[0], args[1:]...)
4343
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
44-
cs, err := client.Connect(ctx, mcp.NewCommandTransport(cmd), nil)
44+
cs, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
4545
if err != nil {
4646
log.Fatal(err)
4747
}

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{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
6262
if err := server.Run(context.Background(), t); err != nil {
6363
log.Printf("Server failed: %v", err)
6464
}

examples/server/memory/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func main() {
137137
log.Printf("MCP handler listening at %s", *httpAddr)
138138
http.ListenAndServe(*httpAddr, handler)
139139
} else {
140-
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
140+
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
141141
if err := server.Run(context.Background(), t); err != nil {
142142
log.Printf("Server failed: %v", err)
143143
}

examples/server/sequentialthinking/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ func main() {
536536
log.Fatal(err)
537537
}
538538
} else {
539-
t := mcp.NewLoggingTransport(mcp.NewStdioTransport(), os.Stderr)
539+
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
540540
if err := server.Run(context.Background(), t); err != nil {
541541
log.Printf("Server failed: %v", err)
542542
}

internal/readme/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func main() {
2020
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
2121

2222
// Connect to a server over stdin/stdout
23-
transport := mcp.NewCommandTransport(exec.Command("myserver"))
23+
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
2424
session, err := client.Connect(ctx, transport, nil)
2525
if err != nil {
2626
log.Fatal(err)

internal/readme/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func main() {
2828

2929
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
3030
// Run the server over stdin/stdout, until the client disconnects
31-
if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil {
31+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
3232
log.Fatal(err)
3333
}
3434
}

mcp/cmd.go

Lines changed: 10 additions & 6 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 {
28-
return &CommandTransport{cmd}
32+
return &CommandTransport{Command: 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

mcp/cmd_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func runServer() {
4949

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

6161
server := mcp.NewServer(testImpl, nil)
62-
if err := server.Run(ctx, mcp.NewStdioTransport()); err != nil {
62+
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
6363
log.Fatal(err)
6464
}
6565
}
@@ -116,7 +116,7 @@ func TestServerInterrupt(t *testing.T) {
116116
cmd := createServerCommand(t, "default")
117117

118118
client := mcp.NewClient(testImpl, nil)
119-
_, err := client.Connect(ctx, mcp.NewCommandTransport(cmd), nil)
119+
_, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
120120
if err != nil {
121121
t.Fatal(err)
122122
}
@@ -198,7 +198,7 @@ func TestCmdTransport(t *testing.T) {
198198
cmd := createServerCommand(t, "default")
199199

200200
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
201-
session, err := client.Connect(ctx, mcp.NewCommandTransport(cmd), nil)
201+
session, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
202202
if err != nil {
203203
t.Fatal(err)
204204
}

0 commit comments

Comments
 (0)