Skip to content

Commit acad9b2

Browse files
committed
start weaving examples
1 parent 34dd253 commit acad9b2

File tree

7 files changed

+185
-29
lines changed

7 files changed

+185
-29
lines changed

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ https://modelcontextprotocol.io/specification/2025-06-18
1010

1111
1. [Lifecycle (Clients, Servers, and Sessions)](protocol.md#lifecycle).
1212
1. [Transports](protocol.md#transports)
13+
1. [Stdio transport](protocol.md#stdio-transport)
1314
1. [Streamable transport](protocol.md#streamable-transport)
14-
1. [Stateless mode](protocol.md#stateless-mode)
15+
1. [Custom transports](protocol.md#stateless-mode)
1516
1. [Authorization](protocol.md#authorization)
1617
1. [Security](protocol.md#security)
1718
1. [Utilities](protocol.md#utilities)

docs/protocol.md

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,86 @@
99

1010
## Lifecycle
1111

12-
13-
1412
## Transports
1513

14+
Transports should not be reused for multiple connections: if you need to create
15+
multiple connections, use different transports.
16+
1617
### Streamable Transport
1718

18-
### Stateless Mode
19+
### Streamable Transport API
20+
21+
The streamable transport API is implemented across three types:
22+
23+
- `StreamableHTTPHandler`: an`http.Handler` that serves streamable MCP
24+
sessions.
25+
- `StreamableServerTransport`: a `Transport` that implements the server side of
26+
the streamable transport.
27+
- `StreamableClientTransport`: a `Transport` that implements the client side of
28+
the streamable transport.
29+
30+
To create a streamable MCP server, you create a `StreamableHTTPHandler` and
31+
pass it an `mcp.Server`:
32+
33+
```go
34+
func ExampleStreamableHTTPHandler() {
35+
// Create a new stramable handler, using the same MCP server for every request.
36+
//
37+
// Here, we configure it to serves application/json responses rather than
38+
// text/event-stream, just so the output below doesn't use random event ids.
39+
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
40+
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
41+
return server
42+
}, &mcp.StreamableHTTPOptions{JSONResponse: true})
43+
httpServer := httptest.NewServer(handler)
44+
defer httpServer.Close()
45+
46+
// The SDK is currently permissive of some missing keys in "params".
47+
resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL)
48+
fmt.Println(resp)
49+
// Output: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.1.0"}}}
50+
}
51+
```
52+
53+
The `StreamableHTTPHandler` handles the HTTP requests and creates a new
54+
`StreamableServerTransport` for each new session. The transport is then used to
55+
communicate with the client.
56+
57+
On the client side, you create a `StreamableClientTransport` and use it to
58+
connect to the server:
59+
60+
```go
61+
transport := &mcp.StreamableClientTransport{
62+
Endpoint: "http://localhost:8080/mcp",
63+
}
64+
client, err := mcp.Connect(context.Background(), transport, &mcp.ClientOptions{...})
65+
```
66+
67+
The `StreamableClientTransport` handles the HTTP requests and communicates with
68+
the server using the streamable transport protocol.
69+
70+
#### Stateless Mode
71+
72+
#### Sessionless mode
73+
74+
### Custom transports
75+
76+
### Concurrency
77+
78+
In general, MCP offers no guarantees about concurrency semantics: if a client
79+
or server sends a notification, the spec says nothing about when the peer
80+
observes that notification relative to other request. However, the Go SDK
81+
implements the following heuristics:
82+
83+
- If a notifying method (such as progress notification or
84+
`notifications/initialized`) returns, then it is guaranteed that the peer
85+
observes that notification before other notifications or calls.
86+
- Calls (such as `tools/call`) are handled asynchronously with respect to
87+
eachother.
88+
89+
See
90+
[modelcontextprotocol/go-sdk#26](https://github.com/modelcontextprotocol/go-sdk/issues/26)
91+
for more background.
1992

2093
## Authorization
2194

@@ -29,28 +102,15 @@ Cancellation is implemented with context cancellation. Cancelling a context
29102
used in a method on `ClientSession` or `ServerSession` will terminate the RPC
30103
and send a "notifications/cancelled" message to the peer.
31104

32-
For example, consider the following slow tool
33-
34-
// go get golang.org/x/example/docs/../../mcp
35-
36105
```go
37-
var (
38-
start = make(chan struct{})
39-
cancelled = make(chan struct{}, 1) // don't block the request
40-
)
41-
slowTool := func(ctx context.Context, req *CallToolRequest, args any) (*CallToolResult, any, error) {
42-
start <- struct{}{}
43-
select {
44-
case <-ctx.Done():
45-
cancelled <- struct{}{}
46-
case <-time.After(5 * time.Second):
47-
return nil, nil, nil
48-
}
49-
return nil, nil, nil
50-
}
106+
ctx, cancel := context.WithCancel(context.Background())
107+
go cs.CallTool(ctx, &CallToolParams{Name: "slow"})
108+
cancel() // cancel the tool call
51109
```
52110

53-
When an RPC exits due to a cancellation error, there's a guarantee
111+
When an RPC exits due to a cancellation error, there's a guarantee that the
112+
cancellation notification has been sent, but there's no guarantee that the
113+
server has observed it (see [concurrency](#concurrency)).
54114

55115
### Ping
56116

internal/docs/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
// The doc package generates the documentation at /doc, via go:generate.
1313
//
1414
// Tests in this package are used for examples.
15-
package doc
15+
package docs

internal/docs/protocol.src.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,39 @@ multiple connections, use different transports.
1111

1212
### Streamable Transport
1313

14+
### Streamable Transport API
15+
16+
The streamable transport API is implemented across three types:
17+
18+
- `StreamableHTTPHandler`: an`http.Handler` that serves streamable MCP
19+
sessions.
20+
- `StreamableServerTransport`: a `Transport` that implements the server side of
21+
the streamable transport.
22+
- `StreamableClientTransport`: a `Transport` that implements the client side of
23+
the streamable transport.
24+
25+
To create a streamable MCP server, you create a `StreamableHTTPHandler` and
26+
pass it an `mcp.Server`:
27+
28+
%include ../../mcp/streamable_example_test.go streamablehandler -
29+
30+
The `StreamableHTTPHandler` handles the HTTP requests and creates a new
31+
`StreamableServerTransport` for each new session. The transport is then used to
32+
communicate with the client.
33+
34+
On the client side, you create a `StreamableClientTransport` and use it to
35+
connect to the server:
36+
37+
```go
38+
transport := &mcp.StreamableClientTransport{
39+
Endpoint: "http://localhost:8080/mcp",
40+
}
41+
client, err := mcp.Connect(context.Background(), transport, &mcp.ClientOptions{...})
42+
```
43+
44+
The `StreamableClientTransport` handles the HTTP requests and communicates with
45+
the server using the streamable transport protocol.
46+
1447
#### Stateless Mode
1548

1649
#### Sessionless mode

internal/readme/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,4 @@ func main() {
4444
}
4545
}
4646

47-
//!-
47+
// !-

mcp/streamable.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,12 @@ type StreamableHTTPOptions struct {
7070
// documentation for [StreamableServerTransport].
7171
Stateless bool
7272

73-
// TODO: support session retention (?)
73+
// TODO(#148): support session retention (?)
7474

75-
// JSONResponse is forwarded to StreamableServerTransport.jsonResponse.
75+
// JSONResponse causes streamable responses to return application/json rather
76+
// than text/event-stream ([§2.1.5] of the spec).
77+
//
78+
// [§2.1.5]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server
7679
JSONResponse bool
7780
}
7881

@@ -181,7 +184,7 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
181184
return
182185
}
183186

184-
// Section 2.7 of the spec (2025-06-18) states:
187+
// 2.7] of the spec (2025-06-18) states:
185188
//
186189
// "If using HTTP, the client MUST include the MCP-Protocol-Version:
187190
// <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
209212
// assume 2025-03-26 if the client doesn't say anything).
210213
//
211214
// This logic matches the typescript SDK.
215+
//
216+
// [§2.7]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
212217
protocolVersion := req.Header.Get(protocolVersionHeader)
213218
if protocolVersion == "" {
214219
protocolVersion = protocolVersion20250326
@@ -370,6 +375,9 @@ type StreamableServerTransport struct {
370375
// request contain only a single message. In this case, notifications or
371376
// requests made within the context of a server request will be sent to the
372377
// hanging GET request, if any.
378+
//
379+
// TODO(rfindley): jsonResponse should be exported, since
380+
// StreamableHTTPOptions.JSONResponse is exported.
373381
jsonResponse bool
374382

375383
// 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
11881196
return fmt.Errorf("%s: %v", requestSummary, err)
11891197
}
11901198

1191-
// Section 2.5.3: "The server MAY terminate the session at any time, after
1199+
// §2.5.3: "The server MAY terminate the session at any time, after
11921200
// which it MUST respond to requests containing that session ID with HTTP
11931201
// 404 Not Found."
11941202
if resp.StatusCode == http.StatusNotFound {

mcp/streamable_example_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package mcp_test
6+
7+
import (
8+
"fmt"
9+
"io"
10+
"log"
11+
"net/http"
12+
"net/http/httptest"
13+
"strings"
14+
15+
"github.com/modelcontextprotocol/go-sdk/mcp"
16+
)
17+
18+
// !+streamablehandler
19+
func ExampleStreamableHTTPHandler() {
20+
// Create a new stramable handler, using the same MCP server for every request.
21+
//
22+
// Here, we configure it to serves application/json responses rather than
23+
// text/event-stream, just so the output below doesn't use random event ids.
24+
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
25+
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
26+
return server
27+
}, &mcp.StreamableHTTPOptions{JSONResponse: true})
28+
httpServer := httptest.NewServer(handler)
29+
defer httpServer.Close()
30+
31+
// The SDK is currently permissive of some missing keys in "params".
32+
resp := mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL)
33+
fmt.Println(resp)
34+
// Output: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.1.0"}}}
35+
}
36+
37+
// !-streamablehandler
38+
39+
func mustPostMessage(msg, url string) string {
40+
req := orFatal(http.NewRequest("POST", url, strings.NewReader(msg)))
41+
req.Header["Content-Type"] = []string{"application/json"}
42+
req.Header["Accept"] = []string{"application/json", "text/event-stream"}
43+
resp := orFatal(http.DefaultClient.Do(req))
44+
defer resp.Body.Close()
45+
body := orFatal(io.ReadAll(resp.Body))
46+
return string(body)
47+
}
48+
49+
func orFatal[T any](t T, err error) T {
50+
if err != nil {
51+
log.Fatal(err)
52+
}
53+
return t
54+
}

0 commit comments

Comments
 (0)