From a9390ec39e20303a88897912f08efa480cfb3661 Mon Sep 17 00:00:00 2001 From: arc53-machine <232052973+arc53-machine@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:57:54 +0100 Subject: [PATCH] Fix MCP OAuth for servers that publish their endpoint as the resource Linear and Sentry publish https://host/mcp as their protected resource. DocsGPTOAuth gave the MCP SDK only the server's origin, so the SDK's resource check (RFC 8707) failed before sign-in with "Protected resource https://mcp.linear.app/mcp does not match expected https://mcp.linear.app". The provider now gets the full MCP URL; servers that publish their origin still pass, and tokens stay keyed by origin. --- docsgpt/agents/tools/mcp_tool.py | 6 +++++- tests/connectors/test_mcp_refresh.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docsgpt/agents/tools/mcp_tool.py b/docsgpt/agents/tools/mcp_tool.py index c3cfdc91..6af9ac4d 100644 --- a/docsgpt/agents/tools/mcp_tool.py +++ b/docsgpt/agents/tools/mcp_tool.py @@ -752,8 +752,12 @@ class DocsGPTOAuth(OAuthClientProvider): connection_id=connection_id, ) + # The SDK checks the server's protected-resource metadata against a + # resource derived from this URL, so it gets the full MCP endpoint: + # Linear and Sentry publish ``https://host/mcp``, and an origin-only + # URL fails that check before sign-in. Tokens stay keyed by origin. super().__init__( - server_url=self.server_base_url, + server_url=mcp_url.rstrip("/") or self.server_base_url, client_metadata=client_metadata, storage=storage, redirect_handler=self.redirect_handler, diff --git a/tests/connectors/test_mcp_refresh.py b/tests/connectors/test_mcp_refresh.py index 86b52eae..c5492498 100644 --- a/tests/connectors/test_mcp_refresh.py +++ b/tests/connectors/test_mcp_refresh.py @@ -120,3 +120,25 @@ class TestDiscover: with pytest.raises(service.ConnectionUnavailable), patch("docsgpt.agents.tools.mcp_tool.MCPTool") as tool_cls: mcp._discover("alice", connection, {"config": {}}) tool_cls.assert_not_called() + + +def test_oauth_resource_is_the_full_mcp_endpoint(): + """Linear and Sentry publish ``https://host/mcp`` as their protected + resource; the SDK must be given that endpoint, not only the origin.""" + from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url + + from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth + + oauth = DocsGPTOAuth( + mcp_url="https://mcp.linear.app/mcp", + redis_client=MagicMock(), + redirect_uri="https://example.com/callback", + user_id="user1", + ) + assert oauth.context.server_url == "https://mcp.linear.app/mcp" + requested = resource_url_from_server_url(oauth.context.server_url) + # Servers that publish the endpoint, and those that publish the origin. + assert check_resource_allowed(requested_resource=requested, configured_resource="https://mcp.linear.app/mcp") + assert check_resource_allowed(requested_resource=requested, configured_resource="https://mcp.linear.app") + # Stored tokens stay keyed by the server's origin. + assert oauth.context.storage.server_url == "https://mcp.linear.app"