Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/dremioai/servers/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,25 @@
from mcp.cli.claude import get_claude_config_path
from mcp.shared.auth import OAuthMetadata
from mcp.types import ToolAnnotations
from pydantic import AnyHttpUrl
from pydantic import AnyHttpUrl, field_serializer
from pydantic.networks import AnyUrl

from dremioai.metrics.registry import get_metrics_app
from starlette.requests import Request
from starlette.responses import Response


class OAuthMetadataRFC8414(OAuthMetadata):
"""RFC 8414 compliant OAuth metadata that strips trailing slash from issuer URL.

The MCP SDK's OAuthMetadata uses AnyHttpUrl for the issuer field, which adds
a trailing slash during serialization. RFC 8414 Section 3.2 requires the issuer
to exactly match the discovery URL without trailing slash.
"""
@field_serializer('issuer')
def serialize_issuer(self, value: AnyHttpUrl) -> str:
return str(value).rstrip('/')

from dremioai.tools import tools
import os
from typing import List, Union, Annotated, Optional, Tuple, Dict, Any
Expand Down Expand Up @@ -180,7 +192,7 @@ def init(
async def authorization_server_metadata(request: Request) -> Response:
if issuer := settings.instance().dremio.auth_issuer_uri:
auth, tok = settings.instance().dremio.auth_endpoints
md = OAuthMetadata(
md = OAuthMetadataRFC8414(
issuer=AnyHttpUrl(issuer),
authorization_endpoint=auth,
token_endpoint=tok,
Expand Down
37 changes: 37 additions & 0 deletions tests/e2e/test_mcp_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,43 @@ async def test_healthz(mock_config_dir, logging_server, logging_level):
), f"/healthz failed with {r.text}, {r.status_code}"


@pytest.mark.asyncio
async def test_oauth_discovery_rfc8414_compliance(mock_config_dir, logging_server, logging_level):
"""Test that OAuth discovery fails with trailing slash (reproduces DX-114676).

This test reproduces the issue that started happening around Feb 12, 2026 when
Claude Desktop clients were updated to strictly validate RFC 8414 Section 3.2,
which requires the issuer field in OAuth metadata to exactly match the discovery URL.

The bug: AnyHttpUrl adds a trailing slash to the issuer URL, causing strict clients
to reject the OAuth metadata because the issuer doesn't match the discovery URL.
"""
async with http_streamable_mcp_server(logging_server, logging_level) as sf:
async with AsyncClient() as client:
oauth_url = urlparse(sf.mcp_server.url)._replace(
path="/.well-known/oauth-authorization-server"
).geturl()

r = await client.get(oauth_url)

if r.status_code == 404:
pytest.skip("OAuth not configured for this test environment")

assert r.status_code == 200, f"OAuth metadata endpoint failed: {r.text}"

# Check the raw JSON response (what clients actually receive)
data = r.json()
issuer_from_json = data["issuer"]

if issuer_from_json.endswith('/'):
pytest.fail(
f"RFC 8414 Section 3.2 violation: issuer has trailing slash.\n"
f"Got: {issuer_from_json}\n"
f"This causes OAuth discovery to fail with strict clients (Claude Desktop after Feb 12, 2026).\n"
f"The issuer field MUST exactly match the discovery URL without trailing slash."
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
"engine_name",
Expand Down