MCP OAuth loopback redirect uses 127.0.0.1 instead of localhost — breaks providers (e.g. Salesforce) that only allow localhost callback URLs

Status Fixed / completed
Maintainer reply None cached
Activity 5 comments · opened Aug 12, 2026 · closed Aug 19, 2026

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:

  1. claude mcp add --transport http <name> <salesforce-mcp-url> --client-id <id> --client-secret --callback-port 8765
  2. claude mcp login <name>
  3. 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).

View original on GitHub ↗

3 Comments

jameslnewell · 18 days ago

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/):

2.1.227  function yze(e=Qjs){return`http://localhost:${e}/callback`}
2.1.228               return`http://localhost:${e}/callback`
2.1.229  function eBr(e=m1a){return`http://127.0.0.1:${e}/callback`}

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-official v1.2.0 (installed from the claude-plugins-official marketplace; plugin authored by Slack) pins a static, pre-registered OAuth client rather than using DCR:

// .claude/plugins/cache/claude-plugins-official/slack/1.2.0/.mcp.json
{"mcpServers": {"slack": {
  "type": "http",
  "url": "https://mcp.slack.com/mcp",
  "oauth": {"clientId": "1601185624273.8899143856786", "callbackPort": 3118}
}}}

Because the client ID is fixed, the allowlist lives in Slack's app config and was registered as http://localhost:3118/callback. Slack string-matches redirect_uri (no RFC 8252 §7.3 loopback normalisation), so on 2.1.229 authorization dead-ends at:

Something went wrong when authorizing Claude. redirect_uri did not match any configured URIs. Passed URI: http://127.0.0.1:3118/callback

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 localhost on the OIDC/IdP login path:

// MCP OAuth path
function eBr(e=m1a){return`http://127.0.0.1:${e}/callback`}
// OIDC/IdP path, same build
let i = e.callbackPort ?? await mPt(), s = `http://localhost:${i}/callback`

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.

function oMS(){let e=Q.MCP_OAUTH_CALLBACK_PORT; return e!==void 0&&e<=65535?e:void 0}

Workaround (confirmed working)

Authorize once on a 2.1.228 binary, then go back to current:

~/.local/share/claude/versions/2.1.228     # then /mcp -> authenticate

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 localhost just re-breaks the other direction — #82096 and #42765 asked for 127.0.0.1 precisely 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:

  1. Bind the loopback listener once and register both http://localhost:<port>/callback and http://127.0.0.1:<port>/callback at DCR time, then send whichever the server's registered set contains (they resolve to the same socket, so no extra listener is needed).
  2. Retry the authorize with the other host when the provider returns redirect_uri_mismatch / invalid_redirect_uri.
  3. Make it explicit — an oauth.callbackHost alongside the existing oauth.callbackPort in .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.

shushu-ant · 18 days ago

hit this as well

RecordedFutureOskbo · 12 days ago

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.

Showing cached comments. Read the full discussion on GitHub ↗