Commit Graph

76 Commits

Author SHA1 Message Date
Max Isbey c53aefd293 Close cancelled HTTP exchanges and harden auth validation
Review-feedback round on the conformance burn-down:

- Cancelled requests no longer leave the legacy streamable-HTTP POST
  hanging. The dispatcher emits a RequestSettled marker when a handler is
  cancelled without producing a response; the transport consumes it by
  closing the per-request stream, so the POST's SSE stream terminates
  without a response frame and JSON-response mode completes with 204 No
  Content (the client treats 202/204 alike). Per-request streams are
  released instead of leaking until session teardown, and a handler that
  survives the cancellation still delivers its normal response. The
  marker is type-visible on the dispatcher write stream and is stripped
  by every serializing transport, so it can never appear on a wire.
- A bearer token whose audience cannot be canonicalized (out-of-range or
  non-numeric port) is now rejected with the standard 401 invalid_token
  instead of raising through the auth middleware as a 500.
- The bundled authorization server's /register now accepts only https
  redirect URIs or http on a loopback host; other schemes on loopback
  hosts (ftp, ws, javascript, custom) are rejected.
- OAuth client scope selection falls back to the caller-configured
  OAuthClientMetadata.scope when neither the WWW-Authenticate challenge
  nor protected-resource metadata names scopes, matching the TypeScript
  SDK, so the documented migration path works as written.
- The cross-dispatcher contract that handler-raised MCPError subclasses
  surface to callers as plain MCPError is now pinned by an explicit test
  and documented; rehydrate with from_error when the subclass matters.
- Docs: migration notes for the bearer-challenge wire-shape changes and
  the cancellation wire spellings; story READMEs updated to the landed
  error contract; strict-capabilities doc corrected to state that
  resources/unsubscribe is gated by the base resources capability only.
2026-06-28 11:30:26 +00:00
Max Isbey 3605ec09b4 Type the elicitation requested schema on the send side
ElicitRequestedSchema was a TypeAlias for dict[str, Any]; it is now a
Pydantic model of the spec's restricted requested-schema subset, backed
by a new PrimitiveSchemaDefinition union (StringSchema, NumberSchema,
BooleanSchema, and the enum schemas). ServerSession.elicit_form (and the
deprecated elicit alias) and ClientPeer.elicit_form accept only this
model, so a nested-object property, an array-of-objects property, or an
anyOf union is unconstructible at the only place a server author
supplies a schema, rather than silently forwarded to the client.

The spec restricts form-mode requested schemas to flat objects with
primitive-typed properties only ("complex nested structures, arrays of
objects ... are intentionally not supported"). The high-level
Context.elicit / elicit_with_validation path is unchanged in behaviour:
it converts the rendered JSON Schema into the typed model, keeping its
existing per-field TypeError contract and producing value-identical wire
output.

Inbound is deliberately untouched. The wire field
ElicitRequestFormParams.requested_schema stays a plain dict[str, Any],
so older servers that emit anyOf for Optional form fields still reach
the client's elicitation callback. The typed-model-to-wire-dict
conversion lives in one place, ElicitRequestedSchema.to_wire(), which
both send sites call.

The schema family is extra="allow": keys schema.ts does not name (a
top-level title, pattern, exclusiveMinimum, json_schema_extra keys)
still round-trip, because the primitives-only restriction is carried by
the union members' required type literals, not by extra-key rejection.
2026-06-27 18:55:17 +00:00
Max Isbey eddfa29d9d Deprecate ServerSession.send_progress_notification
`ServerSession.send_progress_notification` takes an explicit progress token
decoupled from the request it belongs to, so it can keep emitting progress
for a request that has already completed -- which the spec forbids
("Progress notifications MUST stop after completion"). The request-scoped
`report_progress` (and `Context.report_progress`) is the supported path: it
reports against the inbound request's own token, no-ops when the caller did
not ask for progress, and stops when the request completes. The deprecated
method keeps working and emits `MCPDeprecationWarning`.

The warning message deliberately departs from the "<X> is deprecated as of
<version>" pattern used by the spec-driven deprecations: this one is an SDK
API decision, not a spec retirement (2026-07-28 does not retire
server-to-client progress).

For "stops when the request completes" to hold on every dispatcher,
`_DirectDispatchContext` now closes with its request the way
`_JSONRPCDispatchContext` already did: `close()` runs in the dispatch
handler's `finally`, after which `progress`/`notify` deliver nothing,
`can_send_request` is False, and `send_raw_request` raises
`NoBackChannelError` -- the closed state the `DispatchContext` protocol
documents. Two pre-existing tests that asserted `can_send_request` on a
context captured after its handler returned now sample it in-handler, and
the closed-state contract tests are parametrized over both dispatchers.

The interaction test that covered both the server and client side of late
progress is split in two. The server-side property is proved positively on
the wire through `report_progress`; it no longer relies on a session-bound
standalone stream, so it also runs on the stateless streamable-http arm.
The client-side late-drop test keeps using the deprecated explicit-token
method -- the only API that can still produce a late notification -- under
`pytest.warns`, and its arms are unchanged. The migration guide stops
recommending the deprecated method anywhere and documents the replacement.
2026-06-27 18:53:14 +00:00
Max Isbey 28f500c71a Add an opt-in strict_capabilities flag to Client and ClientSession
Client(..., strict_capabilities=True) rejects, before any request reaches
the transport, a call to a method whose required server capability the
connected server did not advertise -- for example list_resources() against
a server that only advertised tools, or subscribe_resource() when the
server's resources capability does not set subscribe. The rejection is an
MCPError with code -32601 (METHOD_NOT_FOUND) and data set to the method,
the same shape a compliant server returns for an unadvertised capability,
so opting in changes where the rejection happens, not what callers catch.

The default is False and unchanged: every request is sent and the server's
answer is surfaced. This mirrors the TypeScript SDK's
enforceStrictCapabilities option (also default-off). The same keyword-only
parameter exists on ClientSession for low-level users; Client forwards it.

The method-to-capability table lives in
mcp_types.methods.SERVER_CAPABILITY_REQUIREMENTS next to the other
per-method maps, with missing_server_capability() as its only evaluator,
so the check in ClientSession.send_request is a single data-driven gate
rather than a per-method condition, and the relationship is the same at
every protocol version. Because the gate reads server_capabilities, a bare
version pin (mode="2026-07-28" with no prior_discover=) would reject every
gated method; that combination is refused at Client construction with a
ValueError that names the fix.

The interaction-requirements entry for the lifecycle capability rule is no
longer marked untested: the new tests pin both the opt-in pre-wire
rejection and the default send-and-surface behaviour.
2026-06-27 18:53:13 +00:00
Max Isbey 1e2bd9befc Reject a second initialize on an already-initialized session
A server that had already completed the initialize handshake on a
connection answered a repeated initialize request on that same
connection as a fresh handshake, silently overwriting the session's
recorded client_params and negotiated protocol version. A
check_capability call made after that point then answered against the
second client's declared capabilities.

The handshake now commits at most once per connection: a repeated
initialize is answered with JSON-RPC error -32600 (INVALID_REQUEST,
"Session already initialized") and the established session keeps
serving. The check lives at the runner's request-dispatch boundary,
right next to its mirror (the request-before-initialize gate), so every
transport gets it without any transport-level body sniffing.

The discriminator is client_params rather than initialize_accepted: the
legacy stateless path builds a per-request connection that is born past
the gate but has no peer info yet, and its one initialize must still be
accepted.

No compliant client is affected. The spec makes initialization the
first interaction of a session, and ClientSession.initialize() is
already idempotent (a repeat call returns the first result without
sending anything). This only applies to the legacy (2025-11-25 and
earlier) handshake; the 2026-07-28 protocol removes initialize
entirely.

Closes #2605
2026-06-27 18:53:13 +00:00
Max Isbey 3eb352c8b0 Align OAuth client with spec on PKCE verification and scope selection
Two changes to how the OAuth client uses discovered authorization-server
metadata, both required by the MCP authorization specification.

Verify PKCE support before the authorization-code grant. The spec's
Authorization Code Protection section requires clients to verify PKCE
support from the authorization server's metadata and to refuse to
proceed when code_challenge_methods_supported is absent. The new
validate_pkce_support() also refuses a method list that omits S256,
since that is the only method this client sends. The check sits at the
top of _perform_authorization_code_grant, so it covers both the initial
401 flow and the 403 insufficient_scope step-up, while grants that never
issue an authorization code (client credentials, private key JWT) are
unaffected. When no metadata document was discovered at all the flow
proceeds as before: absence of a document is not evidence of
non-support.

Stop reading scopes_supported from authorization-server metadata when
selecting a scope. The spec's scope-selection chain is the
WWW-Authenticate scope parameter, then the protected-resource metadata's
scopes_supported, otherwise omit the scope parameter. The SDK inserted
an extra fallback to the authorization server's scopes_supported, which
over-requests (an authorization server may serve many resource servers,
so its list is a superset of any one resource's) and causes
access_denied failures against servers that reject unknown scopes. With
the fallback removed, clients that relied on it should pass an explicit
scope on their OAuthClientMetadata.

Closes #1307
2026-06-27 18:53:13 +00:00
Max Isbey 24061fe98b Reject non-HTTPS, non-loopback redirect URIs at client registration
Two fixes to the optional bundled OAuth authorization server (the
`auth_server_provider=` path).

The registration endpoint accepted any well-formed URL as a
`redirect_uris` entry: cleartext `http://` on a non-loopback host,
`javascript:`, `data:`, and URIs carrying a fragment all registered
successfully. The MCP authorization specification's Communication
Security section requires every redirect URI to be either localhost or
HTTPS, and OAuth 2.1 section 2.3 forbids a fragment component. Such an
entry is now rejected with `400 invalid_client_metadata`. Loopback is
exactly the three forms OAuth 2.1 section 8.4.2 names (`localhost`,
`127.0.0.1`, `[::1]`), on any port; query strings remain permitted.
This also rejects RFC 8252 private-use schemes such as
`com.example.app:/callback`: MCP restricts redirect URIs to HTTPS or
loopback, with no carve-out for native apps.

The rule lives on the request model: `RegistrationRequest`, until now a
dead alias of `OAuthClientMetadata`, becomes a real subclass with a
`redirect_uris` field validator, so a forbidden URI fails parsing and
takes the handler's existing `invalid_client_metadata` arm rather than
needing a post-parse check. `OAuthClientMetadata` itself is unchanged:
the client also serializes it when registering against third-party
authorization servers whose redirect-URI policies the SDK does not own.
The loopback host set moves to a single `LOOPBACK_HOSTS` constant in
`mcp.server.auth.provider`, shared with `validate_issuer_url`, which
previously inlined the same tuple.

Separately, the token endpoint now answers an authorization-code
exchange whose `redirect_uri` does not match the one used at
`/authorize` with `error=invalid_grant` instead of `invalid_request`.
RFC 6749 section 5.2 assigns this case to `invalid_grant` ("does not
match the redirection URI used in the authorization request"), and the
handler's other authorization-code failures already use it. The
exchange was already rejected with HTTP 400; only the `error` field
changes.

Update the affected tests and the interaction-requirement entries, and
add a migration note.

Closes #2629
2026-06-27 18:53:13 +00:00
Max Isbey 884badc944 Use spec error codes for unhandled elicitation/create and roots/list
A client constructed without an elicitation_callback or
list_roots_callback still answers the server's request, via a default
callback that returns a JSON-RPC error. Both defaults used -32600
(invalid request). The spec assigns a specific code to each case:

- elicitation/create: -32602 (invalid params). A client with no
  callback declares no elicitation modes, so every incoming request
  names an undeclared mode, which clients MUST answer with -32602.
- roots/list: -32601 (method not found), the code clients SHOULD use
  when they do not support roots.

The default sampling callback keeps -32600: the spec assigns no code
to a client that does not support sampling. Error messages are
unchanged.

Update the affected tests and the interaction-requirement entries that
recorded the old codes, fix the code named in the client callbacks doc
page, and add a migration note.
2026-06-27 18:53:13 +00:00
Max Isbey a1734460a1 Reject bearer tokens that carry no audience claim
When `AuthSettings.resource_server_url` is configured, `BearerAuthBackend`
previously ran the RFC 8707 audience comparison only for tokens whose
verifier populated `AccessToken.resource`: a token carrying no resource
indicator at all was accepted. The MCP authorization spec requires a
resource server to only accept tokens issued specifically for it, so the
gate now fails closed: a verified token with no `resource` is answered
`401 invalid_token` ("The access token carries no audience claim").
`resource_server_url=None` still means there is no audience to enforce.

For verifiers that validate the audience themselves and cannot surface
the claim (for example a JWT decoder configured with the expected
audience), the new `AuthSettings.verifier_validates_audience=True` opts
the gate out. The `AuthSettings.enforced_audience` property derives the
single value both server wirings pass to `BearerAuthBackend`, whose
signature is unchanged.

`RefreshToken` gains an optional `resource` field so an authorization
server provider can carry the original grant's audience binding through
`exchange_refresh_token`; without it every refreshed access token would
be audience-unbound and rejected by the hardened gate.

The docs tutorials and example servers now populate
`AccessToken.resource` (and the client-credentials demo token endpoint
honors the RFC 8707 `resource` parameter) so they pass the check they
teach. The migration guide entry for audience validation is rewritten
for the fail-closed behavior.
2026-06-27 18:53:13 +00:00
Max Isbey 47639a2b36 Stop replying to cancelled requests; map unhandled handler exceptions to -32603
A request cancelled via notifications/cancelled now gets no response: when
the handler scope's cancel is caught, JSONRPCDispatcher returns instead of
writing an error. The sender retired its own waiter when it cancelled, so
no reply is needed to unblock it.

An unhandled exception in a request handler now produces JSON-RPC error
-32603 (INTERNAL_ERROR) with the opaque message "Internal server error"
instead of code 0 carrying str(exc). The exception is still logged
server-side. To send a specific code/message, raise MCPError; pydantic
ValidationError still maps to INVALID_PARAMS.

handler_exception_to_error_data is now total: it returns
MappedError(error: ErrorData, unexpected: bool) for every Exception.
Callers gate logger.exception / raise_handler_exceptions on the
unexpected flag rather than re-deriving the rung set, so a handler that
deliberately raises MCPError(code=INTERNAL_ERROR) is not treated as a
crash. JSONRPCDispatcher, DirectDispatcher, and the modern HTTP entry's
_to_jsonrpc_response all call the one helper; DirectDispatcher no longer
hand-rolls its own ladder.

The interaction suite's protocol:cancel:in-flight and
protocol:error:internal-error requirements drop their divergence entries
and modern-error-surface arm exclusions; the two prompt-validation
divergence notes are reworded for the new -32603 surface.
docs/migration.md gains a section covering both behaviour changes.
2026-06-27 18:53:13 +00:00
Max Isbey bad42e2841 Emit RFC 6750 scope= in WWW-Authenticate and validate token audience
BearerAuthBackend / RequireAuthMiddleware now produce spec-conformant
challenges and reject tokens issued for a different resource server.

- A request with no credentials gets a bare `Bearer` challenge (with
  scope/resource_metadata only), not error="invalid_token" -- RFC 6750
  Section 3.1 says the error attribute SHOULD NOT appear when no
  authentication information was presented.
- A malformed/unknown token, an expired token, or a token whose audience
  does not match the configured resource_server_url is answered 401
  invalid_token with a specific error_description, carried via a new
  InvalidTokenUser marker so the middleware can distinguish it from
  no-credentials.
- All challenges (401 and the 403 insufficient_scope path) now advertise
  the required scopes in a `scope=` parameter, which the SDK client
  already reads to drive step-up.
- New check_token_audience() helper canonicalises default ports before
  comparing, and is wired through both the lowlevel and MCPServer
  Starlette stacks via the auth settings' resource_server_url.

Docs and migration guide updated; the corresponding interaction-suite
divergence entries are now closed.
2026-06-27 18:53:13 +00:00
Max 3b78f86886 Add docs, tested examples, and a story for SEP-990 identity assertion (#3004) 2026-06-26 21:01:46 +02:00
Max 24717cc8eb feat: RFC 6570 URI templates with operator-aware security (#2356) 2026-06-26 20:29:17 +02:00
Marcelo Trylesinski c0ecb70e24 Support RFC 8693 token exchange for enterprise IdP flows (SEP-990) (#2988)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-06-26 17:57:10 +02:00
Marcelo Trylesinski ecdf09d44f Deprecate Server.__init__ handlers for removed capabilities (#3002) 2026-06-26 17:51:13 +02:00
Max 08b62308d4 Client auto-resolves InputRequiredResult via existing callbacks (SEP-2322) (#2998) 2026-06-26 17:35:23 +02:00
Marcelo Trylesinski b31d95a429 Make OpenTelemetry tracing the single default middleware (#2995) 2026-06-26 15:47:37 +02:00
Marcelo Trylesinski 5b2713d40c Mirror x-mcp-header tool arguments into Mcp-Param-* request headers (SEP-2243) (#2990) 2026-06-26 14:36:56 +02:00
Max 3a8da8c0c3 Fix docs/release follow-ups from the mcp-types package split (#2977) 2026-06-26 13:16:09 +02:00
Max 411a6d3980 Rebuild the docs around tested examples; shrink README.v2.md to a pitch (#2978) 2026-06-26 12:49:19 +02:00
Marcelo Trylesinski f41a5193f3 Preserve empty issuer/resource paths on AuthSettings (#2987) 2026-06-26 11:41:41 +02:00
Max 587340279e Conformance burn-down: server-side InputRequiredResult, Mcp-Method/Name validation, x-mcp-header filter (14 scenarios → green) (#2974)
CI / checks (push) Failing after 0s
CI / all-green (push) Has been cancelled
2026-06-26 09:51:59 +02:00
Marcelo Trylesinski 0ee7f1b293 Split protocol types into a standalone mcp-types package (#2973) 2026-06-25 19:18:38 +02:00
Max 03681ed55e Client call_tool: input_responses/request_state retry params; InputRequiredResult via allow_input_required (#2968) 2026-06-25 17:37:00 +02:00
Max f226d00d0a Client-side 2026-07-28 support: .discover()/.adopt() + Client(mode=); request-metadata green (#2950) 2026-06-25 16:09:23 +02:00
Marcelo Trylesinski ad81ca234a Slim ServerMiddleware to (ctx, call_next) and add OpenTelemetryMiddleware (#2941)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-06-22 14:46:30 +01:00
Max 2397319a68 Server-side 2026-07-28 stateless support: classifier, driver split, server/discover (#2928) 2026-06-21 19:34:17 +01:00
Marcelo Trylesinski 44724284b3 Bind client credentials to their authorization server (SEP-2352) (#2933) 2026-06-20 18:47:22 +01:00
Marcelo Trylesinski 1331131650 Union previously requested scopes on step-up re-authorization (SEP-2350) (#2931) 2026-06-20 18:45:04 +02:00
Marcelo Trylesinski 4573e4ac33 Deprecate roots, sampling, and logging methods per SEP-2577 (#2926) 2026-06-20 18:25:41 +02:00
Marcelo Trylesinski cf41441e44 Send application_type during Dynamic Client Registration (SEP-837) (#2930) 2026-06-20 18:19:12 +02:00
Marcelo Trylesinski 60f37e9d7c Document redirect_uri wire-format change in OAuth migration note (#2929) 2026-06-20 16:14:03 +00:00
Marcelo Trylesinski 48cf4950dc Validate the iss authorization-response parameter (RFC 9207 / SEP-2468) (#2921) 2026-06-20 17:54:18 +02:00
Marcelo Trylesinski b7a5bffed0 Preserve empty URL paths on OAuth metadata models (#2925) 2026-06-20 15:32:03 +00:00
冯基魁 fda4c54362 fix: correct MCPServer call_tool result type (#2816)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2026-06-20 16:56:16 +02:00
Marcelo Trylesinski f253682393 Return -32602 for resource not found (SEP-2164) (#2920) 2026-06-20 16:55:23 +02:00
Max 84bf9bde05 First end-to-end 2026-07-28 stateless tools/call (experimental entry + ClientSession pin) (#2917) 2026-06-20 14:55:59 +01:00
Max 734746a3d9 Resolve protocol version per request and expose it as ctx.protocol_version (#2886) 2026-06-17 08:46:42 +01:00
Max 65be5a7147 Protocol types for 2026-07-28: superset monolith, committed per-version packages, and wire-method maps (#2849) 2026-06-16 17:40:14 +01:00
Max 1012d60004 [v2] ClientSession runs on JSONRPCDispatcher; BaseSession removed (#2838) 2026-06-15 14:46:34 +01:00
Max 7267818e44 Fix unknown-method error code and add a protocol version registry (#2836) 2026-06-11 16:47:22 +01:00
Max 5d826490b6 [v2] Dispatcher/ServerRunner receive-path swap — replaces BaseSession (#2710) 2026-06-09 12:58:47 +01:00
Max b478bff56d Remove the unsupported WebSocket transport (#2785) 2026-06-08 12:05:27 +01:00
Max bdc48e98b1 Fix stdio client shutdown bugs and rebuild the stdio test suite (#2773) 2026-06-05 16:15:43 +01:00
Max 8cc187fac0 Remove Tasks (SEP-1686) from the SDK (#2714) 2026-06-02 18:27:05 +02:00
Max f4753440da ci: deploy docs to py.sdk.modelcontextprotocol.io via Pages artifact (v1 at /, v2 at /v2/) (#2634) 2026-05-18 15:28:13 +01:00
Gyeongjun Paik (Kent) 3d7b311de0 fix: align Context logging methods with MCP spec data type (#2366)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
2026-04-14 21:41:51 +00:00
Max Isbey f27d2aac05 docs: fill migration guide gaps surfaced by automated upgrade eval (#2412) 2026-04-09 13:25:16 +01:00
Jonathan Hefner 5388bea53a docs: generate hierarchical per-module API reference pages (#2103) 2026-03-18 18:15:17 +00:00
Max Isbey 20dd94632e feat(client): store InitializeResult as initialize_result (#2300) 2026-03-18 17:31:26 +00:00