## Summary
- add the optional `description` field to `mcp.Implementation`
- keep the field omitted when empty
- cover JSON marshal/unmarshal behavior with a focused regression test
Fixes#977.
## To verify
- `go test ./mcp -run TestImplementationDescriptionJSON -count=1`
- `go test ./mcp -run TestServerRequest_PerRequestAccessors -count=1`
- `go test ./mcp -count=1`
- `go test ./...`
- `git diff --check`
Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
mcp: add configurable keepalive failure threshold
Introduce `KeepAliveFailureThreshold` option in both `ClientOptions` and
`ServerOptions` to control how many consecutive keepalive ping failures
are tolerated before closing the session.
This aligns with the MCP spec's guidance that "multiple failed pings MAY
trigger a connection reset," allowing operators to tune resilience
against transient network hiccups without immediately tearing down
otherwise healthy sessions.
A threshold of 0 or 1 (the default) closes on the first failure,
preserving existing behavior. Higher values let isolated misses pass
while still closing the session once consecutive failures reach the
threshold. A successful ping resets the counter.
Tolerated failures are logged at WARN level; the final failure that
closes the session is logged at ERROR level.
This is rework of #979.
### Context
Design (`design/mrtr.md`) and implementation proposal for Multi
Round-Trip Requests (MRTR) per
[SEP-2322](de6d76fba3/seps/2322-MRTR.md).
### Changes
- Introduce `InputRequest` and `InputResponse` sealed interfaces with
corresponding `InputRequestMap` and `InputResponseMap` types for custom
map JSON codec implementation.
- Extend `CallToolParams`, `GetPromptParams`, and `ReadResourceParams`
with `InputResponses`, `RequestState` and an unexported `resultType`
("complete" or "input_required") for retry round-trips. The type is set
by the SDK based on input requests presence.
- Add client-side MRTR middleware enabled by default that automatically
fulfills input requests and retries.
- Added MRTROptions on ClientOptions to allow disabling the middleware
and configuring the number of retries.
- Add server-side MRTR middleware for backward compatibility:
transparently bridges input requests to direct
Elicit/CreateMessage/ListRoots calls for older clients
## 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
`ReadOnlyHint` is a `bool` with omitempty, so the zero value (false) is
indistinguishable from unset, and **drops out** of marshaled JSON.
The current behavior causes an issue with the OpenAI MCP app submission
because they require explicit hints.
Consumers that explicitly set `ReadOnlyHint: false` on write tools lose
the field on the wire. Removing `omitempty` ensures `false` is always
serialized, which matches the MCP spec default.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
Co-authored-by: guglielmoc <guglielmoc@google.com>
## Summary
- reject a second `initialize` request on an already initialized server
session
- keep the original `ServerSession.InitializeParams()` instead of
replacing it with later client parameters
- add a raw JSON-RPC regression test for the duplicate-initialize path
Fixes#961.
## To verify
- `go test ./mcp -run TestServerRejectsDuplicateInitialize -count=1`
- `go test ./mcp -count=1`
- `go test ./...`
---------
Co-authored-by: guglielmoc <guglielmoc@google.com>
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>
Replaces the simple time-interval check in `LoggingHandler` with a
standard `rate.Limiter` from `golang.org/x/time/rate`, as suggested by
the existing TODO comment.
This provides a well-tested implementation and removes the need for
manual `lastMessageSent` tracking. The rate (`1/MinInterval`) and burst
(1) match the previous behavior exactly.
/cc @jba
Closes#924
---------
Signed-off-by: wucm667 <stevenwucongmin@gmail.com>
Co-authored-by: Jonathan Amsterdam <jba@users.noreply.github.com>
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
## Bug
`mcp.AddTool` crashes the server with SIGSEGV when input/output struct
tags trigger a nil schema.
## Root Cause
In `mcp/server.go`, the function `setSchema` calls
`internalSchema.Resolve(...)` without checking if `internalSchema` is
nil. If the schema inference (e.g., via `jsonschema.ForType`) or a
provided schema results in a nil value, this dereference causes a panic.
## Fix
Added nil checks for `internalSchema` before calling `Resolve` in two
locations within `setSchema`:
1. After schema generation via `jsonschema.ForType`.
2. After processing a provided schema (which might be nil or fail to
unmarshal).
If `internalSchema` is nil, the function now returns a descriptive error
instead of crashing.
## Verification
- `go test ./mcp/...` passes.
- The fix prevents the crash by handling the nil case gracefully.
Closes#916.
---------
Signed-off-by: wucm667 <stevenwucongmin@gmail.com>
Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
Change the error code returned by ResourceNotFoundError from -32002 to
-32602 (jsonrpc.CodeInvalidParams), aligning with
[SEP-2164](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164).
* Not gated with a protocol version check, because the previous error
code was never formally specified and the behaviour varied across SDKs.
* In case clients which rely on the old error code exist, the previous
behaviour can be restored with `MCPGODEBUG=customresnotfounderrcode=1`.
* `CodeResourceNotFound` deprecated since its only purpose going forward
is backward compatibility. The new code should use
`jsonrpc.CodeInvalidParams` directly.
* `CodeResourceNotFound` changed to `var` so it can stay in sync with
`ResourceNotFoundError` and existing code doing `code ==
mcp.CodeResourceNotFound` keeps working in both the default and compat
modes.
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>
## 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
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>
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>
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>
Since ping is optional per the MCP spec, a peer that doesn't implement
it will respond with a -32601 (method not found) error. Previously,
startKeepalive treated all ping errors identically, closing the session
even when the peer was perfectly healthy.
Now, a method-not-found response stops the keepalive goroutine without
closing the session.
Fixes#897
## Summary
Two error sites in the MCP runtime were previously unobservable:
- **`mcp/shared.go` `startKeepalive`** — when a keepalive ping failed,
the session was closed silently with no indication of why.
- **`mcp/transport.go` `connect`** — `jsonrpc2.OnInternalError` printed
via the package-level `log.Printf`, bypassing the `*slog.Logger`
configured on `ServerOptions`/`ClientOptions`.
Both sites now report through the existing `*slog.Logger` that `Server`
and `Client` already guarantee non-nil via `ensureLogger`. The logger is
threaded into the unexported `startKeepalive` and `connect` helpers as a
parameter — **no new public API surface**.
## Context
Per @jba's [suggestion on
#865](https://github.com/modelcontextprotocol/go-sdk/pull/865#issuecomment-4156166474):
> Maybe we can just use the logger in all the places we currently drop
the error. Then it's still reported, but we don't have to add any API.
This PR is that smaller alternative. It addresses the concrete
observability gap in #218 without expanding `ServerOptions`. PR #865
(the `ErrorHandler` callback proposal) remains open pending community
input on whether a structured callback API is also wanted on top of
logging.
`notifySessions` already logs at `Warn` level on delivery errors and is
intentionally left untouched.
## Test plan
- [x] New regression test `TestKeepAliveFailure_Logged` asserts the
keepalive failure produces a log line on the configured logger.
- [x] Verified the test fails when the new `logger.Error(...)` call is
removed (sanity check that it pins the behavior).
- [x] `go test ./mcp/... -count=1` passes.
- [x] `go vet ./...` clean.
- [x] `go build ./...` clean.
Refs #218
---------
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
This PR fixes a race condition where `ServerSession.keepaliveCancel` was
accessed in `Close` concurrently with its initialization in
`ServerSession.startKeepalive`. It also removes a now redundant test.
The fix moves keepalive initialization into `ServerSession.Connect`
similar to the implementation in `ClientSession.Connect`. This is safe
because the MCP spec allows keepalive pings before the initialization
messages. From the MCP
[spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle)
(emphasis added):
> The client SHOULD NOT send requests **other than pings** before the
server has responded to the initialize request.
The PR includes a reproducer test case, which fails with the following
error without this fix:
```
> go test -count=10 -race -run ^TestStreamableStatelessKeepaliveRace$ github.com/modelcontextprotocol/go-sdk/mcp 8s
==================
WARNING: DATA RACE
Write at 0x00c00007fbf0 by goroutine 1095:
github.com/modelcontextprotocol/go-sdk/mcp.startKeepalive()
/Users/benjamin/Documents/go-sdk/mcp/shared.go:590 +0x58
github.com/modelcontextprotocol/go-sdk/mcp.(*ServerSession).startKeepalive()
/Users/benjamin/Documents/go-sdk/mcp/server.go:1526 +0x130
github.com/modelcontextprotocol/go-sdk/mcp.(*ServerSession).initialized()
/Users/benjamin/Documents/go-sdk/mcp/server.go:1059 +0xf8
github.com/modelcontextprotocol/go-sdk/mcp.init.serverSessionMethod[go.shape.*uint8,go.shape.interface { GetMeta() map[string]interface {}; SetMeta(map[string]interface {}); github.com/modelcontextprotocol/go-sdk/mcp.isResult() }].func27()
/Users/benjamin/Documents/go-sdk/mcp/shared.go:332 +0x98
github.com/modelcontextprotocol/go-sdk/mcp.newServerMethodInfo[go.shape.*github.com/modelcontextprotocol/go-sdk/mcp.InitializedParams,go.shape.interface { GetMeta() map[string]interface {}; SetMeta(map[string]interface {}); github.com/modelcontextprotocol/go-sdk/mcp.isResult() },go.shape.struct { github.com/modelcontextprotocol/go-sdk/mcp.Meta "json:\"_meta,omitempty\"" }].func2()
/Users/benjamin/Documents/go-sdk/mcp/shared.go:272 +0x84
github.com/modelcontextprotocol/go-sdk/mcp.defaultReceivingMethodHandler[go.shape.*uint8]()
/Users/benjamin/Documents/go-sdk/mcp/shared.go:152 +0xe8
github.com/modelcontextprotocol/go-sdk/mcp.defaultReceivingMethodHandler[*github.com/modelcontextprotocol/go-sdk/mcp.ServerSession]()
/Users/benjamin/Documents/go-sdk/mcp/shared.go:146 +0x60
github.com/modelcontextprotocol/go-sdk/mcp.handleReceive[go.shape.*uint8]()
/Users/benjamin/Documents/go-sdk/mcp/shared.go:169 +0x228
github.com/modelcontextprotocol/go-sdk/mcp.(*ServerSession).handle()
/Users/benjamin/Documents/go-sdk/mcp/server.go:1445 +0x370
github.com/modelcontextprotocol/go-sdk/mcp.connect[go.shape.*uint8,go.shape.*uint8].func1.1()
/Users/benjamin/Documents/go-sdk/mcp/transport.go:170 +0x5c
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.HandlerFunc.Handle()
/Users/benjamin/Documents/go-sdk/internal/jsonrpc2/jsonrpc2.go:84 +0x48
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.(*Connection).handleAsync.func3()
/Users/benjamin/Documents/go-sdk/internal/jsonrpc2/conn.go:717 +0xd8
Previous read at 0x00c00007fbf0 by goroutine 1089:
github.com/modelcontextprotocol/go-sdk/mcp.(*ServerSession).Close()
/Users/benjamin/Documents/go-sdk/mcp/server.go:1502 +0x34
github.com/modelcontextprotocol/go-sdk/mcp.(*StreamableHTTPHandler).ServeHTTP.deferwrap1()
/Users/benjamin/Documents/go-sdk/mcp/streamable.go:519 +0x34
runtime.deferreturn()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/runtime/panic.go:589 +0x5c
github.com/modelcontextprotocol/go-sdk/mcp.TestStreamableStatelessKeepaliveRace.mustNotPanic.func2()
/Users/benjamin/Documents/go-sdk/mcp/streamable_test.go:2156 +0x94
net/http.HandlerFunc.ServeHTTP()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/net/http/server.go:2322 +0x48
net/http.serverHandler.ServeHTTP()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/net/http/server.go:3340 +0x270
net/http.(*conn).serve()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/net/http/server.go:2109 +0x9b0
net/http.(*Server).Serve.gowrap3()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/net/http/server.go:3493 +0x48
Goroutine 1095 (running) created at:
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.(*Connection).handleAsync()
/Users/benjamin/Documents/go-sdk/internal/jsonrpc2/conn.go:715 +0x334
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.(*Connection).acceptRequest.func2.gowrap1()
/Users/benjamin/Documents/go-sdk/internal/jsonrpc2/conn.go:676 +0x34
Goroutine 1089 (running) created at:
net/http.(*Server).Serve()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/net/http/server.go:3493 +0x5dc
net/http/httptest.(*Server).goServe.func1()
/opt/homebrew/Cellar/go/1.25.4/libexec/src/net/http/httptest/server.go:311 +0x98
==================
--- FAIL: TestStreamableStatelessKeepaliveRace (0.14s)
testing.go:1617: race detected during execution of test
FAIL
FAIL github.com/modelcontextprotocol/go-sdk/mcp 1.727s
FAIL
```
---------
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
SetError now only populates the Content field when it has not already
been populated. This allows callers to provide a user-friendly message
while still recording the underlying error for middleware inspection
via GetError.
The len check (rather than nil check) ensures both nil and empty-slice
Content are treated as "not yet populated."
Fixes#858
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.
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.
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>
This implements [SEP-1303](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1303).
Input validation errors (missing required fields, wrong types) during
tool calls
are now returned as `CallToolResult` with `IsError=true` instead of
JSON-RPC
`-32602` errors. This allows LLMs to see the validation error and
self-correct,
per the MCP spec.
The existing pattern for handler-returned errors (lines 347-350 in
server.go)
already converts regular errors to tool results with `IsError=true`.
This change
applies the same pattern to argument schema validation and unmarshal
errors,
resolving the `TODO(#450)` in the code.
Thanks to @iwyrkore for filing the issue and proposing the approach, and
@findleyr for the cross-SDK analysis in the discussion.
Fixes#450
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
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`.
When `SetProgressToken` is called on a `Params` struct with a nil `Meta`
field, the newly created map is never assigned back to the struct via
`SetMeta`, causing the progress token to be silently lost.
Call `p.SetMeta(m)` after creating the new map so the struct retains the
reference.
Fixes#845
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
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.
This is done to prevent concurrent reads and writes, which violate Go's
memory model. The concurrent write may be triggered e.g. by setting the
log level on
the client side.
Fixes#828
Update the SDK to use protocol version 2025-11-25 as the latest
supported version. Fix tests and examples that had hardcoded
expectations for the previous version (2025-06-18).
---------
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
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.
## 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>
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`.
This implements
[SEP-2133](https://modelcontextprotocol.io/community/seps/2133-extensions)
and resolves#777.
## Summary of changes
- Add `Extensions` field to `ServerCapabilities` and
`ClientCapabilities` structs per
[SEP-2133](https://modelcontextprotocol.io/community/seps/2133-extensions)
- Add `AddExtension` helper methods on both types that normalize nil
settings to empty maps.
- [The spec requires an object, not
null](https://modelcontextprotocol.io/community/seps/2133-extensions#negotiation:~:text=an%20empty%20object%20indicates%20no%20settings.)
- Include shallow-copy support via `maps.Clone` in the existing
`clone()` methods
- also include shallow-copy support for `Experimental` field.
- Add comprehensive tests: unit tests for nil-safety, capability
preservation, and over-the-wire round-trip
## What's tested
- [x] `go test ./mcp/...` passes
- [x] Server conformance tests pass (30 scenarios, 40 assertions, 0
failures)
- [x] Client conformance tests match existing baseline (auth scenarios
are known failures)
Summary
Add support for tool use within sampling requests, as described in the
MCP 2025-11-25 spec's sampling.tools capability.
New types
Content types:
- ToolUseContent — represents a tool invocation request from the
assistant
- ToolResultContent — represents the result of a tool invocation, with
nested []Content for the result blocks
Capability types:
- SamplingCapabilities gains Tools and Context sub-fields
- SamplingToolsCapabilities, SamplingContextCapabilities
Tool-enabled sampling (parallel tool calls):
- CreateMessageWithToolsParams with SamplingMessageV2 (supports array
content)
- CreateMessageWithToolsResult (supports array content)
- ToolChoice — controls tool invocation mode (auto, required, none)
- ServerSession.CreateMessageWithTools
- ClientOptions.CreateMessageWithToolsHandler
Design
Following the TypeScript SDK's pattern, tool-enabled sampling uses
separate types from basic sampling to avoid breaking the existing API.
The basic CreateMessage/CreateMessageResult path
is unchanged.
Both paths share the same wire method (sampling/createMessage) and go
through the method info table. The table uses the broader
CreateMessageWithToolsResult type internally;
CreateMessage downconverts (erroring if multiple content blocks are
returned). This is documented as a rough edge to unify in v2.
Setting CreateMessageWithToolsHandler automatically infers the
sampling.tools capability. It is a panic to set both
CreateMessageHandler and CreateMessageWithToolsHandler.
References
-
https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
- TypeScript SDK: CreateMessageResultWithToolsSchema /
CreateMessageRequestParamsWithTools
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## 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>
Converted timing-based tests to use Go 1.25's synctest for instant,
deterministic execution with simulated time.
Moved converted tests to *_go125_test.go files. Tests using real I/O
(exec.Command, httptest) remain timing-based as they require actual
system operations.
Fixes#680
---------
Co-authored-by: i586147 <lakshana.goswami@sap.com>