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
docs: era-accurate wording, a top-of-page auth router, and CIMD
Three unrelated small fixes on existing pages.
Era wording. Five sentences described a legacy mechanism as a universal
truth. At protocol revision 2026-07-28 there is no initialize handshake
(the client sends one server/discover probe), so 'the server's half of
the handshake', 'advertised it during the handshake', 'icons arrive
during the handshake', and -- best of all -- 'the [server/discover]
result is cacheable' being explained as 'the handshake result' were each
wrong for a modern connection. Each is reworded to the era-neutral truth
(capabilities are declared to every connecting client, however it
connected). Pages ABOUT the handshake (Protocol versions, Session
groups, the migration guide, the two OAuth 'handshakes' that are not
MCP's) are deliberately untouched.
Auth router. authorization.md and oauth-clients.md are the two halves
of one flow and the most-confused pair in the docs. oauth-clients.md
already corrects a wrong-lander in its opening lines ('This page is the
client side. Making your own server demand a token is Authorization.');
authorization.md only did so in its final line. It now has the mirror
sentence up top, so someone who clicked the wrong one finds out in
sentence three, not paragraph forty.
CIMD. The 2026-07-28 revision deprecates OAuth Dynamic Client
Registration in favor of Client ID Metadata Documents, and the SDK
already implements the client side (client_metadata_url= on
OAuthClientProvider) -- but the page never said so: it presented
dynamic registration as THE registration mechanism and named
client_metadata_url once, in passing, with no explanation. A short
section now covers what CIMD is, the one argument that enables it, the
exact condition under which it is used, that the fallback to dynamic
registration is silent, and the construction-time ValueError on a bad
URL. Deliberately prose-only: the SDK's own authorization server cannot
advertise CIMD support, so no runnable docs example can honestly
demonstrate the selection.
Copy file name to clipboardExpand all lines: docs/client/caching.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
@@ -13,7 +13,7 @@ Out of the box every result says `ttlMs: 0, cacheScope: "private"`: immediately
13
13
* The map is keyed by **method name**, and the six cacheable methods are the only legal keys. The parameter is typed `Mapping[CacheableMethod, CacheHint]`, so your editor autocompletes the keys and flags a typo before you run; anything that slips past the type checker raises at construction.
14
14
* A method you don't mention keeps the defaults. The map is a set of overrides, not a manifest.
15
15
*`CacheHint(ttl_ms=5_000)` left `scope` unset, so it stays `"private"`: five seconds of freshness, per caller. Scope and TTL are independent decisions.
16
-
*`"server/discover"` is a legal key too, since the handshake result is cacheable like any list.
16
+
*`"server/discover"` is a legal key too, since the discovery result is cacheable like any list.
17
17
18
18
!!! warning
19
19
`cacheScope: "public"` means *anyone* may be served your cached response. A shared
Copy file name to clipboardExpand all lines: docs/client/oauth-clients.md
+10-2Lines changed: 10 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,14 +83,22 @@ The first time `Client` sends a request, the server answers `401`. The provider
83
83
84
84
After that it is quiet. Tokens come out of storage, an expired access token is refreshed with the refresh token, and only when none of that works does it run the flow again.
85
85
86
-
You wrote none of it. Three keyword arguments remain (`timeout`, `client_metadata_url` and `validate_resource_url`), and this file needs none of them.
86
+
You wrote none of it. Three keyword arguments remain (`timeout`, `client_metadata_url` and `validate_resource_url`), and this file needs none of them.`client_metadata_url` is the one worth knowing about; it gets its own section below.
87
87
88
88
### Try it
89
89
90
90
Every other example in these docs you can check with an in-memory `Client(server)`. Not this: the whole point of the flow is an HTTP `401`, and there is no HTTP between an in-memory client and its server.
91
91
92
92
The repository ships the live version. `examples/servers/simple-auth/` runs a standalone authorization server and a protected MCP server; `examples/clients/simple-auth-client/` is this page's client grown into a small CLI. Its README has the two commands: start the servers, run the client against them, and you watch the four steps go by.
93
93
94
+
## Client ID Metadata Documents
95
+
96
+
The 2026-07-28 revision of the spec deprecates dynamic client registration in favor of **Client ID Metadata Documents** (CIMD). Instead of POSTing a fresh registration to every authorization server it meets, your client publishes one JSON document about itself at a stable HTTPS URL, and that URL *is* its `client_id`. The authorization server fetches the document; the provider never touches it.
97
+
98
+
The SDK already speaks it: pass the URL as `client_metadata_url=` when you construct the provider. When the authorization server's metadata advertises `client_id_metadata_document_supported: true`, the provider skips the `/register` request entirely — the URL goes into the flow as the `client_id`, and there is no `client_secret`. When the server doesn't advertise it (most don't yet), or you never pass a URL, the provider falls back to dynamic registration **silently**, and everything above works exactly as described. Stored `client_info` still wins over both.
99
+
100
+
The URL must be HTTPS with a non-root path; anything else is a `ValueError` at construction, before any network happens. The shipped `examples/clients/simple-auth-client/` takes it as the `MCP_CLIENT_METADATA_URL` environment variable.
101
+
94
102
## Machine to machine
95
103
96
104
A nightly job, a CI step, another service. There is no browser and nobody to click "allow". That is the **client credentials** grant: you already hold a `client_id` and a `client_secret`, and the token endpoint is the whole flow.
@@ -132,7 +140,7 @@ Not everything is a flow error. The network can still fail; those are ordinary `
132
140
*`OAuthClientProvider` is an `httpx.Auth`. Put it on an `httpx.AsyncClient`, pass that to `streamable_http_client(url, http_client=...)`, and `Client` never knows OAuth happened.
133
141
* You supply four things: the server URL, an `OAuthClientMetadata`, a `TokenStorage`, and the redirect/callback handler pair.
134
142
*`TokenStorage` is a `Protocol`: four async methods, no base class. Persist `client_info` as well as the tokens.
135
-
* Discovery, dynamic registration, PKCE, the `state` and `iss` checks, and token refresh are the provider's job, not yours.
143
+
* Discovery, registration (dynamic, or via a **Client ID Metadata Document**), PKCE, the `state` and `iss` checks, and token refresh are the provider's job, not yours.
136
144
*`ClientCredentialsOAuthProvider` is the no-human version: `client_id` + `client_secret`, no handlers, no browser.
137
145
* Every OAuth failure is an `OAuthFlowError`; `OAuthRegistrationError` and `OAuthTokenError` are its subclasses.
@@ -123,7 +123,7 @@ Look back over this page. You wrote three small Python functions. You did **not*
123
123
* A JSON Schema. `a: int, b: int`*is* the schema for `add`.
124
124
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
125
125
* A capability declaration. `MCPServer` made it for you.
126
-
* A line of protocol. The handshake, the version negotiation, the JSON-RPC framing: all of it happened inside `mcp dev` and `Client(mcp)`, and you never saw it.
126
+
* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `Client(mcp)`, and you never saw it.
Copy file name to clipboardExpand all lines: docs/run/authorization.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,8 @@ Over Streamable HTTP your MCP server is an ordinary web service, and you protect
4
4
5
5
In OAuth terms, your server is a **resource server**. It never signs anyone in and it never issues a token. It does one thing: look at the `Authorization` header on each request and decide whether the token in it is good.
6
6
7
+
This page is the server side. A client that discovers your authorization server and fetches the token is **[OAuth clients](../client/oauth-clients.md)**.
8
+
7
9
## The three parties
8
10
9
11
* The **authorization server** signs people in and issues access tokens. You don't write this. It's your identity provider (Auth0, Keycloak, Entra, your own).
You didn't list `completions` anywhere. The SDK saw the handler and advertised it during the handshake. Every *optional* capability works this way: the handler is the declaration. (The three primitives are not optional: `MCPServer` always declares those, handlers or not.)
75
+
You didn't list `completions` anywhere. The SDK saw the handler and declared the capability for you. Every *optional* capability works this way: the handler is the declaration. (The three primitives are not optional: `MCPServer` always declares those, handlers or not.)
76
76
77
77
!!! check
78
78
Go back to the first `server.py` (the one with no handler) and ask it anyway. The call fails
0 commit comments