A static page that runs a real OAuth authorization code flow against an MCP server, using a
Client ID Metadata Document (CIMD, SEP-991 / draft-ietf-oauth-client-id-metadata-document)
as the client identity.
No build step, no backend. Drop it on GitHub Pages.
- Push this folder to a repo, serve it at
https://dtinth.github.io/cimdemo/. - That is the only URL that works out of the box —
client-v1.jsonand the redirect URI are both hardcoded to it. To host elsewhere, editclient-v1.jsonsoclient_id,client_uriandredirect_urisall point at your own deployment.
client_id must equal the URL the document is served from, byte for byte. A trailing
slash or a www will get you rejected by a correct authorization server.
Enter your server's URL and open the "Client identity and scope" section to point client_id
at whichever metadata document you want validated. Discovery output shows which well-known URL
actually answered, so you can see whether your server matches the shapes real clients expect.
GitHub Pages sits behind a CDN, and authorization servers cache metadata documents per
RFC 9111. Changing client-v1.json in place means waiting out both. Copy it to
client-v2.json instead — a new client_id bypasses every cache.
The interesting tests are the ones that should fail:
client_idinside the document does not match the URL it was fetched from- a
redirect_urithat differs from the request only by port — exact matching is required - a wildcard or
http://redirect URI - the document URL returning 404, or
text/html, or a redirect - a missing or mismatched
resourceparameter (RFC 8707) statethat does not come back unchanged
The page checks state itself and blocks the exchange when it does not match.
Verified by probing client_id_metadata_document_supported in the authorization server
metadata:
| Server | CIMD | Browser token exchange |
|---|---|---|
https://mcp.notion.com/mcp |
yes | CORS ok |
https://mcp.linear.app/mcp |
yes | CORS ok |
https://mcp.canva.com/mcp |
yes | CORS ok |
https://mcp.cloudflare.com/mcp |
yes | CORS ok |
https://huggingface.co/mcp |
yes | CORS ok |
https://mcp.sentry.dev/mcp |
yes | no CORS — use the curl fallback |
https://mcp.paypal.com/mcp |
declared false | — |
https://mcp.webflow.com/sse |
declared false | — |
https://mcp.stripe.com |
not declared | — |
https://mcp.asana.com/sse |
not declared | — |
Discovery shapes vary, which is why the page tries several candidates and logs each attempt:
Figma's authorization server lives on a different origin, Stripe's metadata is path-suffixed
under access.stripe.com, GitHub's is nested under /login/oauth.
Completing a flow gives the page a live access token for a real account. Tokens stay in memory
and sessionStorage, are redacted in the UI until you click through, and are gone when the tab
closes. Nothing is written to localStorage and nothing leaves the browser except the token
request itself.