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>
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>
The Form and URL fields in ElicitationCapabilities were missing JSON
struct tags, causing them to serialize as "Form" and "URL" (capitalized)
instead of the lowercase "form" and "url" required by the MCP protocol.
This caused initialization failures when clients advertised elicitation
capabilities, as servers would not recognize the incorrectly-cased keys.
Discovered via downstream issue: https://github.com/docker/cagent/issues/1574
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
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>
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>
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>