Skip to content

fix(mcp): use localhost (not 127.0.0.1) in OAuth redirect_uri to clear provider WAFs - #61755

Closed
tmdiaz10-achilles wants to merge 1 commit into
NousResearch:mainfrom
tmdiaz10-achilles:fix/mcp-oauth-redirect-uri-localhost
Closed

fix(mcp): use localhost (not 127.0.0.1) in OAuth redirect_uri to clear provider WAFs#61755
tmdiaz10-achilles wants to merge 1 commit into
NousResearch:mainfrom
tmdiaz10-achilles:fix/mcp-oauth-redirect-uri-localhost

Conversation

@tmdiaz10-achilles

Copy link
Copy Markdown

Problem

Some hosted MCP providers front their OAuth authorize endpoint with a Web Application Firewall whose ruleset 403s any redirect_uri containing a raw loopback IP literal (127.0.0.1) while allowing the localhost hostname.

Concrete repro — Motion (https://projects.motionapp.com/mcp, OAuth 2.1 PKCE, served by Microsoft-Azure-Application-Gateway/v2):

  • hermes mcp add motion --url https://projects.motionapp.com/mcp --auth oauth completes dynamic client registration and opens the browser, but the authorize page returns a bare 403 Forbidden (Azure App Gateway) before any login/consent screen.
  • It looks like a permissions/login problem, but it's the WAF rejecting the redirect_uri.

Isolated by incrementally building the authorize query string against the live endpoint:

redirect_uri Result
http://127.0.0.1:<port>/callback ❌ 403 (Azure WAF)
http://127.0.0.1/callback ❌ 403
https://127.0.0.1:<port>/callback ❌ 403
http://localhost:<port>/callback ✅ 302 (proceeds to login)
http://localhost/callback ✅ 302

Hermes hardcoded http://127.0.0.1:{port}/callback in two sites in tools/mcp_oauth.py, so every authorize attempt against such a provider was dead on arrival.

Fix

Advertise the OAuth redirect_uri with the localhost hostname by default. localhost resolves back to 127.0.0.1, so the local loopback callback listener still receives the redirect — transparent for every other provider.

  • New _redirect_host() resolver with precedence: per-server oauth.redirect_host > HERMES_MCP_OAUTH_REDIRECT_HOST env > localhost default. Operators that need the raw IP (or any other host) can opt back in.
  • Applied at both redirect_uri construction sites (_build_client_metadata, _maybe_preregister_client).

Tests

  • TestBuildOAuthAuth::test_redirect_uri_defaults_to_localhost — asserts the advertised redirect_uri uses localhost, never 127.0.0.1.
  • TestRedirectHost — resolver precedence (default / per-server override / env default).
  • Full MCP OAuth suite green locally: 122 passed across test_mcp_oauth*.py (77 pre-existing in the main file + 4 new = 81 in that file, 122 across the OAuth suite).

Docs

New OAuth pitfall in website/docs/user-guide/features/mcp.md documenting the WAF behavior and the redirect_host override.

Compatibility / risk

Low. The callback server still binds 127.0.0.1; only the advertised hostname string changes. Any provider that previously worked with 127.0.0.1 continues to work via localhost (same resolution), and the escape hatch restores the old value for the rare provider that wants the literal IP.

Some MCP providers front their OAuth authorize endpoint with a WAF that
403s any redirect_uri containing a raw loopback IP literal (127.0.0.1)
while allowing the localhost hostname. Observed with Motion
(projects.motionapp.com, served by Microsoft-Azure-Application-Gateway/v2):
the browser shows a bare 403 Forbidden page before any login/consent
screen, which looks like a permissions problem but is the firewall
rejecting the redirect_uri.

localhost resolves back to 127.0.0.1, so the local loopback callback
listener still receives the redirect - the change is transparent for
every other provider.

- Add _redirect_host() resolver: per-server oauth.redirect_host >
  HERMES_MCP_OAUTH_REDIRECT_HOST env > 'localhost' default. Operators
  that need the raw IP can opt back in.
- Use it in both redirect_uri construction sites
  (_build_client_metadata, _maybe_preregister_client).
- Tests: assert the default redirect_uri uses localhost not 127.0.0.1,
  and cover the resolver precedence.
- Docs: new OAuth pitfall in user-guide/features/mcp.md.
@alt-glitch alt-glitch added type/bug Something isn't working tool/mcp MCP client and OAuth area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data P2 Medium — degraded but workaround exists labels Jul 10, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to the MCP-OAuth-redirect cluster: closed narrow predecessor #29875 (same localhost swap), open #47755 (full-URI override), open #21482 (host-only redirect config), and open feature issue #29299 (HTTPS/full-URI callback demand). This PR is the minimal default-host fix (advertise localhost, opt back to a raw IP via oauth.redirect_host / HERMES_MCP_OAUTH_REDIRECT_HOST). Overlapping OPEN cluster — maintainer picks the canonical approach (minimal host default here vs. #47755's full-URI override).

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating both metadata construction paths; current main does still advertise 127.0.0.1 in tools/mcp_oauth.py:849 and tools/mcp_oauth.py:876, so the compatibility issue is real.

Problems

  • The new resolver permits arbitrary hosts, but the callback listener remains bound to 127.0.0.1 at tools/mcp_oauth.py:655. A non-loopback override can therefore advertise a URI that Hermes cannot receive.
  • The new documented HERMES_MCP_OAUTH_REDIRECT_HOST is a user-facing non-secret configuration setting. AGENTS.md:102-107 and AGENTS.md:620-623 require this class of setting to live in config.yaml.
  • The SSH hint still prints http://127.0.0.1:<port>/callback in tools/mcp_oauth.py:569-588; website/docs/guides/oauth-over-ssh.md:78 has the same assumption. Neither remains accurate after the default host change.

Suggested changes

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Closing after discussing the default: redirect_host shipped this morning in dc419d6 (@Peterskaronis, same WAF class — Reclaim.ai's AWS gateway), so the Motion fix is now one config line under the server's oauth block: redirect_host: \"localhost\". We're keeping 127.0.0.1 as the default deliberately: providers pin redirect URIs from dynamic client registration at first auth, so flipping the default would break re-auth for every existing registration that stored the IP-literal form, and localhost resolution has its own edge cases (IPv6 ::1-first resolvers vs a v4-bound listener). Your live-endpoint isolation table was excellent and directly validated the config knob's design — thanks @tmdiaz10-achilles, and the docs example now covers the WAF scenario.

@teknium1 teknium1 closed this Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/mcp MCP client and OAuth type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants