How to Fix Remote MCP Server OAuth 401 Errors in Claude, ChatGPT, and Codex

What broke, and why

You paste your remote MCP server URL into Claude, ChatGPT, or Codex, and instead of a sign-in prompt you get a 401, a failed connection, or a server that connects with no tools. The sign-in flow never starts, because it is triggered by one specific response shape, and most broken servers get that shape wrong.

The MCP authorization specification says a protected server must answer an unauthenticated request with HTTP 401 and a WWW-Authenticate header that points at its protected resource metadata document (RFC 9728). That header is the trigger: the client reads the metadata, finds the authorization server, and opens the browser flow. Authorization is optional in the spec overall, but HTTP servers that use it SHOULD follow this flow, and the 2026-07-28 revision tightened discovery and made Client ID Metadata Documents (CIMD) the recommended way for a client to identify itself.

A field report from the levl team, who launched three remote MCP servers on October 15, 2026 and tested them against claude.ai, Claude Code, Codex CLI, and ChatGPT developer mode, found that almost everything that broke was in OAuth discovery and client registration. Their write-up names the exact traps, and each one maps to a check you can run with curl.

Quick answer

Return HTTP 401 with a WWW-Authenticate header pointing at your RFC 9728 protected resource metadata, serve that metadata document, and treat ChatGPT as a public PKCE client instead of implementing private_key_jwt.

The fix

First, make the protected endpoint return a real 401 with the metadata pointer, exactly like this:

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="tools:read"

Second, serve the metadata document at that URL, with a resource field set to your server’s canonical URL and at least one entry in authorization_servers:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["tools:read"]
}

Third, handle the ChatGPT CIMD quirk. ChatGPT’s client metadata document declares token_endpoint_auth_method as private_key_jwt, which looks like it demands RFC 7523 JWT client authentication. The same document also lists none in token_endpoint_auth_methods_supported, and the levl team confirmed that treating ChatGPT as a public client using PKCE works without implementing private_key_jwt. Do not build JWT client auth just for this declaration.

Fourth, if you rotate refresh tokens, keep the used ones. The levl servers issue refresh tokens that last 30 days and rotate on every use, and a cleanup job that deleted used tokens once they expired broke reuse detection: after about 30 days, a replayed token looked like an ordinary bad token instead of triggering revocation, and the stolen connection stayed alive. Retain exchanged refresh tokens until expiry plus a grace window, so a replay is still recognizable. For a local stdio server, none of this applies: stdio servers take credentials from the environment, and the spec says they SHOULD NOT use this OAuth flow. For the stdio setup, see our MCP server tutorial.

Pitfalls that make the fix look broken

A 200 response kills the flow silently. As the levl team put it, “If an unauthenticated request gets a 200 and a tool list instead, the client stays anonymous.” Claude is stricter than the spec here: it needs a real 401 and ignores a WWW-Authenticate header on a 200 response. One URL cannot serve public tools to everyone and account tools to signed-in users, so split them: the main URL always returns 401 without a token, and anonymous tools live on a separate path such as /mcp/public.

A mismatched resource parameter is the second silent killer. Clients must send the RFC 8707 resource parameter on both the authorization and token requests, set to the server’s canonical URI, and the authorization server must see the same value your metadata document advertises. A trailing slash or http/https mismatch is enough to fail token validation.

A stale client cache is the third. After fixing the server, remove the old grant before retrying: in Claude Code run /mcp and re-authenticate, as the Claude Code MCP documentation describes, and for mcp-remote setups clear its cached credentials. Otherwise the client keeps replaying the old token against the fixed server and you will conclude the fix did not work. The MCP 2026-07-28 stateless spec migration guide covers the related discovery changes in the same revision, and the Claude Code MCP setup guide shows the client-side commands for adding a remote server.

How to verify the fix

Run these two curl commands with no token at all:

curl -i https://mcp.example.com/mcp
curl -s https://mcp.example.com/.well-known/oauth-protected-resource/mcp

The first must return 401 with a WWW-Authenticate header naming the metadata URL, and the second must return JSON with your resource and at least one authorization_servers entry. Then connect from the client and run one read-only tool: a tool result with real data proves the whole chain works end to end.

When this does not apply

This article does not apply to local stdio servers, to servers that authenticate with a static API key header, or to servers you intend to keep fully public. It also does not cover a 403 after a successful sign-in: the token is valid but lacks the scope the tool needs, which is a scope problem at the authorization server, not a 401 discovery problem. If the client shows a sign-in prompt but the browser redirects to the wrong authorization server, the fault is in the authorization_servers list in your metadata document, not in the 401 itself.

Frequently asked questions

Why does my remote MCP server connect in one client but fail with 401 in another?

Each client reads the 401 challenge slightly differently. Claude needs a real 401 and ignores a WWW-Authenticate header on a 200 response, ChatGPT follows its CIMD document and may declare private_key_jwt, and Codex is the most forgiving of the three. Fix the server side once, with a spec-shaped 401 and metadata, and all three usually start working.

Can one MCP URL serve both public tools and signed-in tools?

No, not safely. An unauthenticated request that returns 200 with a tool list tells the client the server is open, so it never starts OAuth. Keep the protected tools on the main URL, which always returns 401 without a token, and put the anonymous tools on a separate path such as /mcp/public.

Do I need to implement private_key_jwt for ChatGPT?

No. ChatGPT's CIMD document declares token_endpoint_auth_method as private_key_jwt, but the same document also lists none in token_endpoint_auth_methods_supported. Treating ChatGPT as a public client with PKCE works, as the levl team verified across three production servers in October 2026.

My server returns 401, but the client still never shows a sign-in prompt. What else can be wrong?

Check the metadata document itself. Fetch the URL named in the WWW-Authenticate header with curl: it must return JSON with a resource field matching your server URL and at least one entry in authorization_servers. If that document 404s or names the wrong authorization server, the client has nowhere to send the user.

TP
ToolPilot Team

We install, break, and fix automation tools so you don't have to. Every tutorial on ToolPilot is tested hands-on before publishing.

Keep reading