Commit graph go-sdk/mcp
Author SHA1 Message Date
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
Yamamoto, Hirotaka
3dd262606e
mcp: add Extensions field to capabilities per SEP-2133 (#794)
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)
2026-02-17 16:10:07 +01:00
Maciek Kisiel
40475813dd mcp: fix multi-select enum elicitation 2026-02-17 14:42:18 +01:00
Robert Findley
3cf9f9908d
mcp: implement sampling with tools (#699)
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>
2026-02-17 12:07:08 +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
original-owl
86f8415122
mcp: use synctest for timing-dependent tests (#756)
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>
2026-02-10 10:40:11 +01:00
Suraj Bobade
41b54dced5
mcp: use omitzero json tag for byte array field in ResourceContents, instead of omitempty
/mcp/content.go: use omitzero json tag for byte array, instead of omitempty earlier.

This change removes requirement of custom MarshalJSON

Fixes #781
2026-02-09 10:41:27 +01:00
Maciek Kisiel
6a19c95fea mcp: finalize cleanup of Go 1.23, make Go version support explicit 2026-02-06 12:50:01 +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
awschmeder
c6e7337e7e fix: add JSON tags to ElicitationCapabilities fields
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
2026-02-03 16:27:48 +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
FanOne
b29ab52878
mcp: validation only for accept action (#766)
Fix: #758

- validate schema only for accept action
- add unit test
2026-01-28 17:06:06 +01:00
Maciek Kisiel
6b75899fd7 mcp: fix a race condition in logging
Reading the buffer outside of a critical section can lead to data corruption.
2026-01-25 19:48:35 +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
Sam Morrow
c2c7edc3f0
perf: add schema caching to avoid repeated reflection (#685)
## Summary

This PR adds schema caching that dramatically reduces the cost of tool
registration in stateless server patterns.

**Contributors:** Thanks to @omgitsads and @almaleksia for collaborating
on and researching this solution!

## Problem

In stateless deployments like `github/github-mcp-server`, a new
`*mcp.Server` is created for each incoming request. This means `AddTool`
is called repeatedly for the same tools, causing:

1. **For typed handlers**: `jsonschema.ForType()` reflection is called
every time
2. **For pre-defined schemas**: `schema.Resolve()` is called every time

## Solution

Add a `schemaCache` that stores:
- Schemas by `reflect.Type` (for auto-generated schemas from typed
handlers)
- Resolved schemas by schema pointer (for pre-defined schemas)

The cache is:
- Concurrent-safe using `sync.Map`
- Unbounded (typical MCP servers have &lt;100 tools)
- Global (lives across server instances)

## Benchmark Results

```
BenchmarkAddToolTypedHandler-22          977080    1223 ns/op     1208 B/op    21 allocs/op
BenchmarkAddToolTypedHandlerNoCache-22    6764  161463 ns/op    39262 B/op  1072 allocs/op
```

| Metric | With Cache | Without Cache | Improvement |
|--------|------------|---------------|-------------|
| Time | 1,223 ns/op | 161,463 ns/op | **132x faster** |
| Allocations | 21 allocs | 1,072 allocs | **51x fewer** |
| Memory | 1,208 B/op | 39,262 B/op | **32x less** |

## Files Changed

- `mcp/schema_cache.go` - New cache implementation
- `mcp/server.go` - Modified `setSchema` to use cache
- `mcp/schema_cache_test.go` - Unit tests for caching behavior
- `mcp/schema_cache_benchmark_test.go` - Benchmarks

## Impact for Integrators

**Automatic** - no code changes required. Integrators using:
- Typed handlers (`AddTool[In, Out]`) → cache by type
- Pre-defined schemas (`Tool{InputSchema: schema}`) → cache by pointer

Both patterns benefit from caching after the first call.

---

## Real-World Performance Validation

The following benchmarks were conducted using `github/github-mcp-server`
(a production MCP server with ~130 tools) to validate the performance
impact in a real-world scenario.

### Test Environment
- **Server:** github-mcp-server (stateless HTTP deployment)
- **Tools registered per request:** ~130 tools
- **Test methodology:** Python benchmark client, 30 iterations
(latency), 100 iterations (stress)
- **Date:** December 4, 2025

### Configurations Tested

| Configuration | Description |
|--------------|-------------|
| **main (mcp-go)** | Original implementation using mcp-go library |
| **go-sdk (no cache)** | go-sdk WITHOUT schema caching (broken) |
| **go-sdk (with cache)** | go-sdk WITH this PR&#39;s schema caching fix
|

### Latency Test Results (n=30)

#### Operation: `initialize`

| Configuration | P50 | P99 | Status |
|--------------|-----|-----|--------|
| main (mcp-go) | 11.48ms | 14.00ms | ✅ Baseline |
| go-sdk (no cache) | 20.47ms | 25.30ms | 🔴 **+78% REGRESSION** |
| go-sdk (with cache) | 10.36ms | 14.33ms | ✅ **FIXED (-10%)** |

#### Operation: `tools/list`

| Configuration | P50 | P99 | Status |
|--------------|-----|-----|--------|
| main (mcp-go) | 13.46ms | 22.47ms | ✅ Baseline |
| go-sdk (no cache) | 22.98ms | 29.11ms | 🔴 **+71% REGRESSION** |
| go-sdk (with cache) | 14.15ms | 15.91ms | ✅ **FIXED (+5%)** |

#### Operation: `prompts/list`

| Configuration | P50 | P99 | Status |
|--------------|-----|-----|--------|
| main (mcp-go) | 11.56ms | 13.81ms | ✅ Baseline |
| go-sdk (no cache) | 21.00ms | 26.42ms | 🔴 **+82% REGRESSION** |
| go-sdk (with cache) | 10.77ms | 15.25ms | ✅ **FIXED (-7%)** |

### Stress Test Results (n=100)

| Configuration | P50 (initialize) | P50 (tools/list) | P50
(prompts/list) |

|--------------|------------------|------------------|-------------------|
| main (mcp-go) | 12.06ms | 14.30ms | 11.34ms |
| go-sdk (no cache) | 20.06ms | 23.44ms | 19.59ms |
| go-sdk (with cache) | **11.83ms** | **14.58ms** | **11.05ms** |

### Memory/Allocation Comparison (from pprof)

| Configuration | Total Allocations | Comparison |
|--------------|-------------------|------------|
| main (mcp-go) | 355.91 MB | Baseline |
| go-sdk (no cache) | **1208.70 MB** | 🔴 **3.4x MORE allocations** |

#### Top Allocation Sources - go-sdk WITHOUT cache (broken)

| Function | Size | % | Issue |
|----------|------|---|-------|
| `jsonschema.UnmarshalJSON` | 324.60 MB | 27% | 🚨 Schema re-parsing |
| `encoding/json.Unmarshal` | 341.10 MB | 28% | JSON deserialization |
| `jsonschema.resolve` | 219.51 MB | 18% | 🚨 Schema re-resolution |
| `jsonschema.MarshalJSON` | 92.52 MB | 8% | Schema JSON encoding |

### Root Cause Analysis

The `google/jsonschema-go` library regenerates JSON schemas on every
request instead of caching them. In a server with ~130 tools, this
causes:
- **70-80% latency regression** on all MCP operations
- **3.4x more memory allocations** per request
- **~70% of all allocations** are schema-related operations

### Key Findings

1. ✅ **REGRESSION CONFIRMED:** go-sdk without schema caching is 70-80%
slower than mcp-go
2. ✅ **FIX VERIFIED:** Schema caching restores performance to baseline
(or better)
3. ✅ **MEMORY IMPACT:** 3.4x reduction in allocations with caching
4. ✅ **PRODUCTION READY:** Fixed version performs equivalently to mcp-go
baseline

---------

Co-authored-by: Adam Holt <4619+omgitsads@users.noreply.github.com>
Co-authored-by: Ksenia Bobrova <1885174+almaleksia@users.noreply.github.com>
2026-01-20 20:16:46 -05:00
Jonathan Amsterdam
13488f7da1
Export GetError and SetError methods (#753)
Exported GetError and SetError. 

Fixes #64

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 11:46:13 -05: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
FanOne
a225d4dc7d
delete the old logger in client (#744)
Fix #708   
Link PR #738

deleted the old logger in client.
2026-01-05 12:29:40 -05:00
FanOne
61be548803
feat: deprecated logger in client & add Logger in ClientOption (#738)
Fix #708

1. deprecated logger in client
2. add Logger in ClientOption
3. add unit test for Logger
2026-01-05 07:41:31 -08: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
Jonathan Amsterdam
9c4d349ec0
go.mod: upgrade to jsonschema v0.4.2 (#732)
Visible changes:

- Inference on structs populates PropertyOrder

- An empty map for Schema.Properties marshals as "{}"
2026-01-04 09:18:01 -05:00
ZAIN ULHASSAN
27b3354f80
mcp: fix SSEClientTransport to report HTTP errors properly (#740)
Fix SSEClientTransport.Connect to check HTTP status code before attempting to parse SSE events.

Fixes #714
2025-12-31 08:16:20 -08:00
Robert Findley
411d5a049a
mcp: switch icon theme to named string type (#733)
To be consistent with logging level, use a named string type for icon
themes. This improves readability / discoverability, and is more
consistent.

Updates #552
For #725
2025-12-19 16:44:10 -05: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
3430e2242e
mcp: add Capabilities fields to ServerOptions and ClientOptions (#713)
This allows users to configure default capabilities, disable defaults,
or suppress listChanged notifications by setting ListChanged: false.

Key changes:
- Add ServerOptions.Capabilities and ClientOptions.Capabilities fields.
- Merge user-provided capabilities with dynamically-added ones (tools,
  prompts, resources, sampling, elicitation).
- Check ListChanged before sending list-changed notifications.
- Deprecate HasPrompts, HasResources, HasTools on ServerOptions.
- Remove unreleased ElicitationModes field from ClientOptions.
- Add clone methods to capability structs using shallowClone helper.
- Add synctest-based tests for notification behavior (go1.25).
- Add TestClientCapabilitiesOverWire and TestServerCapabilitiesOverWire
- Document the new Capabilities API in docs/

Updates #706
2025-12-11 20:17:46 -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
Robert Findley
1a964ae23d
mcp: debounce server change notifications (#717)
Debounce and batch notifications for changes in server feature sets
(adding or removing tools, prompts, resources, or resource templates).

This PR handles server-side change notifications only. We could do the
same for client-side notifications (e.g. roots changed), but at the
moment we don't need to.

Fixes #649.
2025-12-11 15:11:32 -05:00
Robert Findley
a5e369ed12
mcp: fix broken client root capabilities (#698)
To address #607, add ClientCapabilities.RootsV2, and populate it when
constructing sending and receiving InitializeParams.

Fixes #607
2025-12-11 10:16:56 -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
Markus Kusano
5928940d5f
mcp/mcp_test: tests for elicit content validation (#676)
This adds a simple schema and checks that the content returned by an
elicitation handler is validated against the schema. Previously, we had
no test coverage checking if the content was validated (i.e., we would
see no test failures if we removed content validation against the
schema).

The bulk of the actual validation logic is handled by the jsonschema
library so adding comprehensive tests covering different schema-features
is redundant, we simply need to see that some validation is happening.

See https://github.com/modelcontextprotocol/go-sdk/issues/625 for more
details on the motivation of this change.

Fixes #625

---------

Co-authored-by: Markus Kusano <kusano@google.com>
2025-12-03 10:12:07 -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
isfzhang
1bec3c60a0
fix: handle Windows CRLF in MCP client(#664) (#665)
This change updates the MCP client stdio JSON decoder to correctly
handle
both Unix (`\n`) and Windows (`\r\n`) line endings. 

Fixes #664

Co-authored-by: qysz <i_zf@qq.com>
2025-12-01 12:16:39 -05:00
Robert Findley
2ce44d45e9
all: expose JSON-RPC error codes, and support URLElicitationRequired errors (#655)
Expose the jsonrpc.Error type to allow access to underlying JSON-RPC
error codes. Also, expose common JSON-RPC error codes in the jsonrpc
package, and MCP error codes in the mcp package.

Also, implement support for the MCP URL elicitation required error
(-32042), enabling servers to request out-of-band user authorization and
clients to automatically handle these requests with retry logic.

Server-side:
- Add CodeURLElicitationRequired constant
- Add URLElicitationRequiredError() constructor.

Client-side:
- Add urlElicitationMiddleware to intercept URLElicitationRequired
  errors and retry calls.
- Update callElicitationCompleteHandler to signal waiting operations.
- Fix capability advertisement to support both form and URL modes.

Fixes #452
Fixes #623
2025-12-01 11:29:17 -05:00
Robert Findley
21fb03dd1a
mcp: support url mode elicitation (#646)
Add initial support for URL mode elicitation, by introducing the new
types, as well as both server and client-side validation.

A follow-up CL will add support for ErrURLElicitationRequired.

For #623
2025-11-23 12:58:28 -05:00
Markus Kusano
d9263f7e47
mcp: support elicitation defaults (#644)
Fixes #617

---------

Co-authored-by: Markus Kusano <kusano@google.com>
2025-11-21 16:15:21 -05:00
Robert Findley
1943780255
mcp: validate tool names (#640)
Implement SEP 986, enforcing restrictions on tool names. Since we're
already at v1 we couldn't turn this into a panic, so use an error log
instead. This is consistent with the typescript SDK. A note is left in
our 'rough edges' doc to panic in v2.

Fixes #621
2025-11-19 17:01:50 -05:00