Commit graph go-sdk/mcp/streamable_test.go
Author SHA1 Message Date
Robert Findley
104d39c609
mcp: allow disabling stream replay; disable by default (#585)
This CL implements two changes that must be logically connected.

The first is to provide a mechanism for disabling stream replay.
Previously, our documentation stated that if
StreamableServerTransport.EventStore was nil, a default in-memory event
store would be used. In that case, there would be no way to completely
disable stream replay, and as described in #576, there are many cases
where replay is undesirable.

But of course changing the behavior of the transport zero value
implicitly affects its default behavior, and so this CL also implements
the proposal #580: stream replay is disabled by default.

Implementing this change required a significant refactoring, as
previously we were relying on the event store for serialized message
delivery from the JSON-RPC layer to the MCP layer: the connection would
write to the event store, and independently the stream (be it an
incoming POST or replay GET) would iterate and serve messages in the
stream.

In order to achieve the goals of this CL, it was necessary to decouple
storage from delivery. The 'stream' abstraction now tracks a delivery
callback that writes straight to the HTTP response. It would have been
convenient to store the ongoing http.ResponseWriter directly in the
stream (this is how typescript does it), but due to the design of our
EventStore API, only the HTTP handler knows the next event index, so a
'deliver' abstraction was an unfortunate requirement (suggestions for
how to further simplify this are welcome).

More simplification is possible: in particular, as a result of this
refactoring it should be entirely possible to clean up streams once
we've received all responses. Any replay would only need access to the
EventStore, if at all. This is left to a follow-up CL, to limit this
already significant change.

Furthermore, a nice consequence of this refactoring is that, when not
using event storage, servers can get synchronous feedback that message
delivery failed, which should avoid unnecessary work. We can
additionally cancel ongoing requests on early client termination, but
that is also left to a follow-up CL.

Throughout, the terminology 'standalone SSE stream' replaced 'hanging
GET' when referring to the non-replay GET request issued by the client.
This is consistent with other SDKs.

Fixes #576
Updates #580
2025-10-16 12:11:49 -04:00
Friedrich Große
010bdbc64d
mcp: fix goroutine leaks in unit tests (#496)
This PR fixes goroutine leaks in all unit tests.

To find the leaks I integrated https://github.com/uber-go/goleak and I
suggest to keep using it to catch any future regressions.

I used the folowing script to more easily find individual tests which
were leaking goroutines:

```
for t in $(go test -run=Nothing -list=. ./mcp | grep -v ok); do go test ./mcp -run="$t" > /dev/null && echo -n . || echo -e "\nDetected leak: $t"; done
```
2025-10-16 11:13:30 -04:00
Robert Findley
9d6e3074f9
mcp: add a explicit test for concurrent streamable sessions (#584)
We seem not to have a test that explicitly exercises concurrent
streamable sessions. Add one.
2025-10-15 14:43:45 -04:00
Robert Findley
547b5c1426
mcp: fix panics with nil logger; ensure test handlers do not panic (#557)
PR 501 added logging to the streamable server, but didn't pass a non-nil
logger to the streamable transport (an oversight). However, tests still
passed due to net/http's panic recovery.

Update test servers to enforce that handlers do not panic, and fix the
panic by:
1. Passing along the configured logger to the session transport.
2. Using ensureLogger in Connect, since we technically allow
StreamableServerTransports to be constructed by the user.

Fixes #556
2025-10-06 13:34:20 -04:00
Robert Findley
6dbf6c6450
all: minor cleanup (#545)
Also fix a flaky test case in TestStreamableServerTransport.
2025-09-30 15:53:00 -04:00
Manuel Ibar
5d64d61974
mcp: fix context propagation in StreamableClientTransport (#514)
## Problem
Context propagation was broken in `StreamableClientTransport` because:
1. `Connect()` used `context.Background()` instead of the parent context
2. `Close()` created a race condition where DELETE requests were
cancelled before completion

## Solution  
- Use parent context when creating the connection context in `Connect()`
- Reorder `Close()` operations to perform cleanup DELETE before
cancelling context

## Impact
- Request-scoped values (auth tokens, trace IDs) now propagate correctly
to background HTTP operations
- Eliminates race condition in cleanup operations  
- Maintains proper Go context semantics

Fixes #513
2025-09-23 12:27:39 -04:00
Friedrich Große
02f0b25286
Move GetSessionID closure into ServerOptions (#488)
Fixes: #478.
2025-09-18 12:21:02 -04:00
Robert Findley
353d46ff86
mcp: remove the StreamID type (#490)
The streamID type did not carry its weight. Remove it.

Fixes #484.
2025-09-18 12:01:07 -04:00
Jonathan Amsterdam
140b93980c
mcp: add CallToolResult.getError (#481)
Provide a way for middleware to get the error from a tool call,
demonstrating that we can add this functionality without making a
breaking change.

Leave getError unexported for now; we can export it (and setError) at
any time.

For #64.
2025-09-18 08:08:23 -04:00
Rob Findley
9f1b8646bd mcp: propertly validate against JSON, independent of Go values
Our validation logic was avoiding double-unmarshalling as much as
possible, by parsing before validation and validating the Go type.

This only works if the Go type has the same structure as its JSON
representation, which may not be the case in the presence of types with
custom MarshalJSON or UnmarshalJSON methods (such as time.Time).

But even if the Go type doesn't use any custom marshalling, validation
is broken, because we can't differentiate zero values from missing
values.

Bite the bullet and use double-unmarshalling for both input and output
schemas. Coincidentally, this fixes three bugs:
- We were accepting case-insensitive JSON keys, since we parsed first,
  even though they should have been rejected. A number of tests were
  wrong.
- Defaults were overriding present-yet-zero values, as noted in an
  incorrect test case.
- When "arguments" was missing, validation wasn't performed, no defaults
  were applied, and unmarshalling failed even if all properties were
  optional.

First unmarshalling to map[string]any allows us to fix all these bugs.
Unfortunately, it means a 3x increase in the number of reflection
operations (we need to unmarshal, apply defaults and validate,
re-marshal with the defaults, and then unmarshal into the Go type).
However, this is not likely to be a significant overhead, and we can
always optimize in the future.

Update github.com/google/jsonschema-go to pick up necessary improvements
supporting this change.

Additionally, fix the error codes for invalid tool parameters, to be
consistent with other SDKs (Invalid Params: -32602).

Fixes #447
Fixes #449
Updates #450
2025-09-12 10:50:19 -04:00
ankitm123
bf3ff50a16 mcp: remove json-rpc batching for more recent protocol versions
JSON-RPC batching support is removed for protocol versions greater than or
equal to protocolVersion20250618

Fixes #21
2025-09-11 10:50:56 -04:00
Rob Findley
728e0e3ac9 mcp: improve error messages from Wait for streamable clients
Previously, the error message received from ClientSession.Wait would
only report the closeErr, which would often be nil even if the client
transport was broken. Wait should return the reason the session
terminated, if abnormal.

I'm not sure of the exact semantics of this, but surely returning nil is
less useful than returning a meaningful non-nil error. We can refine our
handling of errors once we have more feedback.

Also add a test for client termination on HTTP server shutdown,
described in #265. This should work as long as (1) the session is
stateful (with a hanging GET), or (2) the session is stateless but the
client has a keepalive ping.

Also: don't send DELETE if the session was terminated with 404; +test.

Fixes #265
2025-09-10 11:24:26 -04:00
Rob Findley
b1c75f04d8 mcp: remove deprecated transport constructors
In #272, we made transports open structs and deprecated their
constructors. However, we left the constructors in place to allow go:fix
directives to facilitate migration.

Now let's remove the constructors before cutting the release.

Fixes #305
2025-09-10 11:21:46 -04:00
Rob Findley
4656930de8 mcp: avoid "null" when tools fail to return content
For some reason, this was a regression from v0.3.1, for some of our
examples. Fix it and address the existing TODO.
2025-09-05 16:57:02 -04:00
Rob Findley
9d34aff732 mcp: avoid "null" in structuredContent
Avoid a typed nil in structuredContent (yet another typed nil), leading
to "null" on the wire.

Fixes #417
2025-09-04 19:48:51 -04:00
Rob Findley
ee5141621c mcp: be strict about returning the Mcp-Session-Id header
Rather than returning the Mcp-Session-Id header for all responses, only
return it from initialize, per the spec.

Fixes #412
2025-09-04 15:34:05 -04:00
Rob Findley
a4313f92bf mcp: refactor the streamable client test to be more flexible
Use a fake streamable server to facilitate testing client behavior.
For this commit, just update the existing test (moved to a new file for
isolation). Subsequent CLs will add more tests.

Improve one client error message that occurred while debuging tests.

For #393
2025-09-04 14:09:37 -04:00
Rob Findley
07b9cee8cc mcp: flush headers immediately for the hanging GET
Flush headers immediately for the persistent hanging GET of the
streamable transport; otherwise, clients may time out.

Fixes #410
2025-09-04 12:40:07 -04:00
Rob Findley
a1eb4849b7 mcp: correctly disallow GET requests on stateless servers
The condition sessionID == "" was not quite right for disallowing GET
requests. Since we decided to differentiate "stateless" vs "sessionless"
servers, we need to disallow GET requests for both.

For #393
2025-09-03 19:25:25 -04:00
Jonathan Amsterdam
d0c5943f25
add HTTP Request to TokenVerifier (#404)
The request might be needed to verify the token.

Fixes #403.
2025-09-03 16:23:42 -04:00
ccpro
1c20560beb
mcp: export jsonResponse field
Exporting StreamableHTTPOptions.JSONResponse is helpful for some users behind proxies that don't work well with text/event-stream.

For #211

Co-authored-by: ccpro10 <co1797249167@gmail.com>
2025-09-03 11:35:19 -04:00
Jonathan Amsterdam
7769b2a959
mcp: set content to marshaled output (#398)
The ToolHandler constructed by ToolFor sets the result's Content to the
marshaled output, following the spec's suggestion.

See
https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content.

Also, set StructuredContent to the marshaled RawMessage to avoid a
double marshal.

Fixes #391.
2025-09-03 10:37:27 -04:00
Takeshi Yoneda
a76bae3a11 address review comments
Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
2025-08-27 16:03:32 -04:00
Takeshi Yoneda
c24d9856c4 mcp/streamable: fixes broken DELETE request on connection close
Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
2025-08-27 16:03:32 -04:00
Hongxiang Jiang
fb69971650
mcp: cleanup transport after keepalive ping fails
An onClose function is passed to the ServerSession and ClientSession to
help cleanup resources from the caller.

The onClose function will be executed as part of the ServerSession and
ClientSession closure.

Fixes #258
2025-08-22 15:26:52 -04:00
Jonathan Amsterdam
18c96d6912
mcp: enforce input schema type "object" (#349)
The spec makes it clear that input schemas must have type "object".
Enforce that.
Allow "any" as an input argument type by special-casing it.

Fixes #283.
2025-08-22 11:04:44 -04:00
Jonathan Amsterdam
bf79d784b9
mcp: remove references to ClientRequest[T] for concrete T (#343)
See related PR about ServerRequest[T].
2025-08-21 15:24:58 -04:00
Jonathan Amsterdam
6378df6f24
all: remove ServerRequest[T] for concrete T (#342)
Replace all occurrences of ServerRequest[*CallToolParams] and other
concrete instantiations with CallToolRequest and the like.

Make the XXXRequest types aliases, to preserve the convenience of
generics for the internal machinery (see shared.go, for example.)

I will expand the aliases in a followup PR.
2025-08-21 12:10:53 -04:00
Jonathan Amsterdam
5b1f328fd4
mcp: uncomment test, change struct{} to any (#341)
Address comments on #325.
2025-08-21 10:18:52 -04:00
Jonathan Amsterdam
32cf71de3a
mcp: change tool handler design (#325)
Change the design of tool handlers by removing genericity from the common path.
- `ToolHandler` gets the args as a `json.RawMessage`. Using Server.AddTool does no unmarshaling or schema validation.
- `ToolHandlerFor`, installed with `AddTool`, are the only generic pieces.

TODO:
uncomment and fix some tests
2025-08-20 19:36:34 -04:00
Rob Findley
79f063bc98 mcp: handle the Mcp-Protocol-Version header correctly
Handle the protocol version header according to section 2.7 of the spec
(and the other SDKs).

Fixes #198
2025-08-20 16:48:08 -04:00
Jonathan Amsterdam
8e6ab130d8
mcp: pass TokenInfo to server handler (#292)
If there is a TokenInfo in the request context of a
StreamableServerTransport, then propagate it through to the
ServerRequest that is passed to server methods like callTool.

Fixes #317.
2025-08-20 10:50:16 -04:00
Rob Findley
98e67fdf62 mcp: improvements for 'stateless' streamable servers; 'distributed' mode
Several improvements for the stateless streamable mode, plus support for
a 'distributed' (or rather, distributable) version of the stateless
server.

- Add a 'Stateless' option to StreamableHTTPOptions and
  StreamableServerTransport, which controls stateless behavior.
  GetSessionID may still return a non-empty session ID.
- Audit validation of stateless mode to allow requests with a session
  id. Propagate this session ID to the temporary connection.
- Peek at requests to allow 'initialize' requests to go through to the
  session, so that version negotiation can occur (FIXME: add tests).

Fixes #284
For #148
2025-08-20 09:59:52 -04:00
Rob Findley
0eb03e0d76 mcp: factor out reusable functions from TestStreamableServerTransport
The next CL will test stateless and distributable server transport
configurations, using the HTTP testing strategy of
TestStreamableServerTransport.
2025-08-20 09:59:52 -04:00
Robert Findley
46ba813860
.github: add a staticcheck action (#326)
Add a staticcheck step to the lint job of the Test workflow, and fix
reported diagnostics.
2025-08-19 14:58:01 -04:00
Rob Findley
3db848abec mcp: implement a concurrency model for calls
Implement the concurrency model described in #26: notifications are
synchronous, but calls are asynchronous (except for 'initialize'). To
achieve this, implement jsonrpc2.Async(ctx) to signal asynchronous
handling. This is simpler to use than returning ErrAsyncResponse and
calling Respond, and since this is an internal detail we don't need to
worry too much about whether it's idiomatic.

Add tests that verify both features, for both client and server.

Also:
- replace req.ID.IsValid with req.IsCall
- remove the methodHandler type as we can just use MethodHandler

Fixes #26
2025-08-19 11:21:02 -04:00
Matt L
75f9999590
feat(mcp): export initialized result in client session (#320)
Export ServerCapabilities and related subtypes and update protocol,
server, and tests to use exported types. Add InitializeResult method on
ClientSession for accessing the initialize result from sessions.

Fixes: https://github.com/modelcontextprotocol/go-sdk/issues/166

Co-authored-by: Shashank Pachava <spachava753@gmail.com>
2025-08-18 14:49:02 -04:00
Sam Thanawalla
767dacb820
mcp/streamable: remove StreamableReconnectOptions (#319)
Moves MaxRetries into the parent struct and assume a negative integer
signifies to not retry.

Fixes #308
2025-08-18 13:53:34 -04:00
Rob Findley
1afdb1f273 mcp: several fixes for streamable client reconnection
Do a pass through the streamable client reconnection logic, and fix
several bugs.

- Establish the initial GET even if MaxRetries is 0 (#256). This was
  broken because the GET bypasses the initial request and going straight
  to the SSE GET reconnection logic.
- Release the stream ownership when POST requests exit.
- Don't reconnect POST requests if we've received the expected response.
- Move unexported reconnection config to constants. Otherwise it is too
  hard to set ReconnectOptions (you have to use the DefaultOptions and
  mutate).

Fixes #256
2025-08-15 16:48:09 -04:00
Hugh Palmer
0a8fe40c51 mcp: changed streamID's from int64 to random strings (#266)
- Changed StreamID's to store randomly generated strings.
- Updated all tests.
- Resolved conflicts
2025-08-15 09:38:18 -04:00
Rob Findley
54c99810f6 mcp: add a test for streamable accept headers
Add a test following up on the fix for #290.
2025-08-14 15:42:57 -04:00
Rob Findley
bb6dadecca mcp: fix cancellation for HTTP transport
In #202, I added the checkRequest helper to validate incoming requests,
and invoked it in the stremable transports to preemptively reject
invalid HTTP requests, so that a jsonrpc error could be translated to an
HTTP error.

However, this introduced a bug: since cancellation was handled in the
jsonrpc2 layer, we never had to validate it in the mcp layer, and
therefore never added methodInfo. As a result, it was reported as an
invalid request in the http layer.

Add a test, and a fix. The simplest fix was to create stubs that are
placeholders for cancellation.

This was discovered in the course of investigating #285.
2025-08-14 11:32:51 -04:00
Robert Findley
87f222477b
mcp: add a test for streamable sampling during a tool call
Add a test that attempts (and fails) to reproduce the bug reported in
issue #285.

For #285
2025-08-13 10:11:01 -04:00
Jonathan Amsterdam
6e03217c83
mcp: introduce Requests (#267)
Handlers and client/server methods take a single request argument
combining the session and params. This slightly simplifies the signature
for handlers, since there is no longer an often-ignored session
argument.

But more importantly, it opens the door to adding more information in
requests, such as auth info and HTTP request headers.

For #243.
2025-08-12 17:49:50 -04:00
Robert Findley
e097918484
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
2025-08-12 15:14:21 -04:00
Robert Findley
dae88535ba
mcp: support stateless streamable sessions (#277)
Support stateless streamable sessions by adding a GetSessionID function
to StreamableHTTPOptions. If GetSessionID returns "", the session is
stateless, and no validation is performed. This is implemented by
providing the session a trivial initialization state.

To implement this, some parts of #232 (distributed sessions) are copied
over, since they add an API for creating an already-initialized session.

In total, the following new API is added:
- StreamableHTTPOptions.GetSessionID
- ServerSessionOptions (a new parameter to Server.Connect)
- ServerSessionState
- ClientSessionOptions (a new parameter to Client.Connect, for symmetry)

For #10
2025-08-11 16:43:46 -04:00
Rob Findley
0375535db4 mcp: implement support for JSON responses in the MCP streamable server
Add a new (currently unexported) jsonResponse option to
StreamableServerTransportOptions, and use it to control the response
content type, serving application/json if set.

Additionally:
- A transportOptions field is added to the StreamableHTTPOptions.
- A messages iterator is added to encapsulate the iteration of stream
  messages, since the handling of JSON and SSE responses are otherwise
  very different.
- The serving flow is refactored to avoid returning (statusCode,
  message), primarily because this seemed liable to lead to redundant
  calls to WriteHeader, because only local logic knows whether or not
  any data has been written to the response.
- The serving flow is refactored to delegate to responseJSON and
  responseSSE, according to the currently unexported jsonResponse
  option.
- A bug is fixed where all GET streams were considered persistent: the
  terminal condition req.Method == http.MethodPost && nOutstanding == 0
  was not right: GET requests may implement stream resumption.

Updates #211
2025-08-08 12:38:43 -04:00
Rob Findley
6ea7a6c653 mcp: simplify and fix the streamable client transport
Make several cleanups of the streamable client transport, encountered
during work on JSON support for the streamable server:

- The 'Close' condition is differentiated from asynchronous failures. A
  failure should unblock Read with an error, at which point the JSON-RPC
  connection will be broken and closed.
- Fields are reordered in streamableClientConn to make guards more
  apparent.
- The handling of sessionID is simplified: we simply set the session ID
  whenever we receive response headers. No need to have special handling
  for the first request, as the serializeation of session initialization
  is implemented in Client.Connect.
- Since the above bullet makes Write a trivial wrapper around
  postMessage, the two methods are merged.
- A bug is fixed where JSON responses were handled synchronously in
  Write. This lead to deadlock when a hanging client->server request is
  waiting on a server->client request. Now JSON is handled symmetrically
  to SSE: the Write returns once response headers are received.
  asynchronous to Write.
- The httpConnection interface is renamed to clientConnection, and
  receive the entire InitializeResult.
- streamableClientConn receivers are renamed to be consistently 'c'.
2025-08-08 12:38:43 -04:00
Sam Thanawalla
e9e0da8dac
jsonschema: remove jsonschema code and depend on google/jsonschema-go (#262)
This CL prunes the jsonschema directory and updates the go.mod.

For #28
2025-08-07 15:30:06 -04:00
Sam Thanawalla
faaf77dc05
mcp/server: advertise completions only if installed (#213)
Previously, completions would always be advertised as a capability even
if the CompletionHandler was not installed by the server. This CL fixes
that.
2025-08-04 10:12:01 -04:00