You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: docs/client/index.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -145,7 +145,7 @@ The resource verbs come in pairs: two ways to list, one way to read.
145
145
146
146
`read_resource` returns `contents`, a list of `TextResourceContents` or `BlobResourceContents`. Same idea as tool content: narrow with `isinstance`, then read `.text` (or `.blob`).
147
147
148
-
A client can also **subscribe** to a resource and be told when it changes: `subscribe_resource(uri)` and `unsubscribe_resource(uri)`, same shape as everything else here. `MCPServer` doesn't implement that half. It says so up front (`server_capabilities.resources.subscribe` is `False`) and answers the request with an `MCPError`: `-32601`, *Method not found*. With `strict_capabilities=True` you get the same `-32601` without the round trip: the client sees `server_capabilities.resources.subscribe` is falsy and never sends the request. A server that does support subscriptions is built on the low-level `Server` (**The low-level Server**).
148
+
A client can also **subscribe** to a resource and be told when it changes: `subscribe_resource(uri)` and `unsubscribe_resource(uri)`, same shape as everything else here. `MCPServer` doesn't implement that half. It says so up front (`server_capabilities.resources.subscribe` is `False`) and answers the request with an `MCPError`: `-32601`, *Method not found*. With `strict_capabilities=True` you get the same `-32601`for `subscribe_resource`without the round trip: the client sees `server_capabilities.resources.subscribe` is falsy and never sends the request. `unsubscribe_resource` is still sent — only the base `resources` capability gates it, matching the TypeScript SDK — so its `-32601` comes from the server. A server that does support subscriptions is built on the low-level `Server` (**The low-level Server**).
Copy file name to clipboardExpand all lines: docs/migration.md
+15-3Lines changed: 15 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -41,13 +41,19 @@ An unhandled exception in a request handler now produces JSON-RPC error `-32603`
41
41
code `0` with `str(exc)` as the message, leaking handler internals to the peer; the
42
42
exception is still logged server-side via `logger.exception`. To send a specific
43
43
code and message, raise `MCPError` (unchanged); a pydantic `ValidationError` is
44
-
still mapped to `INVALID_PARAMS`.
44
+
still mapped to `INVALID_PARAMS`. An `MCPError`*subclass* raised by a handler
45
+
(e.g. `UrlElicitationRequiredError`) reaches the caller as plain `MCPError` with
46
+
the same `code`, `message`, and `data` on every dispatch path — including the
47
+
in-process `Client(server)` — so catch `MCPError` and match on `error.code`
48
+
rather than on the subclass type.
45
49
46
50
A request cancelled via `notifications/cancelled` now receives no response at all,
47
51
per the spec's SHOULD. v1 answered the cancelled request with an error
48
52
(`code=0, message="Request cancelled"`). The sender's awaiting call already fails
49
53
with anyio cancellation when its scope is cancelled, so no reply is needed to
50
-
unblock it.
54
+
unblock it. On legacy streamable HTTP this means the cancelled request's POST SSE
55
+
stream now terminates without a response frame, and in JSON-response mode the POST
56
+
completes with `204 No Content`.
51
57
52
58
### A second `initialize` on an already-initialized session is rejected
53
59
@@ -148,7 +154,7 @@ When no authorization-server metadata document could be discovered at all, the f
148
154
149
155
The specification's scope-selection chain is two steps: the `scope` parameter from the `WWW-Authenticate` challenge, then `scopes_supported` from the Protected Resource Metadata document, *otherwise the `scope` parameter is omitted*. The SDK inserted an extra fallback between those two steps — the **authorization-server** metadata'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 caused real `access_denied` failures ([#1307](https://github.com/modelcontextprotocol/python-sdk/issues/1307)). That fallback is removed: when neither the challenge nor the PRM names scopes, the client now omits the `scope` parameter and lets the authorization server apply its defaults.
150
156
151
-
This also affects the SEP-2207 `offline_access` augmentation, which only fires once a base scope was selected: if the authorization server's `scopes_supported` was your only scope source, the client now sends no `scope` at all (not even `offline_access`) and the authorization server's defaults decide whether a refresh token is issued. In either case, if you relied on the removed fallback, pass an explicit `scope` on the `OAuthClientMetadata` you give to `OAuthClientProvider`.
157
+
This also affects the SEP-2207 `offline_access` augmentation, which only fires once a base scope was selected: if the authorization server's `scopes_supported` was your only scope source, the client now sends no `scope` at all (not even `offline_access`) and the authorization server's defaults decide whether a refresh token is issued. In either case, if you relied on the removed fallback, pass an explicit `scope` on the `OAuthClientMetadata` you give to `OAuthClientProvider`. That explicit scope ranks *below* the spec's two sources (matching the TypeScript SDK): a `scope` on the `WWW-Authenticate` challenge or a PRM `scopes_supported` still wins, so the explicit value only takes effect when both are silent.
152
158
153
159
### `get_session_id` callback removed from `streamable_http_client`
154
160
@@ -1583,6 +1589,12 @@ Leaving `resource_server_url=None` continues to disable the check entirely (ther
1583
1589
1584
1590
`RefreshToken` gains an optional `resource` field so an `OAuthAuthorizationServerProvider` can propagate the original grant's audience binding through `exchange_refresh_token`; without it a refreshed access token would carry no audience and be rejected. `BearerAuthBackend.__init__` gains a keyword-only `resource_server_url: AnyHttpUrl | None = None`, wired automatically from `AuthSettings.enforced_audience`; `None` (the default, and what the SDK passes when `verifier_validates_audience` is set) means no audience is enforced.
1585
1591
1592
+
The error responses the bearer gate sends changed shape in the same release, following RFC 6750 §3:
1593
+
1594
+
- A request presenting **no credentials at all** (no `Authorization: Bearer` header) is now answered with a bare challenge — `WWW-Authenticate: Bearer scope="…", resource_metadata="…"` and an empty JSON body `{}` — instead of `error="invalid_token", error_description="Authentication required"` in both the header and the body. RFC 6750 §3.1 says the `error` attribute should only appear when the request actually carried a token, so "no token" and "rejected token" are now distinguishable; anything matching on the literal `Authentication required` string or expecting a non-empty 401 body must be updated.
1595
+
- Every challenge — the `401`s and the `403` — now advertises the configured `required_scopes` in an RFC 6750 `scope="…"` parameter, which spec-conformant clients (including this SDK's OAuth client) read as the first step of scope selection and step-up authorization.
1596
+
- The `error_description` strings changed. A rejected token now states the failure: `The access token is malformed or unknown`, `The access token has expired`, `The access token carries no audience claim`, or `The access token was issued for a different resource` (previously all of these — where they were rejected at all — said `Authentication required`). The `403 insufficient_scope` description is now the fixed string `The access token lacks a required scope` instead of `Required scope: {scope}`; the required scope moved to the machine-readable `scope=` challenge parameter. Treat `error_description` as human-readable prose, not a contract.
0 commit comments