# MCP Server Returns 401? How to Debug OAuth Discovery (RFC 9728, RFC 8414) for Claude, Cursor, and ChatGPT Connectors

> Source: <https://contextiq.trango-compute.com/blog/mcp-server-401-oauth-discovery-debug-protected-resource-metadata>
> Published: 2026-10-06 00:00:00+00:00

# 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 <token>` to the MCP endpoint and repeat the`initialize` call. A`200` with a`serverInfo` object means auth works.
- Follow with `tools/list` . If`initialize` succeeds but tools are empty, the problem is scope or tool registration, not authentication.
- If the token is accepted by the authorization server but rejected by the MCP server, check the `aud` claim against the`resource` value from step 2. Audience mismatches are the usual cause.

## Run the Whole Chain in One Request

Doing this by hand is useful once. For regular checks, [MCP Inspector](https://contextiq.trango-compute.com/mcp-inspector) runs the handshake against a URL, detects OAuth-protected servers from the `WWW-Authenticate` challenge and RFC 9728 metadata, and enumerates `tools/list` where the server allows it. It tests from outside your network with no cookies, which is exactly the position a hosted client is in, so it catches the proxy, redirect, and catch-all failures that don't show up when you test from your own machine.

## Debugging Order Cheat Sheet

1. Is the response a `401` with a`WWW-Authenticate: Bearer resource_metadata=...` header?
2. Is `resource_metadata` an absolute`https://` URL?
3. Does it return JSON (not HTML) with a `resource` matching your MCP URL?
4. Does `authorization_servers[0]` serve RFC 8414 or OIDC metadata at the correct path?
5. Does `issuer` match exactly, and does`code_challenge_methods_supported` include`S256` ?
6. Is there a `registration_endpoint` , or a documented client ID for hosted clients?
7. Do the registered redirect URIs include each hosted client's callback?
8. Does the token's `aud` match the`resource` value?

Work down the list and stop at the first "no". That is your bug.

Follow Trango Compute on LinkedIn

We post updates on new tools, context engineering patterns, and LLM cost research.

[Follow on LinkedIn](https://www.linkedin.com/company/trango-compute)
