fix(mcp): use localhost (not 127.0.0.1) in OAuth redirect_uri to clear provider WAFs - #61755
Conversation
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.
Related to the MCP-OAuth-redirect cluster: closed narrow predecessor #29875 (same |
|
Thanks for isolating both metadata construction paths; current main does still advertise Problems
Suggested changes
Automated hermes-sweeper review. |
|
Closing after discussing the default: |
Problem
Some hosted MCP providers front their OAuth authorize endpoint with a Web Application Firewall whose ruleset 403s any
redirect_uricontaining a raw loopback IP literal (127.0.0.1) while allowing thelocalhosthostname.Concrete repro — Motion (
https://projects.motionapp.com/mcp, OAuth 2.1 PKCE, served byMicrosoft-Azure-Application-Gateway/v2):hermes mcp add motion --url https://projects.motionapp.com/mcp --auth oauthcompletes dynamic client registration and opens the browser, but the authorize page returns a bare403 Forbidden(Azure App Gateway) before any login/consent screen.redirect_uri.Isolated by incrementally building the authorize query string against the live endpoint:
redirect_urihttp://127.0.0.1:<port>/callbackhttp://127.0.0.1/callbackhttps://127.0.0.1:<port>/callbackhttp://localhost:<port>/callbackhttp://localhost/callbackHermes hardcoded
http://127.0.0.1:{port}/callbackin two sites intools/mcp_oauth.py, so every authorize attempt against such a provider was dead on arrival.Fix
Advertise the OAuth
redirect_uriwith thelocalhosthostname by default.localhostresolves back to127.0.0.1, so the local loopback callback listener still receives the redirect — transparent for every other provider._redirect_host()resolver with precedence: per-serveroauth.redirect_host>HERMES_MCP_OAUTH_REDIRECT_HOSTenv >localhostdefault. Operators that need the raw IP (or any other host) can opt back in.redirect_uriconstruction sites (_build_client_metadata,_maybe_preregister_client).Tests
TestBuildOAuthAuth::test_redirect_uri_defaults_to_localhost— asserts the advertisedredirect_uriuseslocalhost, never127.0.0.1.TestRedirectHost— resolver precedence (default / per-server override / env default).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.mddocumenting the WAF behavior and theredirect_hostoverride.Compatibility / risk
Low. The callback server still binds
127.0.0.1; only the advertised hostname string changes. Any provider that previously worked with127.0.0.1continues to work vialocalhost(same resolution), and the escape hatch restores the old value for the rare provider that wants the literal IP.