MCP OAuth loopback redirect uses 127.0.0.1 instead of localhost — breaks providers (e.g. Salesforce) that only allow localhost callback URLs
Related to #69326 and #66511 (both closed as not planned / stale), but with a concrete, current real-world case that isn't a hypothetical headless-server scenario.
Problem: claude mcp login / /mcp builds its OAuth loopback redirect as http://127.0.0.1:<port>/callback. Several major OAuth providers reject IP-literal HTTP callback URLs outright but explicitly allow the literal string localhost as a security exception (HTTPS otherwise required). Salesforce is one: their documentation for setting up an External Client App for their Hosted MCP Servers specifies http://localhost:<port>/callback as the required callback format, and their connected-app validator throws "Cannot be an HTTP URL" if you try to register 127.0.0.1 instead.
Since Claude Code always sends 127.0.0.1 with no way to override the host, it's impossible to complete claude mcp login against a Salesforce-hosted MCP server (or any other provider with the same localhost-only exception), even with a correctly configured client ID/secret and matching port. Hit this directly trying to connect Claude Code to a Salesforce sandbox MCP server for internal testing.
Prior art: Mastra's mastracode CLI just merged this — https://github.com/mastra-ai/mastra/pull/19467 — shipping an oauth.callbackPort config shorthand that synthesizes http://localhost:<port>/callback. Doesn't need the full custom-URL flexibility of #66511, just swapping the hardcoded host from 127.0.0.1 to localhost (or making it configurable) would unblock this.
Repro:
claude mcp add --transport http <name> <salesforce-mcp-url> --client-id <id> --client-secret --callback-port 8765claude mcp login <name>- Provider rejects the authorize request with
redirect_uri_mismatch, because the only registerable callback (http://localhost:8765/callback) never matches what Claude Code actually sends (http://127.0.0.1:8765/callback).
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Hit this too, on a different provider. Adding a few data points that aren't in the issue yet: the exact version this regressed in, an internal inconsistency in the same build, and the fact that it breaks a plugin shipped through Anthropic's own marketplace.
Regression range: 2.1.229
The host flipped between 2.1.228 and 2.1.229. Grepping the redirect-URI builder out of the native install bundles (
~/.local/share/claude/versions/):So this isn't long-standing behaviour — anything that authorized successfully on ≤2.1.228 and needs a fresh consent on ≥2.1.229 breaks.
It breaks the official Slack plugin
slack@claude-plugins-officialv1.2.0 (installed from theclaude-plugins-officialmarketplace; plugin authored by Slack) pins a static, pre-registered OAuth client rather than using DCR:Because the client ID is fixed, the allowlist lives in Slack's app config and was registered as
http://localhost:3118/callback. Slack string-matchesredirect_uri(no RFC 8252 §7.3 loopback normalisation), so on 2.1.229 authorization dead-ends at:This affects every user of that plugin who hasn't already authorized, which makes it a bit more than a per-provider edge case.
Servers using client-id-metadata-document / DCR are unaffected, since the redirect URI is registered at authorize time — e.g. Linear's cached entry (
clientId: https://claude.ai/oauth/claude-code-client-metadata) still holds"redirectUri": "http://localhost:3118/callback"and keeps working.2.1.229 is internally inconsistent
The same build still uses
localhoston the OIDC/IdP login path:Worth aligning these whichever way the fix goes.
No host override exists
Confirming the issue's point: the adjacent env var is port-only, so there's no way to work around this via config.
Workaround (confirmed working)
Authorize once on a 2.1.228 binary, then go back to current:
The token survives the upgrade because refresh grants don't carry
redirect_uri(RFC 6749 §6). Only useful while an old version is still on disk, though — the version pruner will eventually remove it.On the fix
Flipping the hardcode back to
localhostjust re-breaks the other direction — #82096 and #42765 asked for127.0.0.1precisely because some IdPs allowlist only the IP literal. Since the two provider populations are mutually exclusive, a single hardcoded host can't satisfy both. Options that do:http://localhost:<port>/callbackandhttp://127.0.0.1:<port>/callbackat DCR time, then send whichever the server's registered set contains (they resolve to the same socket, so no extra listener is needed).redirect_uri_mismatch/invalid_redirect_uri.oauth.callbackHostalongside the existingoauth.callbackPortin.mcp.json, plus a matching env var. This also lets plugin authors pin the host their app registration actually uses, which is what the Slack case needs.Happy to test a fix against both the Slack plugin and a DCR server if useful.
Env: Claude Code 2.1.229, native install, macOS 26.3 (darwin 25.3.0), arm64.
hit this as well
127.0.0.1 is the spec-correct choice. RFC 8252 §7.3 defines the loopback redirect using the IP literal (127.0.0.1 / ::1), and §8.3 explicitly says using localhost is NOT RECOMMENDED — it can inadvertently bind beyond loopback and is more susceptible to misconfigured host resolution. OAuth 2.1 carries the same guidance forward (it just tightens redirect URI matching in general, with loopback still getting the port-mismatch exception).
A config option to opt into localhost (with a warning) for providers that insist on it — Salesforce, for example, rejects IP-literal callbacks outright — is a reasonable compromise. But 127.0.0.1/::1 should stay the default.