Skip to content

mcp_oauth: configurable redirect_host for WAF-safe OAuth redirect URIs - #63889

Closed
Peterskaronis wants to merge 1 commit into
NousResearch:mainfrom
Peterskaronis:fix/mcp-oauth-redirect-host
Closed

mcp_oauth: configurable redirect_host for WAF-safe OAuth redirect URIs#63889
Peterskaronis wants to merge 1 commit into
NousResearch:mainfrom
Peterskaronis:fix/mcp-oauth-redirect-host

Conversation

@Peterskaronis

Copy link
Copy Markdown
Contributor

Problem

Some MCP providers sit behind WAFs that reject any /authorize request whose query string contains a literal 127.0.0.1. Reclaim.ai's MCP (https://mcp.reclaim.ai, AWS API Gateway) is a live example: every authorize attempt returns {"message":"Forbidden"} before reaching the OAuth application, because _build_client_metadata hardcodes

redirect_uri = f"http://127.0.0.1:{port}/callback"

Clients that use localhost in the redirect URI (e.g. Claude Code) pass the same WAF fine, so the failure looks provider-side and is painful to diagnose — the browser shows Forbidden even for a logged-in user, for every client_id (valid, freshly-registered, or bogus).

Fix

Add an optional redirect_host key to a server's oauth config block, defaulting to 127.0.0.1 (behavior unchanged for existing configs):

mcp_servers:
  reclaim:
    url: https://mcp.reclaim.ai
    auth: oauth
    oauth:
      redirect_port: 8877
      redirect_host: localhost   # WAF-safe

The callback listener still binds 127.0.0.1 either way (localhost resolves there), so only the registered/requested redirect URI string changes.

Verified

Live against Reclaim.ai's MCP: with redirect_host: localhost, DCR + authorize + token exchange + tools/list complete end-to-end (28 tools). With the default 127.0.0.1, the same flow is 403-blocked at the edge.

🤖 Generated with Claude Code

Reclaim.ai's AWS API Gateway WAF 403s any /oauth2/authorize request whose
query string contains a literal 127.0.0.1, so the SDK's hardcoded
redirect_uri made the browser flow impossible. New optional oauth config
key redirect_host (default 127.0.0.1, unchanged behavior) lets a server
entry use localhost instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request tool/mcp MCP client and OAuth area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists duplicate This issue or pull request already exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 13, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #61755 (earlier, open) — both add a configurable redirect_host to the server's oauth config block in tools/mcp_oauth.py to work around MCP providers whose WAF 403s an authorize request carrying a literal 127.0.0.1 redirect_uri. #61755 implements the same mechanism via a fuller resolver (oauth.redirect_host > HERMES_MCP_OAUTH_REDIRECT_HOST env > default) applied at both call sites; the only difference here is the default value. Consolidating onto #61755 as the canonical fix. Related MCP-OAuth-redirect cluster: #47755 (full-URI override), #21482 (host-only config), #29299 (feature request).

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: LGTM (token read-only, submitted as COMMENT)

Adds a configurable redirect_host to MCP OAuth client metadata so WAFs that reject 127.0.0.1 in authorize queries (cloudfront/apigateway style) can use localhost. The redirect listener still binds 127.0.0.1 either way, so no security boundary changes — only the URI string the provider sees.

Notes:

  • Defaults to 127.0.0.1 for backward compatibility.
  • Docstring names the specific failure mode (Reclaim.ai AWS API Gateway FORBIDDEN).
  • Single-file, single-line semantics change, low risk.

Reviewed by Hermes Agent batch cron (token read-only).

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating a real compatibility problem: current main still advertises 127.0.0.1 at tools/mcp_oauth.py:849.

Problems

  • The PR changes only _build_client_metadata; pre-registered clients still write http://127.0.0.1:{port}/callback at tools/mcp_oauth.py:876. Configurations using oauth.client_id therefore do not receive the requested override.
  • redirect_host is accepted without validation, while the callback listener remains bound to 127.0.0.1 at tools/mcp_oauth.py:655. An arbitrary configured hostname can advertise a callback Hermes cannot receive.
  • The diff has no regression tests. Existing metadata tests start at tests/tools/test_mcp_oauth.py:712 and do not cover configured hosts or the pre-registered path.

Suggested changes

  • Share one validated effective redirect-URI helper between _build_client_metadata and _maybe_preregister_client.
  • Add dynamic-registration and client_id tests for the configured host, and align the SSH/docs messaging with the effective URI.

This is an automated hermes-sweeper review.

Comment thread tools/mcp_oauth.py
# returning ``{"message":"Forbidden"}``. ``redirect_host: localhost`` in
# the server's oauth config works around that; the callback listener
# still binds 127.0.0.1 either way.
redirect_host = cfg.get("redirect_host", "127.0.0.1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redirect_host is unrestricted here, but the callback listener remains fixed to 127.0.0.1 (tools/mcp_oauth.py:655). Please restrict this to hosts the listener actually serves, or bind the listener consistently; otherwise a configured non-loopback host advertises a callback Hermes cannot receive.

@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 16, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #65610 with your commit cherry-picked onto current main and your authorship preserved in git log — thanks for tracking down the Reclaim.ai WAF behavior! Your redirect_host option was integrated into the _resolve_redirect_uri helper that landed alongside it (from #47755), so it composes with the new redirect_uri proxy option: an explicit redirect_uri wins, redirect_host rewrites the loopback default's hostname. The WAF pitfall is now documented in the MCP docs with Reclaim.ai as the worked example.

@teknium1 teknium1 closed this Jul 16, 2026
samuelpulfer pushed a commit to immeditech/hermes-agent that referenced this pull request Aug 12, 2026
Upstream hat NousResearch#47755 (configurable redirect_uri) via NousResearch#65610 gemerged und dabei
umgebaut: _make_redirect_handler-Closure statt functools.partial (NousResearch#44588/NousResearch#44590),
_resolve_redirect_uri mit neuer redirect_host-Praezedenz (NousResearch#63889), plus eine
TOCTOU-Portreservierung (NousResearch#22161).

Konflikte in tools/mcp_oauth.py und tests/tools/test_mcp_oauth.py zugunsten der
Upstream-Seite aufgeloest; unser oauth.redirect_bind neu darauf aufgesetzt:

- _oauth_bind_host global (Default Loopback) — jetzt explizit abgegrenzt gegen
  upstreams redirect_host (das nur die *angekuendigte* URI umschreibt)
- _configure_callback_port loest redirect_bind auf, VOR der Portwahl
- _reserve_callback_port bindet auf _oauth_bind_host (NEUE zweite Bindstelle aus
  NousResearch#22161 — sonst waeren Reservierung und HTTPServer uneinig)
- HTTPServer bindet (_oauth_bind_host, port)
- _find_free_port(host=None) faellt auf _oauth_bind_host zurueck

Tests als TestRedirectBind neu aufgesetzt (die alte Datei war upstream
klassenbasiert reorganisiert), inkl. Regressionstest fuer die Reservierung.
5 passed. Der eine Fehlschlag in test_mcp_oauth.py
(test_build_oauth_auth_preserves_server_url_path) ist vorbestehend und
umgebungsbedingt — er faellt auf unveraendertem upstream/main identisch aus
(mcp-SDK im Devcontainer nicht installiert).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 duplicate This issue or pull request already exists 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/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants