mcp-oauth-doctor
by ubukhari
README.md
# mcp-oauth-doctor
The step where your MCP server's sign-in breaks.
```sh
npx --allow-git=root github:agentwares/mcp-oauth-doctor https://your-server.example/mcp
```
It walks a remote MCP server's OAuth chain from outside, the way a client does, with **no
credential and no client registration**, and prints:
- **the first broken step**;
- **whose it is**: the server, its identity provider, or a known client bug, with the issue;
- **the fix**, citing the section of the 2026-07-28 MCP authorization spec or the RFC that requires it;
- **a verdict per client** — claude.ai, Claude Code, ChatGPT, VS Code and Cursor — emulated from
each client's published requirements and the issues filed against it. No client is run.
The same check is a web page, [`/auth?url=`](https://agentwares-mcp-liveness.vercel.app/auth), and
an MCP tool, `mcp_liveness_check_auth` on `https://agentwares-mcp-liveness.vercel.app/mcp`.
## What it walks
1. **The 401.** One unauthenticated request, the way a dual-era client opens (`server/discover` at
2026-07-28, then `initialize`). It must answer 401 — clients start sign-in only on 401 — with a
`WWW-Authenticate: Bearer resource_metadata="…"` challenge, or serve the well-known document.
2. **RFC 9728 protected-resource metadata**: the URL in the challenge (a client that has one uses
it and stops there), then `/.well-known/oauth-protected-resource/<path>`, then the root. Its
`resource` must be the server's URL; it must name an authorization server.
3. **RFC 8414 or OpenID Connect discovery**, at the locations the spec lists, in its order. A
document found only where the spec does not tell clients to look is reported as such. The
`issuer` must be identical to the URL it was looked up for (RFC 8414 §3.3, which clients must
enforce since 2026-07-28), and `code_challenge_methods_supported` must list `S256` (clients
must refuse to proceed without it).
4. **How a client gets a client ID**: a client ID metadata document (preferred since 2026-07-28),
RFC 7591 dynamic registration (deprecated, kept for compatibility), or a pasted one.
5. **Each client's published rules**: which registration it uses, the redirect URIs it sends, and
known bugs that match this server — every rule with its source and date; a rule seen only in
an issue is labelled _observed in_, never presented as documented.
A metadata document behind a bot challenge (Cloudflare, Vercel, DataDome, …) is named: clients
fetch it from servers and CLIs, which cannot solve one.
## Options
```text
npx --allow-git=root github:agentwares/mcp-oauth-doctor <url> [--json] [--client <name>]
--json the full report as JSON (schema mcp-oauth-doctor/1), plus `next` and `also`
--client <name> judge for one client only: claude-ai, claude-code, chatgpt, vscode, cursor
```
Exit code 1 when sign-in is broken or nothing answered, 0 otherwise, 2 for a usage error. Usage
errors carry `code`, `cause`, `fix` and `retryable` (as JSON with `--json`).
## In CI
```yaml
- uses: agentwares/mcp-oauth-doctor@main
with:
url: https://your-server.example/mcp
# client: claude-ai # one client only
# fail-on: at_risk # broken (default) | at_risk | never
```
The Action runs the committed `dist/` from its own checkout (no npm install), writes the report to
the job summary, and sets `verdict`, `first-broken` and `report` (the JSON's path) as outputs.
## A recorded run
Recorded on 9 October 2026 with `pnpm record` (`scripts/record.mjs`): our own servers, live, and
two **synthetic** broken servers that the script serves on 127.0.0.1 for the length of the run
(`src/vendor/mcp-oauth-walk/synthetic.ts`). The synthetic ones are not anyone's real server. Our
own servers ask for no credential, so the doctor says there is no sign-in to check and offers the
free hourly watch. The synthetic servers have a sign-in, but nothing outside this machine can
reach 127.0.0.1, so their hand-off is the watch form left empty, for once a server is deployed,
then the nightly checks.
```text
## Our own server, live (2026-10-09)
$ npx --allow-git=root github:agentwares/mcp-oauth-doctor https://agentwares-mcp-liveness.vercel.app/mcp
mcp-oauth-doctor 0.1.0
https://agentwares-mcp-liveness.vercel.app/mcp · 2026-10-09 18:19 UTC · 0.8 s
Discovery only: no credential sent, no client registered.
The server answered without asking for a credential, so there is no sign-in to break.
Steps
info Unauthenticated request HTTP 200: the server accepts requests without a token, so there is no sign-in to check.
Re-check it every hour and email me when it stops answering: agentcheck's free watch, no account. https://agentwares-agentcheck.vercel.app/watch?target=https%3A%2F%2Fagentwares-mcp-liveness.vercel.app%2Fmcp&ref=mcp-oauth-doctor
(exit 0)
## Our own server, live (2026-10-09)
$ npx --allow-git=root github:agentwares/mcp-oauth-doctor https://agentwares-agentcheck.vercel.app/api/mcp
mcp-oauth-doctor 0.1.0
https://agentwares-agentcheck.vercel.app/api/mcp · 2026-10-09 18:19 UTC · 0.5 s
Discovery only: no credential sent, no client registered.
The server answered without asking for a credential, so there is no sign-in to break.
Steps
info Unauthenticated request HTTP 200: the server accepts requests without a token, so there is no sign-in to check.
Re-check it every hour and email me when it stops answering: agentcheck's free watch, no account. https://agentwares-agentcheck.vercel.app/watch?target=https%3A%2F%2Fagentwares-agentcheck.vercel.app%2Fapi%2Fmcp&ref=mcp-oauth-doctor
(exit 0)
## SYNTHETIC broken server ("issuer-mismatch"), served on 127.0.0.1 for this recording — not anyone's real server
$ npx --allow-git=root github:agentwares/mcp-oauth-doctor http://127.0.0.1:56268/mcp
mcp-oauth-doctor 0.1.0
http://127.0.0.1:56268/mcp · 2026-10-09 18:19 UTC · 0.0 s
Discovery only: no credential sent, no client registered.
Sign-in breaks at "Metadata `issuer` is identical to the advertised authorization server", and
the fix is the identity provider's.
Steps
ok Unauthenticated request returns 401 HTTP 401
ok WWW-Authenticate points at the resource metadata resource_metadata=http://127.0.0.1:56268/.well-known/oauth-protected-resource/mcp
info Challenge advertises the scope to request scope="mcp:tools"
ok Protected resource metadata is discoverable http://127.0.0.1:56268/.well-known/oauth-protected-resource/mcp (header)
ok PRM `resource` is this server's canonical URI resource=http://127.0.0.1:56268/mcp
ok PRM lists an authorization server http://127.0.0.1:56269/tenant1
info Scopes advertised mcp:tools
ok Authorization server metadata is discoverable http://127.0.0.1:56269/.well-known/oauth-authorization-server/tenant1; issuer=http://127.0.0.1:56269; token_e…
FAIL Metadata `issuer` is identical to the advertised au… issuer=http://127.0.0.1:56269 vs http://127.0.0.1:56269/tenant1
ok PKCE S256 advertised S256
ok Client registration client ID metadata documents and dynamic registration (registration_endpoint=http://127.0.0.1:56269/tenant1/r…
ok Refresh tokens supported authorization_code, refresh_token
ok Public clients can use the token endpoint none, client_secret_basic
First broken step: Metadata `issuer` is identical to the advertised authorization server
whose: the identity provider
what: issuer=http://127.0.0.1:56269 vs http://127.0.0.1:56269/tenant1
fix: Make `issuer` in the metadata exactly the URL listed in the protected-resource
metadata's authorization_servers (http://127.0.0.1:56269/tenant1), character for
character, or list exactly the issuer the metadata states.
spec: RFC 8414 §3.3; MCP 2026-07-28 "Authorization Server Metadata Discovery"
about "Issuer mismatch": https://agentwares-mcp-liveness.vercel.app/auth/issuer-mismatch?ref=mcp-oauth-doctor
Clients (emulated from each client's published requirements and known issues; no client was run)
claude.ai BLOCKED Metadata `issuer` is identical to the advertised authorization server: issuer=http://127.0.0.1:56269 vs http://127.0.0.…
Claude Code BLOCKED Metadata `issuer` is identical to the advertised authorization server: issuer=http://127.0.0.1:56269 vs http://127.0.0.…
ChatGPT BLOCKED Metadata `issuer` is identical to the advertised authorization server: issuer=http://127.0.0.1:56269 vs http://127.0.0.…
VS Code BLOCKED Metadata `issuer` is identical to the advertised authorization server: issuer=http://127.0.0.1:56269 vs http://127.0.0.…
Cursor BLOCKED Metadata `issuer` is identical to the advertised authorization server: issuer=http://127.0.0.1:56269 vs http://127.0.0.…
Redirect URIs to allow if your identity provider restricts them
claude.ai https://claude.ai/api/mcp/auth_callback
Claude Code http://localhost/callback http://127.0.0.1/callback
ChatGPT https://chatgpt.com/connector_platform_oauth_redirect
VS Code http://127.0.0.1:33418 https://vscode.dev/redirect
Cursor http://localhost:8787/callback https://www.cursor.com/agents/mcp/oauth/callback cursor://anysphere.cursor-mcp/oauth/callback
Once it is deployed, re-check it every hour and email me when a step breaks: agentcheck's free watch, no account — it follows the sign-in a new client follows, with these same rules. https://agentwares-agentcheck.vercel.app/watch?ref=mcp-oauth-doctor
Nightly, with history: mcpcheck Server Pro re-runs these sign-in checks against the server every night, with 90 days of history, and emails when one fails ($49 a server a month; the first run is free). https://agentwares-agentcheck.vercel.app/mcpcheck?ref=mcp-oauth-doctor
(exit 1)
## SYNTHETIC broken server ("second-issuer"), served on 127.0.0.1 for this recording — not anyone's real server
$ npx --allow-git=root github:agentwares/mcp-oauth-doctor http://127.0.0.1:56268/mcp --client claude-ai
mcp-oauth-doctor 0.1.0
http://127.0.0.1:56268/mcp · 2026-10-09 18:19 UTC · 0.0 s
Discovery only: no credential sent, no client registered.
Sign-in breaks at "claude.ai cannot sign in", and the fix is the server's (only claude.ai is
affected).
Steps
ok Unauthenticated request returns 401 HTTP 401
ok WWW-Authenticate points at the resource metadata resource_metadata=http://127.0.0.1:56268/.well-known/oauth-protected-resource/mcp
info Challenge advertises the scope to request scope="mcp:tools"
ok Protected resource metadata is discoverable http://127.0.0.1:56268/.well-known/oauth-protected-resource/mcp (header)
ok PRM `resource` is this server's canonical URI resource=http://127.0.0.1:56268/mcp
ok PRM lists an authorization server http://127.0.0.1:56269/gone, http://127.0.0.1:56269/tenant1
info Scopes advertised mcp:tools
ok Authorization server metadata is discoverable http://127.0.0.1:56269/.well-known/oauth-authorization-server/tenant1; issuer=http://127.0.0.1:56269/tenant1;…
ok PKCE S256 advertised S256
ok Client registration client ID metadata documents and dynamic registration (registration_endpoint=http://127.0.0.1:56269/tenant1/r…
ok Refresh tokens supported authorization_code, refresh_token
ok Public clients can use the token endpoint none, client_secret_basic
First broken step: claude.ai cannot sign in
whose: the server — claude.com/docs connectors: authentication (2026-10-08)
what: claude.ai uses only the first authorization_servers entry (http://127.0.0.1:56269/gone),
and it published no metadata; the one that answered is http://127.0.0.1:56269/tenant1
fix: Put the authorization server that serves metadata first in authorization_servers, or
serve metadata for the first one.
spec: MCP 2026-07-28 "Authorization Server Location"
docs: https://claude.com/docs/connectors/building/authentication
Clients (emulated from each client's published requirements and known issues; no client was run)
claude.ai BLOCKED claude.ai uses only the first authorization_servers entry (http://127.0.0.1:56269/gone), and it published no metadata; …
Redirect URIs to allow if your identity provider restricts them
claude.ai https://claude.ai/api/mcp/auth_callback
Once it is deployed, re-check it every hour and email me when a step breaks: agentcheck's free watch, no account — it follows the sign-in a new client follows, with these same rules. https://agentwares-agentcheck.vercel.app/watch?ref=mcp-oauth-doctor
Nightly, with history: mcpcheck Server Pro re-runs these sign-in checks against the server every night, with 90 days of history, and emails when one fails ($49 a server a month; the first run is free). https://agentwares-agentcheck.vercel.app/mcpcheck?ref=mcp-oauth-doctor
(exit 1)
```
## What it will not do
- It never sends a credential, never registers a client and never requests a token.
- It never retries. Each request has a 10-second timeout and the whole walk a 30-second budget.
- It makes one POST (two for a server on the older protocol revision) and GETs of public
discovery documents. It sends no telemetry and needs no account.
## Error messages people paste
One page each, saying what the message means, its three common causes and how to tell them
apart, with the public issues and their dates:
- [Authorization with the MCP server failed](https://agentwares-mcp-liveness.vercel.app/auth/authorization-with-the-mcp-server-failed)
- [Couldn't reach the MCP server](https://agentwares-mcp-liveness.vercel.app/auth/couldnt-reach-the-mcp-server)
- [Failed to discover OAuth metadata](https://agentwares-mcp-liveness.vercel.app/auth/failed-to-discover-oauth-metadata)
- [Dynamic Client Registration not supported](https://agentwares-mcp-liveness.vercel.app/auth/dynamic-client-registration-not-supported)
- [Incompatible auth server: does not support dynamic client registration](https://agentwares-mcp-liveness.vercel.app/auth/incompatible-auth-server-does-not-support-dynamic-client-registration)
- [Issuer mismatch](https://agentwares-mcp-liveness.vercel.app/auth/issuer-mismatch)
- [invalid_redirect_uri](https://agentwares-mcp-liveness.vercel.app/auth/invalid-redirect-uri)
- [Connection expired](https://agentwares-mcp-liveness.vercel.app/auth/connection-expired)
- [token exchange failed](https://agentwares-mcp-liveness.vercel.app/auth/token-exchange-failed)
## Sources, read 8 October 2026
- MCP authorization, revision 2026-07-28:
[main](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization),
[Authorization Server Discovery](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/authorization-server-discovery),
[Client Registration](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration),
[Security Considerations](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/security-considerations).
- claude.ai: [connectors, authentication](https://claude.com/docs/connectors/building/authentication).
- Claude Code: [MCP](https://code.claude.com/docs/en/mcp).
- ChatGPT: [Apps SDK, authentication](https://developers.openai.com/apps-sdk/build/auth).
- VS Code: [MCP configuration](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration),
[MCP extension guide](https://code.visualstudio.com/api/extension-guides/ai/mcp).
- Cursor: [MCP](https://cursor.com/docs/mcp); client ID metadata documents are not supported yet,
observed in [forum thread 148096](https://forum.cursor.com/t/mcp-oauth-cimd-support-plans-and-timelines/148096)
(opened 6 January 2026, staff reply 22 September 2026).
The per-client rules and known issues are in
[`src/vendor/mcp-oauth-walk/clients.ts`](src/vendor/mcp-oauth-walk/clients.ts), each with its
source and date.
## Keep checking
A check is one look. The doctor's last lines say how to keep looking:
- **Every hour, free:** [agentcheck's watch](https://agentwares-agentcheck.vercel.app/watch?ref=mcp-oauth-doctor)
takes an email address and the server's URL, with no account. For a server with a sign-in it
follows the sign-in a new client follows, with these same rules, and emails the step when one
breaks, then again when it is fixed. For a server without one, it emails when the server stops
answering or its tools change. A server on localhost cannot be watched from outside, so the
doctor offers the form empty, for once it is deployed.
- **Every night, with history:** [mcpcheck Server Pro](https://agentwares-agentcheck.vercel.app/mcpcheck?ref=mcp-oauth-doctor)
re-runs these sign-in checks with 90 days of history and emails when one fails ($49 a server a
month; the first run is free). The doctor offers it to a server with a sign-in, after the watch.
With `--json`, the watch is `next` and Server Pro is `also` (`null` for a server with no sign-in).
## Library
```sh
npm install github:agentwares/mcp-oauth-doctor
```
```ts
import { diagnose } from "mcp-oauth-doctor";
const report = await diagnose("https://your-server.example/mcp", { clients: ["claude-ai"] });
```
Web-standard (`fetch`, `URL`, `AbortSignal`), no runtime dependencies.
MIT, by agentwares.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues