Summary
When mcp-remote runs as a stdio proxy (e.g. from Claude Desktop via npx mcp-remote <url>), the OAuth finishAuth code path appears to POST the authorization-code exchange to the resource URL rather than to the token_endpoint advertised in the discovered authorization-server metadata. The standalone mcp-remote-client binary against the same server, on the same network, with the same flags, correctly POSTs to the configured token_endpoint and succeeds.
The symptom is a fatal HTTP 404: ... Raw body: 404 page not found returned from executeTokenRequest. The plaintext 404 page not found body is Go's net/http stdlib default — i.e. the request is reaching a Go-based server that doesn't have a token endpoint at the path mcp-remote chose, rather than the actual OAuth token endpoint (which is on a different host entirely and would return a JSON error if reached).
Related: modelcontextprotocol/typescript-sdk#744 (different but adjacent metadata-discovery issue in the underlying SDK).
Environment
wrangler-tail.txt
mcp-remote-bug-debug.log
mcp-remote: 0.1.38 from npm (internally self-reports as 0.1.37)
- Reproduced on macOS (Sequoia) with Claude Desktop, Node.js 22.x
- Authorization server: Okta custom AS (
/oauth2/default)
- MCP server: custom Go-based HTTP backend behind a Cloudflare Worker that serves the OAuth Protected Resource Metadata, Authorization Server Metadata, and forwards
POST /oauth2/v1/token to Okta
- Client: PKCE, public client (
token_endpoint_auth_method: "none"), static client info passed via --static-oauth-client-info
Configuration in use (sanitized)
{
"mcpServers": {
"Example MCP Server": {
"command": "npx",
"args": [
"-y",
"mcp-remote@0.1.38",
"https://my-auth-proxy.example.com/mcp",
"--transport", "http-only",
"--callback-port", "16120",
"--debug",
"--static-oauth-client-info",
"{\"client_id\":\"REDACTED_CLIENT_ID\",\"redirect_uris\":[\"http://localhost:16120/oauth/callback\"],\"token_endpoint_auth_method\":\"none\",\"grant_types\":[\"authorization_code\",\"refresh_token\"],\"response_types\":[\"code\"],\"scope\":\"my:scope openid\"}",
"--static-oauth-client-metadata",
"{\"scope\":\"my:scope openid\"}"
]
}
}
}
Protected Resource Metadata served at https://my-auth-proxy.example.com/.well-known/oauth-protected-resource:
{
"resource": "https://my-auth-proxy.example.com/mcp",
"authorization_servers": ["https://my-auth-proxy.example.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["my:scope", "openid"]
}
Authorization Server Metadata served at https://my-auth-proxy.example.com/.well-known/oauth-authorization-server:
{
"issuer": "https://my-auth-proxy.example.com",
"authorization_endpoint": "https://example.okta.com/oauth2/default/v1/authorize",
"token_endpoint": "https://my-auth-proxy.example.com/oauth2/v1/token",
"jwks_uri": "https://example.okta.com/oauth2/default/v1/keys",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"scopes_supported": ["openid", "my:scope"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
Reproduction
- Launch Claude Desktop with the config above.
- mcp-remote probes the MCP URL, gets
401 with WWW-Authenticate: Bearer resource_metadata="...".
- mcp-remote fetches the PRM, discovers
authorization_servers[0].
- mcp-remote fetches the AS metadata, parses it. Debug log confirms it stored the correct
token_endpoint.
- Browser opens to Okta
/v1/authorize, user signs in, code returns to local callback.
- mcp-remote enters
finishAuth, which calls executeTokenRequest.
- The POST goes somewhere other than the configured
token_endpoint. The response body is 404 page not found (Go default).
- Fatal error; stdio proxy exits.
Importantly: the proxy server (Cloudflare Worker) configured as token_endpoint never receives a POST. I verified this with wrangler tail during the failure. The Worker received only the GET requests to /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource. Zero POSTs.
Evidence
mcp-remote's debug log knows the right token endpoint
From ~/.mcp-auth/mcp-remote-0.1.37/<hash>_debug.log:
[timestamp] Successfully fetched authorization server metadata
{"issuer":"https://my-auth-proxy.example.com","scopes_supported":["openid","my:scope"]}
[timestamp] Discovered authorization server: https://my-auth-proxy.example.com
[timestamp] authorizationServerMetadata: {
"issuer":"https://my-auth-proxy.example.com",
"authorization_endpoint":"https://example.okta.com/oauth2/default/v1/authorize",
"token_endpoint":"https://my-auth-proxy.example.com/oauth2/v1/token",
...
}
The token_endpoint is logged verbatim from the metadata — pointing at the proxy.
…but the POST never reaches that endpoint
wrangler tail during the failing run, complete (only these entries):
GET https://my-auth-proxy.example.com/.well-known/oauth-protected-resource - 200
GET https://my-auth-proxy.example.com/.well-known/oauth-authorization-server - 200
GET https://my-auth-proxy.example.com/mcp - 401 (initial probe)
POST https://my-auth-proxy.example.com/mcp - 401 (initialize fallback test)
GET https://my-auth-proxy.example.com/.well-known/oauth-authorization-server - 200
No POST /oauth2/v1/token. The proxy that the metadata advertised as the token endpoint was never contacted for the token exchange.
And the resulting error
Authorization error: ServerError: HTTP 404: Invalid OAuth error response:
SyntaxError: Unexpected non-whitespace character after JSON at position 4
(line 1 column 5). Raw body: 404 page not found
at parseErrorResponse (.../mcp-remote/dist/chunk-65X3S4HB.js:18531:12)
at async executeTokenRequest (.../mcp-remote/dist/chunk-65X3S4HB.js:18884:11)
at async authInternal (.../mcp-remote/dist/chunk-65X3S4HB.js:18596:21)
at async auth (.../mcp-remote/dist/chunk-65X3S4HB.js:18536:12)
at async StreamableHTTPClientTransport.finishAuth (.../mcp-remote/dist/chunk-65X3S4HB.js:19809:20)
at async connectToRemoteServer (.../mcp-remote/dist/chunk-65X3S4HB.js:20567:9)
at async runProxy (.../mcp-remote/dist/proxy.js:154:29)
The 404 page not found plaintext body is Go's net/http default. Okta would respond with a JSON error body — so whatever URL the POST landed on, it wasn't the configured token_endpoint (which is on the Cloudflare Worker → Okta path).
mcp-remote-client (runClient) succeeds against the same server
Running the standalone client with identical flags against the same MCP URL:
rm -rf ~/.mcp-auth
npx -p mcp-remote@latest mcp-remote-client \
https://my-auth-proxy.example.com/mcp \
--transport http-only \
--callback-port 16120 \
--static-oauth-client-info '...' \
--static-oauth-client-metadata '...'
…completes the token exchange successfully. The Worker logs show POST /oauth2/v1/token, Okta returns 200 with an access token, and mcp-remote proceeds to JSON-RPC initialize.
So the bug is specific to the runProxy → finishAuth path; runClient → Client.connect does not exhibit it.
Suspected cause
Without diving into the bundled chunk-65X3S4HB.js, the call sites suggest that StreamableHTTPClientTransport.finishAuth either:
- doesn't pass the previously-discovered
authorizationServerMetadata to the token-exchange call, or
- re-derives the token endpoint from the resource URL when no in-memory metadata is available at that point in the proxy flow.
A useful diff would be to compare:
runClient → connectToRemoteServer → Client.connect → … → executeTokenRequest (works)
runProxy → connectToRemoteServer → finishAuth → auth → authInternal → executeTokenRequest (fails)
…and check whether authorizationServerMetadata is in scope at the executeTokenRequest call site for both paths.
Workaround
I worked around it by putting a Cloudflare Worker in front of the MCP server and adding a defensive catch-all token handler: any POST with Content-Type: application/x-www-form-urlencoded and grant_type= in the body is forwarded to the real Okta token endpoint, regardless of the request path. This catches the misrouted token exchange and lets the flow complete. Happy to share the Worker code if it's useful for a regression test.
Attached
mcp-remote-bug-debug.log — sanitized mcp-remote debug log from a failing run
wrangler-tail.txt — sanitized Cloudflare Worker logs from the same run
Both confirm: AS metadata correctly discovered → POST never arrives at the configured token_endpoint → 404 from a different host returned to executeTokenRequest.
Summary
When
mcp-remoteruns as a stdio proxy (e.g. from Claude Desktop vianpx mcp-remote <url>), the OAuthfinishAuthcode path appears to POST the authorization-code exchange to the resource URL rather than to thetoken_endpointadvertised in the discovered authorization-server metadata. The standalonemcp-remote-clientbinary against the same server, on the same network, with the same flags, correctly POSTs to the configuredtoken_endpointand succeeds.The symptom is a fatal
HTTP 404: ... Raw body: 404 page not foundreturned fromexecuteTokenRequest. The plaintext404 page not foundbody is Go'snet/httpstdlib default — i.e. the request is reaching a Go-based server that doesn't have a token endpoint at the path mcp-remote chose, rather than the actual OAuth token endpoint (which is on a different host entirely and would return a JSON error if reached).Related: modelcontextprotocol/typescript-sdk#744 (different but adjacent metadata-discovery issue in the underlying SDK).
Environment
wrangler-tail.txt
mcp-remote-bug-debug.log
mcp-remote:0.1.38from npm (internally self-reports as0.1.37)/oauth2/default)POST /oauth2/v1/tokento Oktatoken_endpoint_auth_method: "none"), static client info passed via--static-oauth-client-infoConfiguration in use (sanitized)
{ "mcpServers": { "Example MCP Server": { "command": "npx", "args": [ "-y", "mcp-remote@0.1.38", "https://my-auth-proxy.example.com/mcp", "--transport", "http-only", "--callback-port", "16120", "--debug", "--static-oauth-client-info", "{\"client_id\":\"REDACTED_CLIENT_ID\",\"redirect_uris\":[\"http://localhost:16120/oauth/callback\"],\"token_endpoint_auth_method\":\"none\",\"grant_types\":[\"authorization_code\",\"refresh_token\"],\"response_types\":[\"code\"],\"scope\":\"my:scope openid\"}", "--static-oauth-client-metadata", "{\"scope\":\"my:scope openid\"}" ] } } }Protected Resource Metadata served at
https://my-auth-proxy.example.com/.well-known/oauth-protected-resource:{ "resource": "https://my-auth-proxy.example.com/mcp", "authorization_servers": ["https://my-auth-proxy.example.com"], "bearer_methods_supported": ["header"], "scopes_supported": ["my:scope", "openid"] }Authorization Server Metadata served at
https://my-auth-proxy.example.com/.well-known/oauth-authorization-server:{ "issuer": "https://my-auth-proxy.example.com", "authorization_endpoint": "https://example.okta.com/oauth2/default/v1/authorize", "token_endpoint": "https://my-auth-proxy.example.com/oauth2/v1/token", "jwks_uri": "https://example.okta.com/oauth2/default/v1/keys", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "scopes_supported": ["openid", "my:scope"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["none"] }Reproduction
401withWWW-Authenticate: Bearer resource_metadata="...".authorization_servers[0].token_endpoint./v1/authorize, user signs in, code returns to local callback.finishAuth, which callsexecuteTokenRequest.token_endpoint. The response body is404 page not found(Go default).Importantly: the proxy server (Cloudflare Worker) configured as
token_endpointnever receives a POST. I verified this withwrangler tailduring the failure. The Worker received only the GET requests to/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource. Zero POSTs.Evidence
mcp-remote's debug log knows the right token endpoint
From
~/.mcp-auth/mcp-remote-0.1.37/<hash>_debug.log:The
token_endpointis logged verbatim from the metadata — pointing at the proxy.…but the POST never reaches that endpoint
wrangler tailduring the failing run, complete (only these entries):No
POST /oauth2/v1/token. The proxy that the metadata advertised as the token endpoint was never contacted for the token exchange.And the resulting error
The
404 page not foundplaintext body is Go'snet/httpdefault. Okta would respond with a JSON error body — so whatever URL the POST landed on, it wasn't the configuredtoken_endpoint(which is on the Cloudflare Worker → Okta path).mcp-remote-client(runClient) succeeds against the same serverRunning the standalone client with identical flags against the same MCP URL:
…completes the token exchange successfully. The Worker logs show
POST /oauth2/v1/token, Okta returns 200 with an access token, and mcp-remote proceeds to JSON-RPC initialize.So the bug is specific to the
runProxy→finishAuthpath;runClient→Client.connectdoes not exhibit it.Suspected cause
Without diving into the bundled
chunk-65X3S4HB.js, the call sites suggest thatStreamableHTTPClientTransport.finishAutheither:authorizationServerMetadatato the token-exchange call, orA useful diff would be to compare:
runClient → connectToRemoteServer → Client.connect → … → executeTokenRequest(works)runProxy → connectToRemoteServer → finishAuth → auth → authInternal → executeTokenRequest(fails)…and check whether
authorizationServerMetadatais in scope at theexecuteTokenRequestcall site for both paths.Workaround
I worked around it by putting a Cloudflare Worker in front of the MCP server and adding a defensive catch-all token handler: any
POSTwithContent-Type: application/x-www-form-urlencodedandgrant_type=in the body is forwarded to the real Okta token endpoint, regardless of the request path. This catches the misrouted token exchange and lets the flow complete. Happy to share the Worker code if it's useful for a regression test.Attached
mcp-remote-bug-debug.log— sanitized mcp-remote debug log from a failing runwrangler-tail.txt— sanitized Cloudflare Worker logs from the same runBoth confirm: AS metadata correctly discovered → POST never arrives at the configured
token_endpoint→ 404 from a different host returned toexecuteTokenRequest.