Back to How Muse works100%

CIMD, step by step: mock and real

With a Client ID Metadata Document the app's client_id is a web address. The authorization server fetches that address, checks what it finds, and only then lets the sign-in go ahead. Below, every request and response, once as a labelled mock of what Muse's connector would do, and once as captured from real servers.

Real ChatGPT, Codex and Claude Code, against Linear and Notion

Captured on 29 October 2026 from the public internet. The apps are the real ones: OpenAI hosts ChatGPT's and Codex's documents, Anthropic hosts Claude Code's. The servers are the real MCP servers of Linear and Notion, both of which advertise CIMD. Nothing here needed an account, because an authorize request only starts a sign-in.

Not captured: what happens after Approve. That needs a signed-in Linear or Notion user, so the code and token steps are not shown on this tab.

1. The three apps' own documents

Request, no credentials, same for all three

GET /oauth/client.json HTTP/2                   Host: chatgpt.com
GET /oauth/codex/client.json HTTP/2             Host: chatgpt.com
GET /oauth/claude-code-client-metadata HTTP/2   Host: claude.ai

ChatGPT: 200, https://chatgpt.com/oauth/client.json

{
  "client_id": "https://chatgpt.com/oauth/client.json",
  "client_name": "ChatGPT",
  "client_uri": "https://chatgpt.com/",
  "logo_uri": "https://persistent.oaistatic.com/sonic/misc/openai-logo.png",
  "redirect_uris": ["https://chatgpt.com/connector_platform_oauth_redirect"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "private_key_jwt",
  "token_endpoint_auth_methods_supported": ["none", "private_key_jwt"],
  "token_endpoint_auth_signing_alg": "RS256",
  "jwks_uri": "https://chatgpt.com/oauth/jwks.json"
}

Codex: 200, https://chatgpt.com/oauth/codex/client.json

{
  "client_id": "https://chatgpt.com/oauth/codex/client.json",
  "client_name": "Codex",
  "client_uri": "https://chatgpt.com/codex",
  "application_type": "native",
  "logo_uri": "https://persistent.oaistatic.com/sonic/misc/openai-logo.png",
  "redirect_uris": ["http://127.0.0.1/callback", "http://localhost/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "token_endpoint_auth_methods_supported": ["none"]
}

Claude Code: 200, https://claude.ai/oauth/claude-code-client-metadata

{
  "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"
}

Two different kinds of client. Codex and Claude Code are public clients: no secret, PKCE only, a loopback callback with no port written in the document. ChatGPT is a confidential client: it asks for private_key_jwt, so it proves who it is at the token step by signing with a key published at its jwks_uri. (Its document also lists none.)

2. A real server says it understands CIMD

Request

GET /.well-known/oauth-authorization-server HTTP/2
Host: mcp.linear.app

Response (200, seven fields kept out of the full document)

{
  "issuer": "https://mcp.linear.app",
  "authorization_endpoint": "https://mcp.linear.app/authorize",
  "token_endpoint": "https://mcp.linear.app/token",
  "registration_endpoint": "https://mcp.linear.app/register",
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
  "client_id_metadata_document_supported": true
}

Linear also keeps its registration endpoint, so a client that has no document can still register (DCR). Notion's metadata is the same shape, with private_key_jwt added to the methods it accepts, the one ChatGPT asks for.

3. ChatGPT asks Linear to sign someone in

Request (the code_challenge is a sample)

GET /authorize
  ?response_type=code
  &client_id=https://chatgpt.com/oauth/client.json
  &redirect_uri=https://chatgpt.com/connector_platform_oauth_redirect
  &code_challenge=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  &code_challenge_method=S256&state=x
Host: mcp.linear.app

Response

HTTP/2 200
content-type: text/html; charset=UTF-8

<title>ChatGPT | Authorization Request</title>
  (page preloads https://persistent.oaistatic.com/sonic/misc/openai-logo.png)
  Website: https://chatgpt.com/
  [ Approve ]

Nobody registered ChatGPT with Linear. Linear fetched the document at the client_id address and built its consent page from it: the name in the title, OpenAI's logo from logo_uri, and the website from client_uri. The page is Linear's own, not ChatGPT's.

4. Codex asks, with a callback port nobody listed

Request

GET /authorize
  ?response_type=code
  &client_id=https://chatgpt.com/oauth/codex/client.json
  &redirect_uri=http://127.0.0.1:1455/callback
  &code_challenge=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  &code_challenge_method=S256&state=x
Host: mcp.linear.app

Response

HTTP/2 200
content-type: text/html; charset=UTF-8

<title>Codex | Authorization Request</title>
  Website: https://chatgpt.com/codex
  Callback: http://127.0.0.1:1455/callback
  [ Approve ]

The document lists http://127.0.0.1/callback with no port. Linear accepted port 1455 anyway. For a loopback address any port matches, because a command-line app picks a free port when it starts (Codex lets you fix it with mcp_oauth_callback_port). Linear shows the exact callback on its page.

5. A callback the document does not list is refused

Request: the same Codex request, with redirect_uri=https://evil.example/cb

Linear

HTTP/2 400
Invalid redirect URI

Notion

HTTP/2 400
{"error":"invalid_request","error_description":"Invalid redirect URI"}

This is what stops anyone from borrowing ChatGPT's or Codex's address: a code is only ever sent to a callback in the document its owner published. Neither server redirected anywhere.

6. Notion accepts the same two apps

GET /authorize?client_id=https://chatgpt.com/oauth/client.json&redirect_uri=https://chatgpt.com/connector_platform_oauth_redirect&...
Host: mcp.notion.com

HTTP/2 302
location: https://api.notion.com/v1/oauth/authorize?client_id=1f8d872b-594c-80a4-...&redirect_uri=https%3A%2F%2Fmcp.notion.com%2Fcallback&...

The Codex request got the same 302. Notion accepted the client and handed the user on to api.notion.com with a Notion client ID and a callback back on mcp.notion.com. That looks like the MCP server acting as the authorization server the app talks to, and signing people in with Notion's own login behind it. That reading is my inference from the redirect, not something Notion documents here.

ServerAuthorization server metadata, 29 Oct 2026Authorize with ChatGPT and Codex documents
Linear mcp.linear.appclient_id_metadata_document_supported: trueAccepted (consent page). Unlisted callback refused.
Notion mcp.notion.comtrue, and private_key_jwt acceptedAccepted (302 to Notion sign-in). Unlisted callback refused.
Sentry, Vercel, Cloudflare, Canva, Monday, Shopify, WorkOS, StytchtrueNot tried.
Also tried: real PingFederate with client.dev's example document

Host names of the PingFederate servers are replaced with .example names here. The responses are as captured.

PingFederate 13.1 added CIMD. Both a local 13.1.3 and the SE one (pf.example) advertise client_id_metadata_document_supported: true and list none and private_key_jwt.

First try: refused. An authorize request with client_id=https://client.dev/oauth/metadata.json returned 400 - Invalid client_id. Reading the SE server's configuration showed why: CIMD was on, but only three policies existed, each allowing one exact metadata URL (PKCE required), and client.dev was in none of them. PingFederate rejects a CIMD request when no enabled policy matches.

GET /as/authorization.oauth2?client_id=https://client.dev/oauth/metadata.json&response_type=code&redirect_uri=https://client.dev/oauth/callback&code_challenge=E9Melho...&code_challenge_method=S256&state=x
Host: pf-local.example:9031

HTTP/2 400
400 - Invalid client_id

After adding a policy for client.dev only (one new policy and an opaque-token access token manager, with the owner's approval; the other three untouched), the same request on the SE server:

GET /as/authorization.oauth2?client_id=https://client.dev/oauth/metadata.json&response_type=code&redirect_uri=https://client.dev/oauth/callback&code_challenge=E9Melho...&code_challenge_method=S256&state=x
Host: pf.example

HTTP/2 302
location: https://client.dev/oauth/callback?error=server_error&error_description=There+are+no+authentication+methods+available+for+OAuth.&state=x

The 400 is gone, so PingFederate fetched and validated the document: the client is accepted. It then could not finish the sign-in and sent the error back to the callback the document lists. The cause is on the new access token manager I describe above, not on CIMD: it has no access-token mapping for the login adapter. The fix is one additive mapping that is waiting for the owner's approval, and nothing shared would be edited. Not captured yet: the sign-in and the code. A second PingFederate (pf-acme.example) does not advertise CIMD at all.

Mock What Muse's connector would do

This is scripted, not captured. Muse's connector does not do CIMD yet, so nothing below has been run. The app is Codex, using its real document from the other tab, so the shapes match. Codes, tokens and the PKCE values are samples. Codex needs only PKCE. ChatGPT's real document asks for private_key_jwt, so supporting ChatGPT would also mean fetching its key set and checking a signed assertion at the token step, which this walk-through does not show.

1. Muse advertises CIMD

GET /.well-known/oauth-authorization-server
Host: muse.example

200 OK
{
  "issuer": "https://muse.example",
  "authorization_endpoint": ".../connector/authorize",
  "token_endpoint": ".../connector/token",
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "client_id_metadata_document_supported": true   <- the one new line
}

2. The app sends the user to authorize

GET /connector/authorize
  ?client_id=https://chatgpt.com/oauth/codex/client.json
  &redirect_uri=http://127.0.0.1:1455/callback
  &response_type=code&scope=mcp
  &code_challenge=SAMPLE_CHALLENGE&code_challenge_method=S256&state=SAMPLE_STATE

Today Muse would answer 400 here, because that client_id is not registered. The new behaviour starts when the client_id is an HTTPS address.

3. Muse fetches and checks the document

GET /oauth/codex/client.json
Host: chatgpt.com
Accept: application/json

200 OK   (same body as the real capture)
CheckPass whenOn failure
AddressHTTPS, a real host, and the address does not resolve to a private or internal networkinvalid_client, nothing fetched
FetchAnswers in a few seconds, small body, JSONinvalid_client
Same addressDocument client_id equals the address it was fetched frominvalid_client
CallbackThe request's redirect_uri is one of redirect_uris. For a loopback address any port matches.400, no redirect (never send a code to an unlisted address)
Public clienttoken_endpoint_auth_method is noneinvalid_client
Allow-listThe host is one Muse trusts (a Muse setting, like PingFederate's CIMD policy)invalid_client

4. Sign-in goes ahead as it does today

With the app accepted, Muse sends the user to PingOne through the DaVinci flow with Protect, the same as for any connector. The user signs in and approves, then Muse redirects to the callback the document listed:

302 Location: http://127.0.0.1:1455/callback?code=SAMPLE_CODE&state=SAMPLE_STATE

5. The app swaps the code for a token

POST /connector/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SAMPLE_CODE
&client_id=https://chatgpt.com/oauth/codex/client.json
&redirect_uri=http://127.0.0.1:1455/callback
&code_verifier=SAMPLE_VERIFIER

200 OK
{ "access_token": "SAMPLE_TOKEN", "token_type": "Bearer", "scope": "mcp" }

No client secret anywhere. PKCE proves the app that asked is the app that gets the token, and the document's callback list proves the code went where its owner said.

6. Refusals worth showing

RequestMuse answers
client_id=http://chatgpt.com/... (not HTTPS)invalid_client
Document says a different client_id than its addressinvalid_client
redirect_uri=https://evil.example/cb, not in the document400 error page, no redirect
client_id on a host that resolves to 127.0.0.1 or a cluster addressinvalid_client, never fetched

What each side does

PingFederate's CIMD support follows an evolving draft specification, and its documentation warns later changes may not be backward compatible. See PingFederate CIMD.