Commit graph go-sdk/mcp/streamable.go
Author SHA1 Message Date
Guglielmo Colombo
4cbdd6a514
mcp: Implement stateless server (SEP-2275) (#965)
## Description

This PR lays the foundational server-side groundwork for the `>=
2026-06-30` sessionless and stateless feature introduced by
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)
and SEP-2567, tracked in
[design/stateless.md](614460a225/design/stateless.md).

### SEP-2575: Stateless MCP

*   **Per-request protocol detection in `ServerSession.handle()`**
Unmarshal `_meta` from the raw JSON-RPC params determines whether each
request follows the new protocol.

*   **Per-request typed accessors on `ServerRequest[P]`**
    ```go
    func (r *ServerRequest[P]) ProtocolVersion() string
    func (r *ServerRequest[P]) ClientInfo() *Implementation
    func (r *ServerRequest[P]) ClientCapabilities() *ClientCapabilities
    ```

* **Reject client->server `initialize`, `initialized` and `ping` for
new-protocol requests**

*   **Per-request `_meta` field name constants**
Three constants for the wire-protocol field names
(`MetaKeyProtocolVersion`, `MetaKeyClientInfo`,
`MetaKeyClientCapabilities`)

* **Stop synthesizing fake `InitializeParams` for new-protocol
requests**

Fixes: #966
2026-05-26 14:25:51 +00:00
Maciej Kisiel
1253d17add
mcp: do not call DELETE for sessionless & doc adjustments (#960) 2026-05-15 21:47:34 +02:00
Maciej Kisiel
8eb54632ad
mcp: remove session header handling in stateless mode (#952) 2026-05-15 09:51:14 +02:00
Maciej Kisiel
9f5e89fea2
mcp: refactor streamable handler control flow (#949) 2026-05-08 14:29:01 +02:00
Guglielmo Colombo
909372d8d1
mcp: HTTP Header Standardization for x-mcp-header (#915)
## Description

Implements
[SEP-2243](https://modelcontextprotocol.io/seps/2243-http-standardization)
(HTTP Header Standardization) for x-mcp-param custom header.

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

---------

Co-authored-by: Maciej Kisiel <mkisiel@google.com>
2026-05-08 12:39:18 +02:00
Justin Chang
d10c315008
mcp: write SSE comment on standalone stream so HTTP/2 reverse proxies flush HEADERS frame (#938)
Fixes #937.

### What this changes

Adds a single line — an SSE comment (`: ok\n\n`) — after `WriteHeader`
and before `Flush` on the standalone SSE GET stream. SSE comments are
explicitly ignored by clients (any line starting with `:` per the SSE
spec), but they produce an HTTP/2 DATA frame, which is what HTTP/2
reverse proxies need before they will forward the HEADERS frame.

### Why the existing fix isn't sufficient

Issue #410 was closed by #413, which added `WriteHeader(StatusOK)` +
`Flush()` to the same site. #870 refactored the `Flush()` to use
`http.NewResponseController`. Both correctly address HTTP/1.1: `Flush()`
writes the headers as text on the TCP stream and any HTTP/1.1-aware
proxy forwards them.

In HTTP/2, headers and body are separate frame types (HEADERS, DATA).
Reverse proxies batch them for efficiency and won't forward the HEADERS
frame on its own. `Flush()` on the response writer only pushes the
in-process buffer to the HTTP/2 stack; the proxy still holds the HEADERS
frame waiting for DATA. The standalone SSE stream never sends body data
on connect (the whole point — it's the long-poll listener for
server-pushed notifications), so without a deliberate "kick" the headers
never reach the client until the proxy tears down the stream on timeout.

The SSE-comment trick is the established mitigation for SSE servers
behind HTTP/2 proxies. It's used in Caddy
(https://github.com/caddyserver/caddy/issues/4247), referenced in the Go
issue tracker (https://github.com/golang/go/issues/31125), and is
independent of the proxy implementation.

### The diff

```go
 if s.id == "" {
     // Issue #410: the standalone SSE stream is likely not to receive messages
     // for a long time. Ensure that headers are flushed.
+    //
+    // On HTTP/2, headers and body travel as separate frames (HEADERS and
+    // DATA). Reverse proxies (e.g. Envoy, Caddy, net/http/httputil)
+    // commonly buffer the HEADERS frame until they have a DATA frame to
+    // coalesce it with — there is no HTTP/2 equivalent of HTTP/1.1's
+    // Transfer-Encoding: chunked signal that says "this is streaming, send
+    // headers now". Calling Flush() alone is not sufficient: it pushes
+    // the kernel buffer to the proxy, but the proxy still holds the
+    // HEADERS frame.
+    //
+    // Write an SSE comment (lines starting with ":" are ignored by
+    // clients per RFC) so a DATA frame is produced, which forces the
+    // proxy to forward both frames. See:
+    //   https://github.com/golang/go/issues/31125
+    //   https://github.com/caddyserver/caddy/issues/4247
     w.WriteHeader(http.StatusOK)
+    fmt.Fprint(w, ": ok\n\n")
     rc := http.NewResponseController(w)
     // Ignore returned error as flushing is best-effort.
     _ = rc.Flush()
 }
```

`fmt` is already imported in this file; no other dependencies change.

### Test

Added `TestStandaloneSSEEmitsCommentForHTTP2Flush`. It initializes a
streamable session via raw POST, then opens the standalone SSE GET and
asserts the first body byte starts with `:` (the SSE comment marker,
which is what produces the DATA frame HTTP/2 proxies need).

The test catches the bug behaviorally without requiring a full HTTP/2
reverse-proxy setup: it verifies the SDK's contract (emit body bytes
after headers on the standalone SSE stream). HTTP/2-specific behavior is
then a property of any conforming proxy.

I confirmed the test fails on `main` without the patch — it times out at
2s waiting for the first body byte — and passes in <1ms with the patch.
Full streamable test suite still passes.

### Reproduction matrix (from #937)

| Configuration | TTFB | Result |
|---|---|---|
| HTTP/2 + 30s request timeout | ~31s | Headers only on stream tear-down
|
| HTTP/2 + no request timeout | never | Headers never arrive |
| HTTP/1.1 + 30s request timeout | ~300ms | Headers immediate |
| HTTP/1.1 + no request timeout | ~270ms | Headers immediate |
| HTTP/2 POST (response has body) | ~350ms | Works (DATA frame
coalesces) |
| **HTTP/2 + this PR** | ~1ms | Headers immediate |

### Risk

- Behavior-preserving for HTTP/1.1 clients (a leading `:` line is a
valid SSE comment and is ignored by every conforming SSE client).
- No protocol-level concern: SSE explicitly defines comment lines as a
no-op for the consumer.
- The DATA frame adds 7 bytes per standalone SSE connection, sent once
on connect.

Co-authored-by: Maciej Kisiel <mkisiel@google.com>
2026-05-07 14:28:38 +02:00
Scott Leggett
4f1b3e5062
fix: handle oauth2.RetrieveError during token refresh (#917)
Previously, an expired refresh token in the oauth2.Token returned from
OAuthHandler.TokenSource() would cause the connection to fail.

From the client perspective, this meant that the MCP connection was in a
hard-failed state with no way to re-authorize.

The change in this commit causes Authorize() to be called in the event
of both an oauth2.RetrieveError from the token endpoint, as well as in
the pre-existing case of a 401/403 HTTP response from the MCP server.
Clients will handle this in their existing Authorize() flows to get a
new valid token for the connection.

Supersedes and closes: #909.

Co-authored-by: Yaroslav <yarolegovich@gmail.com>
2026-05-04 13:07:03 +02:00
Dayna Blackwell
8e45782dbd
mcp: close HTTP response body from DELETE request in streamableClientConn.Close (#929)
## Summary

The DELETE request that terminates a streamable HTTP session discards
the `*http.Response` without closing its body. Per the `net/http`
documentation, this prevents the underlying TCP connection from being
reused and leaks it.

## The bug

```go
// mcp/streamable.go:2239 (before)
} else if _, err := c.client.Do(req); err != nil {
    c.closeErr = err
}
```

On the success path (no error), the response is silently discarded. The
default client is `http.DefaultClient` with keep-alives enabled, so the
leaked connection stays open until GC or process exit.

## The fix

```go
// mcp/streamable.go:2239 (after)
} else if resp, err := c.client.Do(req); err != nil {
    c.closeErr = err
} else {
    resp.Body.Close()
}
```

All 10 other `client.Do` call sites in this file close the response
body. This one was missed.

## Test plan

- [x] `go build ./...` passes
- [x] `go test ./... -short` passes

Fixes #928
2026-05-04 12:14:20 +02:00
Maciej Kisiel
f5f2015413
MCPGODEBUG update for 1.6.0 (#893) 2026-04-30 09:54:32 +02:00
Guglielmo Colombo
e01639a318
feat: HTTP Header Standardization for method and name (#907)
## Description

Implements SEP-2243
(https://modelcontextprotocol.io/seps/2243-http-standardization) (HTTP
Header Standardization) for Mcp-Method and Mcp-Name standard headers
(x-mcp-param custom headers will be covvered in separate PR).

Fixes #905
2026-04-29 17:33:39 +02:00
Ravish Gupta
db50910c70
mcp: do not re-prompt OAuth after cancelled Authorize (#885)
When `OAuthHandler.Authorize` returns a context-cancelled error, the
cancellation notification that the call layer sends in response to the
same ctx cancellation is routed back through the streamable client
transport, re-triggering the `401` → `Authorize` path and prompting the
user a second time for a request they have already abandoned.

Fix in two layers:

1. **`mcp/transport.go`** — bound the cancellation `Notify` with a fresh
`context.Background()`-derived timeout instead of reusing the cancelled
caller context. This guarantees the caller never blocks on a doomed
notify against a degraded connection.
2. **`mcp/streamable.go`** — when `Authorize` returns a
context-cancelled error, mark the streamable client connection as failed
via `c.fail()`. The existing failure check in `Write()` then
short-circuits the cancellation notification without re-invoking the
OAuth handler, matching the bug reporter's expectation that cancelling
`Authorize` abandons the connection.

Includes a regression test
(`TestStreamableClientOAuth_CancelledAuthorize_NoReprompt`) with a
blocking `OAuthHandler` that asserts `Authorize` is invoked exactly
once. The test was verified to fail on unfixed code (`got 2` calls) and
pass with the fix.

Fixes #882

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:58:57 +02:00
John Pugliesi
5f2cd8fd14
mcp: preserve transport errors in Write error chain (#888)
mcp: preserve transport errors in Write error chain

Custom RoundTrippers that return typed errors (e.g. for auth failures)
have those errors flattened to strings by `%v`, making `errors.As`
unusable for callers inspecting transport-level errors.

This is inconsistent with the OAuth handler path (line 1807) which
already uses `%w` to preserve errors for client inspection.

Relates to #579

Co-authored-by: Maciej Kisiel <mkisiel@google.com>
2026-04-24 13:20:45 +02:00
Maciej Kisiel
41e1f941bb
mcp: remove default cross origin protection (#906)
Fixes #902
2026-04-24 11:35:22 +02:00
Kalvin C
35f1b07e48
mcp: accept parameterized Content-Type types (#890)
Use a shared helper for Content-Type parsing in streamable transport
request validation and client response handling.

Follow up to https://github.com/modelcontextprotocol/go-sdk/pull/853,
where we did this for `Accept` headers.

This allows the streamable mcp server to validate the following header
`Content-Type: application/json;charset=utf-8`

Co-authored-by: Maciej Kisiel <mkisiel@google.com>
2026-04-23 08:51:47 -04:00
Maciej Kisiel
15e93a231c
mcp: add DNS rebinding and cross origin protections to SSE transport (#891)
Similar protections were already introduced before to the Streamable
transport.
2026-04-14 11:36:20 +02:00
Rafael Dantas Justo
27f29c1cef
mcp: allow Content-Type parameters in streamable transport (#878)
Use `mime.ParseMediaType` to parse the `Content-Type` header instead of
exact string matching. Previously, the server required an exact match on
`application/json`.

This allows the server to accept `application/json` with parameters
(e.g. `charset=utf-8`), even though [they are not
required](https://datatracker.ietf.org/doc/html/rfc4627.html#section-6),
following the robustness principle.

Per [RFC 8259 (section
8.1)](https://datatracker.ietf.org/doc/html/rfc8259.html#section-8.1),
`application/json` is always UTF-8, so such parameters are technically
redundant. However, parsing the media type makes the server more
tolerant of client behaviour, improving compatibility without
compromising security.
2026-04-03 17:42:20 +02:00
Dan McGee
d3fd25b68a
mcp: use http.ResponseController to ensure writes are flushed (#870)
If the ResponseWriter is a wrapped one (which is quite common when using
middleware to log things like response sizes), the direct cast to
`http.Flusher` doesn't find the flush capability. Instead, use
`http.ResponseController` as introduced in go1.20 which handles
unwrapping to find the base response writer.
2026-03-31 13:13:16 +02:00
Kalvin C
c1a12cfeda
mcp: accept parameterized Accept media types (#853)
Normalize accept tokens before validation so valid headers with media
type parameters still match their base media types.

This allows the streamable mcp server to validate the following header
`Accept: application/json;charset=utf-8, text/event-stream`

Co-authored-by: Maciej Kisiel <mkisiel@google.com>
2026-03-31 11:17:49 +02:00
Oleksandr Redko
b71a8391a6
all: fix typos (#869)
Grammar issues in docs, comments, and error messages.
2026-03-30 18:43:01 +02:00
Maciej Kisiel
755b9ed4df
mcp: re-enable race test after fixing data races (#851)
1. (non-test) Don't queue notifications if there are no connected
sessions.
2. Change `reconnectInitialDelay` to be atomic, as it is modified in
tests.
3. Protect map access with mutex in `TestServerTransportCleanup`.
2026-03-23 08:35:27 +01:00
Maciej Kisiel
a433a831d6
mcp: verify 'Origin' and 'Content-Type' headers (#842)
This PR introduces protection against cross-origin requests by
installing `http.CrossOriginProtection`.
By default, the zero value is used which does not contain any trusted
origins or bypass patterns.
The protection can be customized by providing a custom
`http.CrossOriginProtection` in `mcp.StreamableHTTPOptions`. Currently,
the deny handler set on the protection is ignored.
This default has an MCPGODEBUG option (`disableoriginverification`) to
disable the default protection.
It will be available until `v1.6.0` of the SDK.
We also increase the Go version required by the SDK to 1.25, since this
is the version that introduced `http.CrossOriginProtection`. This is in
line with the SDK's policy of supporting two newest Go versions (1.26
was released in February 2026).

Additionally, we start validating if the `Content-Type` header for
`POST` requests is set to `application/json` to avoid accepting
CORS-safelisted requests.
2026-03-13 11:36:41 +01:00
Maciej Kisiel
c9317fb5b7
all: client side OAuth support (#785)
This PR introduces client-side OAuth support.

# Overall design philosophy

The main new interface introduced to support client-side OAuth is the
`OAuthHandler`. Contrary to its counter-parts in some other SDKs, it is
intentionally small and generic so that it can support most of the OAuth
authorization flow variants. The main `mcp` package purposefully
delegates almost all auth handling to `OAuthHandler` implementations.
This way, the `mcp` package continues to be largely OAuth agnostic.

The `auth` package comes with a default `OAuthHandler` implementation –
`AuthorizationCodeHandler`, which implements the authorization flow
for the `authorization_code` grant type, as defined in the [MCP
specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
It is expected that the `OAuthHandler` implementation granularity will
be roughly per grant type, as this parameter influences the
authorization flow the most.

# Detailed design

## `OAuthHandler` (auth/client.go)

The main interface for the integration consists of two methods:

```go
type OAuthHandler interface {
	// TokenSource returns a token source to be used for outgoing requests.
	// Returned token source might be nil. In that case, the transport will not
	// add any authorization headers to the request.
	TokenSource(context.Context) (oauth2.TokenSource, error)

	// Authorize is called when an HTTP request results in an error that may
	// be addressed by the authorization flow (currently 401 Unauthorized and 403 Forbidden).
	// It is responsible for performing the OAuth flow to obtain an access token.
	// The arguments are the request that failed and the response that was received for it.
	// The headers of the request are available, but the body will have already been consumed
	// when Authorize is called.
	// If the returned error is nil, TokenSource is expected to return a non-nil token source.
	// After a successful call to Authorize, the HTTP request will be retried by the transport.
	// The function is responsible for closing the response body.
	Authorize(context.Context, *http.Request, *http.Response) error
}
```

`StreamableClientTransport` accepts this interface as a field and uses
the `TokenSource` method to add an `Authorization: Bearer` header to
outgoing requests to the MCP server. If the request fails with `401:
Unauthorized` or `403: Forbidden`, the transport calls `Authorize` to
perform the authorization flow. If `Authorize` returns a non-nil error,
the request is retried once. `Authorize` errors do not result in
termination of the client session (unless they happen during
connection initialization), as some OAuth flows are multi-legged and it should be
acceptable to retry for example when the authorization grant was
received.

*Note:* There was already an `OAuthHandler` type defined as part of a
previous iteration of OAuth support. It was renamed to
`OAuthHandlerLegacy`. This is the only backwards incompatible API change
in the experimental client `auth` part (protected by the
`mcp_go_client_oauth` Go build tag). The cost of replacing previous
usages is deemed worthwhile to maintain clear naming, under the
assumption that the experimental client `auth` part was not widely used.

## `AuthorizationCodeHandler` (auth/authorization_code.go)

This is the `OAuthHandler` implementation that fulfills the [MCP
specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
In particular, in supports:

- Protected Resource Metadata discovery and fetching
- Authorization Server Metadata discovery and fetching
- Client registration
  - OpenID Connect metadata document
  - Pre-registration
  - Dynamic Client Registration
- PKCE
- `client_secret_post`, `client_secret_basic` token endpoint
auth methods
- Automatic refresh of tokens

### Authorization Server redirect

As part of the flow for the `authorization_code` grant type, the client
needs to point the user to the authorization URL, so that they confirm
the access request. This design proposes that it will be supported via a
pluggable dependency that will initiate this process in an
application-specific way:

```go
type AuthorizationCodeHandler struct {
	...

	// AuthorizationCodeFetcher is a required function called to initiate the authorization flow.
	// It is responsible for opening the URL in a browser for the user to start the authorization process.
	// It should return the authorization code and state once the Authorization Server
	// redirects back to the RedirectURL.
	AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error)

	...
}
```

The function is supposed perform any application specific logic to handle the redirect
from the Authorization Server and return the resulting code and state.

## `oauthex` changes

Some additional/adjusted building blocks that may be useful when
creating `OAuthHandler` implementations are present in the `oauthex`
package:

- `auth_meta.go`: Authorization Server Metadata utilities:
  - `GetAuthServerMeta`: get Authorization Server Metadata from a URL
- `dcr.go`: Dynamic Client Registration utilities (unchanged)
- `resource_meta.go`: Protected Resource Metadata utilities:
- `GetProtectedResourceMetadata`: get Protected Resource Metadata from a
URL
  - `ParseWWWAuthenticate` – as per name (unchanged)
- [deprecated] `GetProtectedResourceMetadataFromID` and
`GetProtectedResourceMetadataFromHeader` in favor of more generic
`GetProtectedResourceMetadata`. They will be kept under the Go build tag
and removed one version of the SDK in the future.
2026-02-27 15:30:13 +01:00
Ronald
4e8b6ca17d
mcp: return 400 instead of 500 when body read fails in stateless mode (#817)
## Summary

Changes `http.StatusInternalServerError` to `http.StatusBadRequest` when
`io.ReadAll(req.Body)` fails during stateless mode initialization in the
streamable HTTP handler.

Fixes #816

## Rationale

Body read failures are client-side errors (disconnection, timeout,
incomplete request), not server errors. Returning 500 is misleading and
causes unnecessary alert fatigue for users monitoring their MCP servers.

This makes the behavior consistent with `servePOST` which already
returns 400 for the same error condition.

## Testing

- Existing tests pass
- This is a one-line change affecting only the HTTP status code returned

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-27 15:15:33 +01:00
Maciej Kisiel
7b8d81c264
all: use case-sensitive JSON unmarshaling (#807)
As reported in
https://github.com/modelcontextprotocol/go-sdk/issues/805, case
insensitive unmarshaling can pose security issues. This change adjusts
all non-test and non-example unmarshaling to use a case sensitive mode.

This introduces a new external dependency
`github.com/segmentio/encoding`.
2026-02-18 16:38:36 +01:00
Paul Carleton
67bd3f2e2b
mcp: add automatic DNS rebinding protection for localhost servers (#760)
## Summary

Add DNS rebinding protection that is automatically enabled when requests
arrive via localhost (127.0.0.1, [::1]). This protects against malicious
websites using DNS rebinding to interact with local MCP servers.

(note: this was a claude assisted PR, mostly wanted to see how difficult
passing this test would be for this SDK)

## Design Goal: Secure by Default

The primary goal is to make it difficult to run a localhost server
without these protections by mistake. There are other approaches that
could provide secure defaults (e.g., a helper for `ListenAndServe` that
does the localhost check), but using `http.LocalAddrContextKey` for
runtime detection seemed like the most backwards compatible approach and
least likely to be disabled by accident.

With this implementation:
- **No code changes required** - existing servers get protection
automatically
- **No opt-in needed** - protection activates based on the connection's
local address
- **Explicit opt-out** - users must deliberately set
`DisableLocalhostProtection: true`

## Changes

- Add `DisableLocalhostProtection` option to `StreamableHTTPOptions`
- Add `isLocalhostAddr` and `isLocalhostHost` helper functions  
- Validate Host header at start of `ServeHTTP`, rejecting non-localhost
Host headers with 403 Forbidden

## How it works

The protection uses `http.LocalAddrContextKey` to detect the
connection's local address at runtime. When a request arrives via
localhost (127.0.0.1 or [::1]), the handler validates that the Host
header also matches a localhost value. If not, the request is rejected
with 403 Forbidden.

This approach means:
- Protection is enabled for requests arriving via localhost, regardless
of whether the server listens on `127.0.0.1` or `0.0.0.0`
- Requests arriving via non-localhost IPs (e.g., external network
requests) are **not** affected

### Edge case: Reverse proxies

If a reverse proxy (e.g., Envoy, nginx) runs on the same host and
forwards requests to the MCP server via localhost while preserving the
original Host header, those requests would be rejected. In this case,
users should either:
1. Set `DisableLocalhostProtection: true`
2. Configure the proxy to rewrite the Host header to localhost

## Testing

- Added unit tests for `isLocalhostAddr` and `isLocalhostHost` helper
functions
- Added integration tests for the full protection flow
- Verified against the MCP conformance test suite:
  - `localhost-host-rebinding-rejected`: PASS
  - `localhost-host-valid-accepted`: PASS

## Related

- Spec:
https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
- TypeScript SDK implementation: `localhostHostValidation()` middleware
- Conformance test: `dns-rebinding-protection` scenario

---------

Co-authored-by: Maciek Kisiel <mkisiel@google.com>
2026-02-13 16:23:32 +01:00
Maciek Kisiel
d0922abcd3 mcp: allow SSE messages with empty data (SEP-1699) 2026-02-05 15:36:53 +01:00
Alexandru Bumbacea
1fb6e204d7
mcp: improve http transports error handling and make transport work with any size message (#734)
We have a use case for messages larger than 1MB, hence making this
configurable would help.
In order to discover the source of our problems we had to spend some
time investigating, as errors were silently discarded. So we also
included changes to improving surfacing errors .

---------

Co-authored-by: Maciej Kisiel <macio92@gmail.com>
2026-02-05 12:59:37 +01:00
FanOne
7ecd8049c6
mcp: export session missing error (#771)
Fix: #715

- export session missing error
- modify `errSessionMissing` to `ErrSessionMissing`
- add unit test about session missing error
2026-02-03 15:46:49 +01:00
Suraj Bobade
df4f0d56a4
mcp: use rand.Text and slog.DiscardHandler over internal implementation (#773)
Address TODOs in code requiring go 1.24 as minimum version

mcp/logging.go: use slog.DiscardHandler, over local implementation
mcp/util.go: use rand.Text, over local implementation

Fixes #772
2026-02-03 14:23:59 +01:00
lif
cf88bda0c5
fix: enforce retry limit when SSE stream makes no progress (#742)
Fixes #679

## Changes
- Track last event ID to detect progress in SSE streams
- Only reset retry counter when Last-Event-ID advances
- Fail connection after exceeding maxRetries without progress

This implements Option 2 from the issue: progress-based retry limiting
that prevents infinite retry loops while allowing long-running streams
to continue as long as progress is made.

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
2026-02-03 10:17:32 +01:00
Sam Morrow
52e03de6c8
mcp: add Allow header to 405 responses per RFC 9110 §15.5.6 (#757)
405 Method Not Allowed responses MUST include an Allow header listing
supported methods, per RFC 9110 Section 15.5.6. This fixes issues with
strict HTTP gateways (like Apigee) that treat 405 responses without an
Allow header as malformed, returning 502 Bad Gateway errors.

## Changes

- **SSEHandler**: Add `Allow: GET, POST` header for unsupported methods
- **StreamableHTTPHandler**: Add Allow header for GET-without-session case:
  - Stateless mode: `Allow: POST, DELETE` (GET is never valid)
  - Stateful mode: `Allow: GET, POST, DELETE` (GET is valid once you have
a session)
- Add tests to verify Allow header presence in all 405 responses

## RFC 9110 Reference

[Section 15.5.6 (405 Method Not
Allowed)](https://httpwg.org/specs/rfc9110.html#status.405):

> The origin server MUST generate an Allow header field in a 405
response containing a list of the target resource's currently supported
methods.

## Testing

All existing tests pass, plus new tests added:
- `TestSSE405AllowHeader` - verifies SSE handler compliance
- `TestStreamable405AllowHeader` - verifies Streamable handler
compliance in both stateful and stateless modes

---------

Co-authored-by: omgitsads <4619+omgitsads@users.noreply.github.com>
2026-01-22 09:43:45 +01:00
zhaoxin
b4f957ff3c
mcp: add DisableListening option to StreamableClientTransport (#729)
mcp: add DisableStandaloneSSE option to control standalone SSE stream

Currently, StreamableClientTransport always establishes a standalone SSE
connection after initialization to receive server-initiated messages.
While this is useful for receiving notifications like
ToolListChangedNotification, it causes problems in several real-world
scenarios:

1. **Unnecessary resource usage**: Many use cases (for me), especially
in scientific computing scenarios, only require simple request-response
communication. Clients don't need server-initiated notifications, making
the persistent SSE connection wasteful.

2. **Server compatibility issues**: Some third-party MCP servers don't
properly handle GET requests for SSE streams. When the client
automatically tries to establish the SSE connection, it fails or hangs,
blocking the entire connection process. This is particularly problematic
when using third-party servers that cannot be modified. This issue is
similar to what was discussed in issue #634.

3. **Lack of user control**: The MCP specification states that the
standalone SSE stream is optional ("The client MAY issue an HTTP GET"),
but the current SDK implementation doesn't provide a way to opt out.
This implementation is also inconsistent with other MCP SDKs like
github.com/mark3labs/mcp-go, which provides getListeningEnabled control
(defaults to false, requires explicit enablement).

Fixes #728

Co-authored-by: zhaoxin <zhaoxin@pjlab.org.cn>
2026-01-11 07:56:47 -05:00
Liu Shuang
e66c23fae4
mcp: fix connectStandaloneSSE checking Content-Type header (#736)
Issue encountered: the MCP server request was unexpectedly redirected to
an HTML page. Because the client didn’t validate the response
Content-Type, it still entered handleSSE, which then kept treating the
HTML as an SSE stream and repeatedly re-requesting the same
page—creating an infinite request loop.
2026-01-04 13:16:06 -08:00
Robert Findley
875d1d30c2
mcp: don't break the streamable client connection for transient errors (#723)
When POST requests in the streamableClientConn return a transient error,
return this error to the caller rather than permanently breaking the
connection.

This is achieved by using the special sentinel ErrRejected error to the
jsonrpc2 layer. In doing so, the change revealed a pre-existing bug:
ErrRejected had the same code as ErrConnectionClosing, and
jsonrpc2.WireError implements errors.Is, so the two sentinel values
could be conflated. This is fixed by using a new internal code.

There's more to do for #683: we should also retry transient errors in
handleSSE.

For #683
2025-12-12 13:49:41 -05:00
Robert Findley
bae0929820
mcp: address review comments on streamable cleanup (#716)
Follow up on some post-submit comments from jba.
2025-12-11 15:16:04 -05:00
Robert Findley
b3399e6730
mcp: use args struct for CloseSSEStream (#718)
Change CloseSSEStream from func(time.Duration) to
func(CloseSSEStreamArgs) for forward compatibility, allowing new fields
to be added without breaking existing callers.
2025-12-11 15:13:38 -05:00
Rob Findley
6f6e196c27 mcp: fix stray log.Printf in notifySessions
I encountered these logs, and they were distracting and noisy. We
shouldn't log to stderr unless configured by the user. Thread through
the logger.

Also, use ensureLogger in the streamable client to simplify call sites.

Updates #708
Updates #684
2025-12-10 18:15:54 -05:00
John Howard
b012d0824e client: allow re-using connections in more cases
I am seeing a bug where connections with a Streamable HTTP client are
not reused at all. This makes performance terrible, since each request
establishes a new TCP connection.

I was able to root cause this down to behavior of the `processStream`
function. This sets up a bufio.NewScanner, and waits to get data. Once
we get a message, we abort and *close the connection*.

At this point, there are two paths:
* The bufio reader read the *full* response body to EOF, and then we
  closed. This case is fine; we can re-use the idle connection.
* The bufio reader *did not* real the full response body. It may have
  read the entire actual data that *would be sent*, but since its
streaming happened to not yet be delivered. In this case, we call
`body.Close()` before an EOF, which causes the HTTP client to not
attempt to re-use the connection.

In my case, the server I am using reliable sends the response over 2 TCP
packets (one with the event, and the other with the closure of the
`chunked` encoding (`0`)). This causes the body to never be fully read.

This PR fixes that case by unconditionally reading the full body before
we close the connection. With this change, I am able to reliably re-use
connections for future requests.
2025-12-10 17:06:20 -05:00
Rob Findley
6848e36ef0 mcp: finish cleanup notes in streamable.go
Address remaining TODO(cleanup) comments in streamable.go: eliminate a
pointless context cancellation, and simplify forCall logic.
2025-12-10 16:25:38 -05:00
Rob Findley
e69fd7ba25 mcp: simplify stream state management in streamable transport
Replace closure-based deliver and closeLocked fields with explicit
state:
- pendingJSONMessages: signals JSON mode and collects messages
- w: indicates stream is claimed by HTTP request
- done: signals stream completion (non-nil implies w is non-nil)
- lastIdx, supportsPrimeClose, logger: moved from closures to fields

Add stream methods to consolidate logic:
- close: sends close event and closes done channel
- release: releases stream from HTTP request for resumption
- deliverLocked: writes/stores messages, handles request accounting

Factor out hangResponse to document HTTP response lifecycle.

Inline writeEvent to eliminate confusing pointer-based index
management.
2025-12-10 13:28:25 -05:00
Markus Kusano
dd5a8ff77b mcp: clarify documentation for the streamable transports
After a deep dive with the team, document areas of the codebase
identified as unclear. For now, this is a documentation-only commit (apart
from one switch of jsonrpc2.Request.ID.IsValid->Request.IsCall). Special
TODO(cleanup) comments are added for additional code changes in
follow-up commits.

Also, include high level doc comments for server and client transports,
in new streamable_server.go and streamable_client.go. Eventually, the
server and client transport logic should be migrated to these files, as
streamable.go has gotten too large. However, we can do that at a future
date, to minimize diff.
2025-12-10 10:46:10 -05:00
Rob Findley
b8c21e9fc0 mcp: add lastEventIDHeader for streamable implementation
Use a constant, to be consistent with other headers.
2025-12-10 10:46:10 -05:00
Robert Findley
87c81260e2
mcp: gate prime and close events on protocol version 2025-11-25 (#696)
The prime and close SSE events (SEP-1699) were added in protocol version
2025-11-25. Only send these events when the client negotiates that
version or later.

- In servePOST, extract protocol version from InitializeParams for
initialize requests, otherwise use the Mcp-Protocol-Version header
- In serveGET/acquireStream, read protocol version from header
- Only set closeLocked callback when prime/close is supported
- Add unexported protocolVersion field to ClientSessionOptions for
testing

Fixes #686
2025-12-08 15:24:41 -05:00
Robert Findley
18cd63599d
auth, mcp: add UserID to TokenInfo for session hijacking prevention (#695)
Add a UserID field to auth.TokenInfo that TokenVerifiers can populate
from JWT "sub" claims or token introspection. The streamable HTTP
transport uses this to bind sessions to users, rejecting requests where
the user ID doesn't match the session's original user.

Fixes #589
2025-12-08 14:32:33 -05:00
Robert Findley
272e0cd4f5
mcp: better handling for streamable context cancellation (#677)
After walking through our handling of streamable client context
cancellation (due to encountering a shutdown deadlock), I think I've
settled on a more coherent strategy for handling call cancellation:

- In our call handler, retire the request if the call exits due to
cancellation: the caller will never see the actual result anyway.
- In connectSSE, use the actual request context (the same context used
in Write) for the client request, so that it terminates when the context
is cancelled. Thread through the initialization context for the
standalone SSE request.

Also, a couple minor improvements:
- Use a detached context for the background context of the client
connection. We want to preserve context values (see #513), but it is not
right to cancel the connection after Connect has already returned, if
the context times out.
- Don't use Last-Event-ID != "" as the signal for whether the connectSSE
call is initial: if the standalone SSE stream disconnects without an
event ID, we'll still reconnect it, and don't want to do so without a
delay.

+ tests, updating the streamable client connection test harness to
accomodate the new aspects being exercised.

Fixes #662
2025-12-03 12:26:50 -05:00
Robert Findley
623780125c
mcp: support SSE polling and server disconnects (SEP-1699) (#663)
This CL implements support for SEP-1699, adding the following new API:

- Event.Retry corresponds to the 'retry' field of a server sent event.
This is used to signal client retry behavior.
- RequestExtra.CloseStream is set by the streamable transport to allow
closing a stream.

The streamable server transport is updated to set CloseStream for calls,
allowing server operations or middleware to close the underlying stream.
If the configured 'reconnectAfter' delay is set, a 'retry' message is
sent prior to terminating the stream.

The streamable client transport is updated to read the configured delay.

Fixes #630
2025-12-01 16:21:37 -05:00
Raphael MANSUY
9ef2b2742e
Fix: Skip non-message SSE events in processStream (#637)
Fixes #636

## Problem
The StreamableClientTransport tries to JSON-decode ALL SSE events,
including ping events sent by servers for keep-alive purposes. This
causes errors like 'invalid character 'p' looking for beginning of
value' when encountering non-JSON event data.

## Solution
Modified processStream to check evt.Name and skip events that are not
'message' events (or unnamed events, which default to 'message' per SSE
spec).

## Changes
- Added event name check in processStream before attempting JSON decode
- Added test case for ping event filtering
- Added test for scanning multiple events including ping events

## Testing
- All existing tests pass
- New tests verify ping events are properly filtered
- Tested with DeepWiki MCP server which sends ping events
2025-11-13 20:48:23 -05:00
Xie Zhihao
d297272705
mcp: relax SSE connection response handling in non-strict mode (#611)
Some MCP servers do not support SSE, but when receiving a request, they
return 400 Bad Request or 404 Not Found instead of the standard 405
Method Not Allowed. The TypeScript and Python SDK ignore this error,
while the Go SDK terminates the session. This patch addresses the issue
in the non-strict mode.

Fixes #610

---------

Signed-off-by: Xie Zhihao <zhihao.xie@bytedance.com>
2025-11-03 10:12:28 -05:00
Robert Findley
d256a9c109
mcp: don't persist failed streamable sessions (#605)
If a stateful streamable session fails to initialize, it is unusable.
Avoid allocating resources by closing the session immediately.

Also:
- Fix a bug where InitializeParams is not guarded with its mutex.
- Fix a resource leak where sessions are persisted with session id "",
even though they are unaddressable.
- Add a relevant benchmark, and update tests.

Fixes #578
2025-10-27 16:29:16 -04:00
Robert Findley
ae6bda692b
mcp: establish the streamable client standalone SSE stream in Connect (#604)
When Connect returns, client should be guaranteed that the streamable
SSE stream is connected.

Fixes #583
2025-10-27 14:03:22 -04:00