# NousResearch/hermes-agent · MCP OAuth strips URL path breaking resource validation

## Open now

- 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.

## TL;DR

- 10 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 1 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #16031. Verify it addresses your case.
- **Recommended:** read PR #16031 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-16031`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #10271: \[Bug\]: OAuth 2.1 PKCE fails for servers with a path in their endpoint URL (RFC 8707 resource mismatch)
- #11807: MCP OAuth: server_url path stripped breaks GitHub MCP resource validation
- #11838: MCP OAuth: server_url path stripped breaks GitHub MCP resource validation
- #16015: MCP OAuth strips /mcp from server_url, breaking Notion protected resource validation

## Prior & competing PRs

- **#16031: fix(mcp-oauth): preserve server_url path for protected-resource validation**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_mcp_oauth.py`, `tools/mcp_oauth.py`, `tools/mcp_oauth_manager.py`
  - fix hunks:
  - `tests/tools/test_mcp_oauth.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -362,7 +362,6 @@ def _build_provider(
                 _configure_callback_port,
                 _is_interactive,
                 _maybe_preregister_client,
    -            _parse_base_url,
                 _redirect_handler,
                 _wait_for_callback,
             )
    ```

    ```diff
    @@ -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,
    ```
- **#10278: fix(mcp): pass full server URL to OAuthClientProvider, strip trailing slash**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d09ab8ff1`, `#16031`
- **#10932: fix(mcp): preserve OAuth server URL paths**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d09ab8ff13329da1715d20e3fb17d47f499fbc18`, `#16031`
- **#11808: fix: preserve server URL path in MCP OAuth resource matching**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#11837: fix: preserve server URL path in MCP OAuth resource matching**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d09ab8ff13329da1715d20e3fb17d47f499fbc18`, `#16031`
- **#15747: fix(mcp-oauth): preserve server URL path when constructing OAuthClientProvider**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#10932`, `#13230`, `#13853`, `#10271`

## Fix status

- Merged to main in PR #16031 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-26`.
- Subsystem: `mcp`
