MCP Server Returns 401? How to Debug OAuth Discovery (RFC 9728, RFC 8414) for Claude, Cursor, and ChatGPT Connectors Remote MCP server deployments most commonly fail with a 401 because of OAuth discovery problems rather than token problems, according to a debugging guide covering the four-step discovery chain defined by RFC 9728 protected resource metadata and RFC 8414 authorization server metadata. The guide instructs operators to verify that the 401 response carries a WWW-Authenticate header with an absolute resource_metadata URL, that the protected resource metadata returns JSON with a matching resource value and authorization_servers list, and that the authorization server metadata exposes authorization_endpoint, token_endpoint and registration_endpoint before the OAuth authorization code flow begins. Common failures include missing WWW-Authenticate headers, http:// or relative resource_metadata URLs, 403 responses instead of 401, SPA catch-all routes returning HTML with a 200 status, and resource values that mismatch the connected server URL. MCP Server Returns 401? How to Debug OAuth Discovery RFC 9728, RFC 8414 for Claude, Cursor, and ChatGPT Connectors Debug a failing MCP server OAuth flow for Claude, Cursor and ChatGPT: WWW-Authenticate, RFC 9728 and RFC 8414 metadata, PKCE and dynamic client registration. You deployed a remote MCP server, added it to Claude, Cursor, or a ChatGPT connector, and the client says "authorization failed", "could not connect", or just spins. The server logs show a 401 and nothing else. This is the most common failure in remote MCP deployments, and it is almost always a discovery problem, not a token problem. Before any token is issued, an MCP client has to discover where to authenticate. That discovery is a chain of four HTTP responses. If any link in the chain is wrong, the client gives up with a vague error. This post walks the chain in order, with a curl command for each link and the failure modes we see most often. The Discovery Chain 1. Client sends an unauthenticated request to the MCP endpoint. 2. Server answers 401 with a WWW-Authenticate header that points to protected resource metadata RFC 9728 . 3. Client fetches that metadata, which names one or more authorization servers . 4. Client fetches the authorization server's metadata RFC 8414, or OpenID Connect discovery to find the authorization endpoint , token endpoint , and, if the client wants to self-register, the registration endpoint . Only after step 4 does the OAuth authorization code flow begin. Steps 1–4 involve no user and no credentials, which means you can test every one of them from a terminal. Step 1: Does the 401 Carry a Usable Challenge? curl -si -X POST https://mcp.example.com/mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' You want a 401 and a header like: WWW-Authenticate: Bearer resource metadata="https://mcp.example.com/.well-known/oauth-protected-resource" Common failures: | Symptom | Cause | Fix | |---|---|---| | 401 with no WWW-Authenticate | Auth middleware rejects before your challenge logic runs | Emit the header from the same layer that returns the 401 | | resource metadata is http:// | TLS terminates at a proxy and the app builds URLs from the internal scheme | Trust X-Forwarded-Proto or hard-code the public origin | | resource metadata is a relative path | Header built from a route name | Use an absolute URL | | 403 instead of 401 | Gateway treats missing credentials as forbidden | Return 401 for missing or invalid credentials; reserve 403 for insufficient scope | | 200 with an HTML login page | A web-app auth redirect is sitting in front of the API route | Exempt the MCP path from browser redirects | Step 2: Does the Protected Resource Metadata Resolve? curl -si https://mcp.example.com/.well-known/oauth-protected-resource Expect 200 , content-type: application/json , and a body like: { "resource": "https://mcp.example.com/mcp", "authorization servers": "https://auth.example.com" } Failures to look for: - SPA catch-all. A single-page app or CDN rule returns index.html with 200 for every unknown path. The status looks fine; the body is HTML, so JSON parsing fails. Always check the content-type , not just the status. - Path mismatch. If your resource lives at a path /mcp , RFC 9728 metadata can be served path-suffixed at /.well-known/oauth-protected-resource/mcp . The URL in your resource metadata parameter must point to wherever you actually serve it. - resource doesn't match the server URL. Clients compare the metadata's resource to the URL they connected to and reject a mismatch. A trailing slash or a different hostname internal vs public is enough. - Redirects. A 301 from http to https , or from a bare domain to www , on the well-known path breaks clients that don't follow redirects on discovery requests. - Empty authorization servers . The array must name at least one issuer. Step 3: Does the Authorization Server Publish Metadata? Take the first entry in authorization servers and fetch both discovery documents. They are two separate documents and a spec-compliant issuer may serve either or both: curl -si https://auth.example.com/.well-known/oauth-authorization-server curl -si https://auth.example.com/.well-known/openid-configuration If your issuer has a path component https://auth.example.com/tenant1 , the RFC 8414 path is inserted between the well-known prefix and the path: /.well-known/oauth-authorization-server/tenant1 . OpenID Connect discovery instead appends : /tenant1/.well-known/openid-configuration . Mixing these up is a classic multi-tenant bug Keycloak realms and Auth0 custom domains both hit it . Check the response for: - issuer exactly equal to the URL you fetched from, with no trailing-slash difference. - authorization endpoint and token endpoint present and reachable. - code challenge methods supported containing S256 . MCP clients use PKCE, and a client that finds no S256 support may refuse to proceed. If PKCE is new to you, see What is PKCE https://contextiq.trango-compute.com/blog/pkce-oauth2-authorization-code-flow . For the difference between an OpenID Connect provider and bare OAuth 2.0 here, and why it matters, see Is Your MCP Server Using OpenID Connect or Just OAuth 2.0 https://contextiq.trango-compute.com/blog/mcp-server-openid-connect-oauth2-detection . Step 4: Can the Client Register Itself? Hosted clients like Claude and ChatGPT connectors typically don't have a pre-provisioned client ID for your server. They rely on dynamic client registration RFC 7591 via a registration endpoint in the authorization server metadata, unless you give them a client ID manually. If registration endpoint is absent, the client has two options: use a client ID you supplied in its connector settings, or fail. Many "authorization failed" reports end here. Either add dynamic registration at the authorization server, or document the client ID and redirect URI each client must use. Also verify the redirect URIs your authorization server allows. Each client has its own callback URL, and an allowlist containing only localhost will pass your manual tests and fail every hosted client. After Discovery: Quick Sanity Checks Once the four steps return clean responses and you can obtain a token, confirm the server accepts it: - Send the token as Authorization: Bearer