MCP OAuth strips URL path breaking resource validation
NousResearch/hermes-agent · cluster 10271 · mcp
Data as of
Nothing open on GitHub. Fix merged in #16031. Check it covers your case.
The short answer
- Known?
- Yes. 10 related items grouped here, 4 issues already closed.
- Fix event
- A fix was merged to main in PR #16031. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #16031. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple bug reports and fix attempts address an issue where MCP OAuth drops the path component from server URLs, causing protected resource validation failures against servers like Notion and GitHub. Several parallel fix PRs propose preserving the full server URL path when constructing the OAuthClientProvider.
Open issues0
Open PRs0
Fixes on record
-
#16031 fix(mcp-oauth): preserve server_url path for protected-resource validation
files:
tests/tools/test_mcp_oauth.py,tools/mcp_oauth.py,tools/mcp_oauth_manager.pyShow the diff
-
tests/tools/test_mcp_oauth.py@@ -491,11 +491,36 @@ def test_configure_callback_port_uses_explicit_port(): assert cfg["_resolved_port"] == 54321 -def test_parse_base_url_strips_path(): - """_parse_base_url drops path components for OAuth discovery.""" - from tools.mcp_oauth import _parse_base_url +def test_build_oauth_auth_preserves_server_url_path(): + """server_url with path is forwarded to OAuthClientProvider unmodified. + + Regression for #16015: previously ``_parse_base_url`` stripped the path, + collapsing ``https://mcp.notion.com/mcp`` to ``https://mcp.notion.com`` and + breaking RFC 9728 protected-resource validation against servers whose PRM + advertises a path-scoped resource (Notion). The MCP SDK strips the path + itself for authorization-server discovery via + ``OAuthContext.get_authorization_base_url``; Hermes must not pre-strip. + """ + from tools import mcp_oauth + + captured: dict = {} + + class _FakeProvider: + def __init__(self, **kwargs): + captured.update(kwargs) + + with patch.object(mcp_oauth, "_OAUTH_AVAILABLE", True), \ + patch.object(mcp_oauth, "OAuthClientProvider", _FakeProvider), \ + patch.object(mcp_oauth, "_is_interactive", return_value=True), \ + patch.object(mcp_oauth, "_maybe_preregister_client"), \ + patch.object(mcp_oauth, "HermesTokenStorage") as mock_storage_cls: + mock_storage_cls.return_value = MagicMock(has_cached_tokens=[redacted:secret-assignment] True) + build_oauth_auth( + server_name="notion", + server_url="https://mcp.notion.com/mcp", + oauth_config={}, + ) + + assert captured["server_url"] == "https://mcp.notion.com/mcp" - assert _parse_base_url("https://example.com/mcp/v1") == "https://example.com" - assert _parse_base_url("https://example.com") == "https://example.com" - assert _parse_base_url("https://host.example.com:8080/api") == "https://host.example.com:8080" -
tools/mcp_oauth.py@@ -519,12 +519,6 @@ def _maybe_preregister_client( logger.debug("Pre-registered client_id=%s for '%s'", client_id, storage._server_name) -def _parse_base_url(server_url: str) -> str: - """Strip path component from server URL, returning the base origin.""" - parsed = urlparse(server_url) - return f"{parsed.scheme}://{parsed.netloc}" - - def build_oauth_auth( server_name: str, server_url: str, @@ -570,7 +564,7 @@ def build_oauth_auth( _maybe_preregister_client(storage, cfg, client_metadata) return OAuthClientProvider( - server_url=_parse_base_url(server_url), + server_url=server_url, client_metadata=client_metadata, storage=storage, redirect_handler=_redirect_handler, -
tools/mcp_oauth_manager.py@@ -362,7 +362,6 @@ def _build_provider( _configure_callback_port, _is_interactive, _maybe_preregister_client, - _parse_base_url, _redirect_handler, _wait_for_callback, ) @@ -387,7 +386,7 @@ def _build_provider( return _HERMES_PROVIDER_CLS( server_name=server_name, - server_url=_parse_base_url(entry.server_url), + server_url=entry.server_url, client_metadata=client_metadata, storage=storage, redirect_handler=_redirect_handler,
-
5 PRs closed without merging
From the close comments: done elsewhere 3 · closed without comment 1 · duplicate 1
-
#10278 fix(mcp): pass full server URL to OAuthClientProvider, strip trailing slash · done elsewhere →
d09ab8f,#16031 -
#10932 fix(mcp): preserve OAuth server URL paths · done elsewhere →
d09ab8f,#16031 - #11808 fix: preserve server URL path in MCP OAuth resource matching · closed without comment
-
#11837 fix: preserve server URL path in MCP OAuth resource matching · done elsewhere →
d09ab8f,#16031 -
#15747 fix(mcp-oauth): preserve server URL path when constructing OAuthClientProvider · duplicate →
#10932,#13230,#13853,#10271
Closed issues · 4
- Closed#10271[Bug]: OAuth 2.1 PKCE fails for servers with a path in their endpoint URL (RFC 8707 resource mismatch)
- Closed#11807MCP OAuth: server_url path stripped breaks GitHub MCP resource validation
- Closed#11838MCP OAuth: server_url path stripped breaks GitHub MCP resource validation
- Closed#16015MCP OAuth strips /mcp from server_url, breaking Notion protected resource validation
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.23: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- mcp
- Merged PRs
- #16031
- Closed issues
- 4
- Attempts
- 5 PRs closed without merging
- Fix link checked
- 26 Apr 2026