Skip to content

Proxy mode (runProxy)POSTs token exchange to resource URL instead of token_endpoint from discovered AS metadata #270

Description

@acitatorq

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

  1. Launch Claude Desktop with the config above.
  2. mcp-remote probes the MCP URL, gets 401 with WWW-Authenticate: Bearer resource_metadata="...".
  3. mcp-remote fetches the PRM, discovers authorization_servers[0].
  4. mcp-remote fetches the AS metadata, parses it. Debug log confirms it stored the correct token_endpoint.
  5. Browser opens to Okta /v1/authorize, user signs in, code returns to local callback.
  6. mcp-remote enters finishAuth, which calls executeTokenRequest.
  7. The POST goes somewhere other than the configured token_endpoint. The response body is 404 page not found (Go default).
  8. 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 runProxyfinishAuth path; runClientClient.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions