Skip to content

Commit a82e2a0

Browse files
committed
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.
1 parent f592d8b commit a82e2a0

7 files changed

Lines changed: 18 additions & 8 deletions

File tree

docs/client/caching.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Out of the box every result says `ttlMs: 0, cacheScope: "private"`: immediately
1313
* 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.
1414
* A method you don't mention keeps the defaults. The map is a set of overrides, not a manifest.
1515
* `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.
1717

1818
!!! warning
1919
`cacheScope: "public"` means *anyone* may be served your cached response. A shared

docs/client/oauth-clients.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,22 @@ The first time `Client` sends a request, the server answers `401`. The provider
8383

8484
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.
8585

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.
8787

8888
### Try it
8989

9090
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.
9191

9292
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.
9393

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+
94102
## Machine to machine
95103

96104
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 `
132140
* `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.
133141
* You supply four things: the server URL, an `OAuthClientMetadata`, a `TokenStorage`, and the redirect/callback handler pair.
134142
* `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.
136144
* `ClientCredentialsOAuthProvider` is the no-human version: `client_id` + `client_secret`, no handlers, no browser.
137145
* Every OAuth failure is an `OAuthFlowError`; `OAuthRegistrationError` and `OAuthTokenError` are its subclasses.
138146

docs/get-started/first-steps.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ asyncio.run(main())
100100
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
101101
```
102102

103-
That dictionary is the server's half of the handshake:
103+
That dictionary is your server's declared **capabilities**the first thing every connecting client learns:
104104

105105
| Capability | The client may now call |
106106
|-------------|------------------------------------------------------------|
@@ -123,7 +123,7 @@ Look back over this page. You wrote three small Python functions. You did **not*
123123
* A JSON Schema. `a: int, b: int` *is* the schema for `add`.
124124
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
125125
* 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.
127127

128128
That ratio is the whole point of the SDK.
129129

docs/run/asgi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ A browser-based client needs two permissions from you: to **send** its MCP reque
150150

151151
* The handler is plain Starlette: an `async` function from `Request` to `Response`.
152152
* `streamable_http_app()` picks up every custom route. `app.routes` is now `/mcp` and `/health`.
153-
* `GET /health` answers `{"status": "ok"}` with no MCP in sight: no session, no handshake.
153+
* `GET /health` answers `{"status": "ok"}` with no MCP in sight.
154154

155155
!!! warning
156156
Custom routes are **never authenticated**, even when the rest of the server is. That is

docs/run/authorization.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Over Streamable HTTP your MCP server is an ordinary web service, and you protect
44

55
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.
66

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+
79
## The three parties
810

911
* 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).

docs/servers/completions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Registering the handler is the declaration. Connect a client and look:
7272
client.server_capabilities.completions # CompletionsCapability()
7373
```
7474

75-
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.)
7676

7777
!!! check
7878
Go back to the first `server.py` (the one with no handler) and ask it anyway. The call fails

docs/servers/media.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ The same `icons=[...]` keyword is accepted by `MCPServer(...)`, `@mcp.tool()`, `
8989

9090
### Where a client sees them
9191

92-
Icons travel with whatever they decorate. The server's arrive during the handshake, on `client.server_info`:
92+
Icons travel with whatever they decorate. The server's arrive when the client connects, on `client.server_info`:
9393

9494
```python
9595
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]

0 commit comments

Comments
 (0)