diff --git a/src/fabledassistant/mcp/server.py b/src/fabledassistant/mcp/server.py index d7cde5c..6d42f35 100644 --- a/src/fabledassistant/mcp/server.py +++ b/src/fabledassistant/mcp/server.py @@ -78,10 +78,12 @@ def mount_mcp(app: Quart) -> None: if scope["type"] == "http": path = scope.get("path", "") if path == "/mcp" or path.startswith("/mcp/"): - sub_scope = dict(scope) - sub_scope["path"] = path[len("/mcp"):] or "/" - sub_scope["root_path"] = scope.get("root_path", "") + "/mcp" - return await auth_wrapped(sub_scope, receive, send) + # Don't rewrite the path: FastMCP's streamable_http_app mounts + # its handler at /mcp by default. If we strip the prefix to "/", + # FastMCP's internal routing returns 404 because there's no + # handler at "/" — only at "/mcp". Pass the scope through + # untouched and let FastMCP's own routing match. + return await auth_wrapped(scope, receive, send) return await original_asgi(scope, receive, send) app.asgi_app = dispatch diff --git a/tests/test_mcp_endpoint.py b/tests/test_mcp_endpoint.py index a054d11..d799e42 100644 --- a/tests/test_mcp_endpoint.py +++ b/tests/test_mcp_endpoint.py @@ -85,8 +85,10 @@ async def test_mcp_endpoint_invalid_token_returns_401(): @pytest.mark.asyncio async def test_mcp_endpoint_valid_token_passes_auth(): - """With a valid Bearer, the request reaches FastMCP. Anything other than 401 - confirms auth passed and the request was handed off to the sub-app.""" + """With a valid Bearer, the request must successfully reach FastMCP's + initialize handler. Asserting `!= 401` is too weak: it lets a 404 from + a path-mismatch (the original bug) through. FastMCP responds 200 to a + well-formed initialize handshake.""" fake_key = MagicMock() fake_key.user_id = 7 fake_key.scope = "write" @@ -112,7 +114,7 @@ async def test_mcp_endpoint_valid_token_passes_auth(): }, body=initialize_body, ) - assert status != 401 + assert status == 200, f"expected 200 from FastMCP initialize, got {status}" # Note: there's no explicit "non-/mcp paths bypass the middleware" test here