TL;DR:Claude Code, Claude Desktop and Claude Cowork all use CIMD on the user-initiated paths I observed. An MCP authorization server can ship CIMD-only and cover the current Anthropic client population.
Anthropic only documents Client ID Metadata Document (CIMD) support for one of their products: Claude Code. For Desktop, Web and Cowork the documentation says the surfaces share infrastructure but never names the client_id or the registration mechanism. If you are designing an OAuth 2.1 authorization server for MCP and want to know whether you can ship CIMD-only or have to support Dynamic Client Registration (DCR) as well, that gap matters.
A Reddit thread from May 2026 asks exactly the same question. The answers are not authoritative. A few third-party blogs claim hosted surfaces use a pre-registered UUID; those claims do not cite primary sources and turned out to be incorrect once tested. So I tested each product directly against an authorization server (AS) I control, logged every request and read the answer off the wire.
Why CIMD beats DCR #
DCR is a permanent operational liability. It exposes an open registration endpoint, so anti-abuse becomes the AS operator’s problem. RFC 7591 leaves the gating policy entirely to the operator. Without software_statement
validation or some other admission control, spammers can carve registration rows into the database until storage explodes or queries become slow scans.
Every registered client gets to claim its own client_name
, client_uri
and redirect_uris
. Consent screens can lie unless registration is gated on a trust signal stronger than the request itself, such as pre-shared signing keys, manual review or an allowlist. Each registration also creates a long-lived row with its own garbage collection story and a question of what happens to issued tokens when the client is deleted. RFC 7592 adds an entire CRUD API on top.
CIMD avoids all of that. The client_id is a URL. The origin of the URL is the trust anchor; if the client_id is on claude.ai
, the AS already knows whether it trusts claude.ai
. The metadata document is fetched fresh each time, so there is no stored state. There is no open endpoint to abuse and no admission policy to write.
As such, given the choice between the two, the right call is to ship CIMD first. DCR can wait until a non-Anthropic MCP client that needs it shows up.
Why Cowork needs an extra step #
Web, Desktop and Mobile all run the OAuth client in the user’s own browser or app process. Whatever client_id
reaches my AS is, by construction, the one those products use. There is no service in between that could mint a different identity.
Cowork is different. Cowork runs on Anthropic’s servers. When a Cowork agent invokes an MCP tool, the request originates from Anthropic’s egress, not from the user’s machine. Even if the initial OAuth flow looks identical to Web (because the user kicks it off from a browser), nothing in the protocol rules out Cowork’s server side using a different OAuth client when it refreshes a token, picks up a scheduled task or runs autonomously. The shared-infrastructure claim in Anthropic’s docs is suggestive but not conclusive on its own.
As such, the Cowork test has to go past the initial authorize. I need to see Cowork actually invoke a tool and confirm the bearer it sends matches the one issued during the user-initiated CIMD flow. That covers the user-initiated happy path. Token refresh, scheduled tasks and autonomous runs are each separate observable events that would each require their own capture; this article only closes the user-initiated case directly.
Setup #
Tested clients- Claude Code CLI: 2.1.142- Claude Desktop: v1.7196.0 (macOS)- Claude.ai Web: personal account- Claude Cowork: running inside Claude.app v1.7196.0, personal account
Test infrastructure- Language: Python 3.14, FastAPI + uvicorn (~150 LOC)- Public URL: cloudflared quick tunnel (https://<random>.trycloudflare.com)- Logging: every inbound HTTP request to JSONL, full headers + body
The AS advertised both registration_endpoint
and client_id_metadata_document_supported: true
so each client had every path available and picked the one it actually wanted to use. Per Anthropic's selection rule, Claude picks CIMD only when both client_id_metadata_document_supported: true
and none
in token_endpoint_auth_methods_supported
are advertised. Both conditions were satisfied.
{ "issuer": "https://<tunnel>.trycloudflare.com", "authorization_endpoint": "https://<tunnel>.trycloudflare.com/oauth/authorize", "token_endpoint": "https://<tunnel>.trycloudflare.com/oauth/token", "registration_endpoint": "https://<tunnel>.trycloudflare.com/oauth/register", "client_id_metadata_document_supported": true, "grant_types_supported": ["authorization_code", "refresh_token"], "response_types_supported": ["code"], "token_endpoint_auth_methods_supported": ["none", "private_key_jwt"], "code_challenge_methods_supported": ["S256"], "scopes_supported": []}
The MCP server returned 401 Unauthorized
with the standard WWW-Authenticate: Bearer resource_metadata="..."
challenge. The classification rule is whichever OAuth-related request hits the AS first after that 401:
GET /oauth/authorize?...&client_id=https://... -> CIMD (client_id is a URL)POST /oauth/register with a JSON body -> DCR (client self-registered first)GET /oauth/authorize?...&client_id=<UUID> -> Pre-registered (out-of-band)
For CIMD captures I also fetched the metadata document independently to record its full contents.
Findings #
Claude Code CLI 2.1.142
After claude mcp add --transport http cimd-test https://<tunnel>/mcp
and a new session:
POST /mcp ua=claude-code/2.1.142 (cli)GET /.well-known/oauth-protected-resource ua=claude-code/2.1.142 (cli)GET /.well-known/oauth-authorization-server ua=claude-code/2.1.142 (cli)GET /.well-known/oauth-protected-resource ua=Bun/1.3.14GET /.well-known/oauth-authorization-server ua=Bun/1.3.14GET /oauth/authorize?response_type=code &client_id=https%3A%2F%2Fclaude.ai%2Foauth%2Fclaude-code-client-metadata &redirect_uri=http%3A%2F%2Flocalhost%3A61264%2Fcallback &code_challenge_method=S256 &resource=https%3A%2F%2F<tunnel>%2Fmcp &state=...
CIMD. The metadata document, fetched independently:
{ "client_id": "https://claude.ai/oauth/claude-code-client-metadata", "client_name": "Claude Code", "client_uri": "https://claude.ai", "redirect_uris": [ "http://localhost/callback", "http://127.0.0.1/callback" ], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none"}
The metadata declares loopback redirect URIs without ports, but the runtime authorize uses http://localhost:61264/callback
. The port is OS-assigned and changes on every invocation. Strict byte-for-byte redirect_uri
matching against the registered list would reject every Claude Code flow on the first request. RFC 8252 §7.3 covers exactly this case: for loopback redirect URIs, the AS must accept any port at request time. Without that carve-out, Claude Code does not work; there is no client-side workaround because the OS picks the port.
The CLI also duplicates discovery between its main process (claude-code/2.1.142 (cli)
) and an internal Bun worker (Bun/1.3.14
). Each /.well-known
endpoint gets hit twice. The duplication does not affect correctness but doubles the discovery traffic per session.
Claude Web (claude.ai)
The custom-connector modal at https://claude.ai/customize/connectors?modal=add-custom-connector
accepts a Name and a Remote MCP server URL, with optional OAuth Client ID and Client Secret fields. With both OAuth fields left blank:
POST /mcp ua=Claude-UserPOST /mcp ua=python-httpx/0.28.1GET /.well-known/oauth-protected-resource ua=python-httpx/0.28.1GET /.well-known/oauth-authorization-server ua=python-httpx/0.28.1GET /oauth/authorize?response_type=code &client_id=https%3A%2F%2Fclaude.ai%2Foauth%2Fmcp-oauth-client-metadata &redirect_uri=https%3A%2F%2Fclaude.ai%2Fapi%2Fmcp%2Fauth_callback &code_challenge_method=S256 &resource=https%3A%2F%2F<tunnel>%2Fmcp &state=...
CIMD again, with a different URL than Code:
{ "client_id": "https://claude.ai/oauth/mcp-oauth-client-metadata", "client_name": "Claude", "client_uri": "https://claude.ai", "redirect_uris": [ "https://claude.ai/api/mcp/auth_callback" ], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none"}
The python-httpx/0.28.1
requests are a server-side probe Anthropic's backend performs before the user-facing browser redirect. The connector form validates the URL before sending the user anywhere. A localhost MCP server cannot be tested through Web; the tunnel needs to be publicly reachable from Anthropic's egress.
Claude Desktop v1.7196.0
A connector added on Web shows up in Desktop’s Connectors view automatically because both share the same account. Clicking “Connect” in Desktop:
POST /mcp ua=python-httpx/0.28.1GET /.well-known/oauth-protected-resource ua=python-httpx/0.28.1GET /.well-known/oauth-authorization-server ua=python-httpx/0.28.1GET /oauth/authorize?response_type=code &client_id=https%3A%2F%2Fclaude.ai%2Foauth%2Fmcp-oauth-client-metadata &redirect_uri=https%3A%2F%2Fclaude.ai%2Fapi%2Fmcp%2Fauth_callback &code_challenge_method=S256 &resource=https%3A%2F%2F<tunnel>%2Fmcp &state=...
Functionally identical to Web from the AS’s perspective: same CIMD URL, same redirect URI, same auth method. The user agent and originating process differ (the captures show the Desktop app rather than the browser kicking off the flow), but the AS sees the same OAuth client.
Claude Cowork inside Claude Desktop
For Cowork, the AS had to complete the flow rather than stub it out. I extended it to issue a code, issue a bearer and serve initialize
/ tools/list
/ tools/call
with a single ping
tool. Then I connected the test server, opened a Cowork space and prompted "Can you try the ping tool in cimd-test mcp?". Claude replied "Got pong". The relevant captures:
GET /oauth/authorize client_id=https://claude.ai/oauth/mcp-oauth-client-metadataPOST /oauth/token ua=python-httpx/0.28.1POST /mcp rpc=initialize auth=Bearer tt_5R7FPF5KnzEOaS4...POST /mcp rpc=notifications/initialized auth=Bearer tt_5R7FPF5KnzEOaS4...POST /mcp rpc=tools/list auth=Bearer tt_5R7FPF5KnzEOaS4...POST /mcp rpc=initialize auth=Bearer tt_5R7FPF5KnzEOaS4...POST /mcp rpc=notifications/initialized auth=Bearer tt_5R7FPF5KnzEOaS4...POST /mcp rpc=tools/list auth=Bearer tt_5R7FPF5KnzEOaS4...POST /mcp rpc=tools/call auth=Bearer tt_5R7FPF5KnzEOaS4...
The Cowork tools/call
carries the same bearer token issued during the original CIMD-based exchange. No second authorize, no second token request, no separate OAuth client. Cowork's server-side runtime reuses the user's CIMD-issued token verbatim when it invokes a tool, which is the most common path. The three other paths I flagged earlier (token refresh, scheduled tasks, autonomous runs) are not covered by this single observation: the test token had a 1-hour TTL and the session was short, so no refresh occurred, and I did not exercise scheduled or autonomous Cowork runs. Those each remain untested.
What the AS needs to do in practice #
The two CIMD URLs to allowlist, both on claude.ai
:
https://claude.ai/oauth/claude-code-client-metadata (Claude Code)https://claude.ai/oauth/mcp-oauth-client-metadata (Web / Desktop / Mobile / Cowork)
And a short list of things the AS must do to make CIMD-only work end-to-end:
- Accept loopback redirect URIs with any port (RFC 8252 §7.3). Required for Claude Code.
- Advertise
token_endpoint_auth_methods_supported
including"none"
. Required for any product to pick CIMD. - Require PKCE S256. Every product sends it.
- Honor the
resource
parameter (RFC 8707) and audience-bind issued tokens. Every product sends it. - Accept connectors with no declared scope. Empty
scopes_supported: []
is enough. - Be reachable from Anthropic’s egress, not just from the user’s machine. Required for Web, Desktop and Cowork to even reach the OAuth step.
Caveats #
Tested on 2026–05–15. Claude Code CLI 2.1.142, Claude.app v1.7196.0. Builds older than the Web/Desktop unification (sometime in 2025) may behave differently. Future Anthropic builds could ship per-surface CIMD URLs, signed software statements or DCR-based registration at any point.
The Cowork observation is a single user-initiated tool call. That is enough to confirm token reuse on the happy path but not enough to rule out a different OAuth client being used for token refresh, scheduled tasks or autonomous runs. A follow-up test could exercise refresh by issuing a short-TTL access token and waiting for Cowork to refresh, and exercise the scheduled path by leaving an autonomous task on the connector. I have not done either yet.
Cowork was tested under a personal Anthropic account because the team workspace I had primary access to disables member-added custom connectors. The captures matched the Web/Desktop captures on client_id
, redirect_uri
and token_endpoint_auth_method
, but a re-run on an admin-allowed team workspace is worth doing if the difference between team and personal is on your threat model.
DCR is not the moral equivalent of CIMD with extra steps. There are legitimate reasons MCP clients without a hosted origin (desktop IDEs, etc.) reach for it. Cursor and Windsurf both use DCR at the time of writing. If the AS needs to support non-Anthropic clients, DCR comes back into scope. The conclusion here is narrowly about Anthropic’s own products.
Sources #
Authentication for connectors, Anthropic docsr/ClaudeAI — Any info about CIMD for Claude Desktop/Web client?RFC 7591, OAuth 2.0 Dynamic Client Registration ProtocolRFC 7592, OAuth 2.0 Dynamic Client Registration Management ProtocolRFC 8252 §7.3, OAuth 2.0 for Native Apps, Loopback Interface RedirectionRFC 8707, Resource Indicators for OAuth 2.0MCP Authorization SpecAaron Parecki, Client ID Metadata Document draft