From 4159eddb91158419cb9f2891b88b4ebcd88f32a7 Mon Sep 17 00:00:00 2001 From: Daniyal Hermes Date: Tue, 4 Aug 2026 15:39:12 +0500 Subject: [PATCH] fix(http): fail closed without auth config --- fastapi_mcp/server.py | 15 +++++++++++++-- tests/test_http_real_transport.py | 32 +++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index bb751067..1049d71e 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -3,7 +3,7 @@ from typing import Dict, Optional, Any, List, Union, Literal, Sequence from typing_extensions import Annotated, Doc -from fastapi import FastAPI, Request, APIRouter, params +from fastapi import FastAPI, Request, APIRouter, params, HTTPException, Depends from fastapi.openapi.utils import get_openapi from mcp.server.lowlevel.server import Server import mcp.types as types @@ -309,6 +309,17 @@ def _setup_auth(self): else: logger.info("No auth config provided, skipping auth setup") + @staticmethod + def _fail_closed_auth_dependency(): + async def require_auth() -> None: + raise HTTPException( + status_code=401, + detail="Authentication required. Configure AuthConfig before exposing MCP tools over HTTP.", + headers={"WWW-Authenticate": "Bearer"}, + ) + + return Depends(require_auth) + def mount_http( self, router: Annotated[ @@ -349,7 +360,7 @@ def mount_http( assert isinstance(router, (FastAPI, APIRouter)), f"Invalid router type: {type(router)}" http_transport = FastApiHttpSessionManager(mcp_server=self.server) - dependencies = self._auth_config.dependencies if self._auth_config else None + dependencies = self._auth_config.dependencies if self._auth_config else [self._fail_closed_auth_dependency()] self._register_mcp_endpoints_http(router, http_transport, mount_path, dependencies) self._setup_auth() diff --git a/tests/test_http_real_transport.py b/tests/test_http_real_transport.py index 06f63174..c3b9dffe 100644 --- a/tests/test_http_real_transport.py +++ b/tests/test_http_real_transport.py @@ -8,11 +8,12 @@ import threading import coverage from typing import AsyncGenerator, Generator -from fastapi import FastAPI +from fastapi import FastAPI, Depends +from fastapi.routing import APIRoute import pytest import httpx import uvicorn -from fastapi_mcp import FastApiMCP +from fastapi_mcp import FastApiMCP, AuthConfig import mcp.types as types @@ -59,6 +60,7 @@ def periodic_save(): fastapi_app, name=SERVER_NAME, description="Test description", + auth_config=AuthConfig(dependencies=[Depends(lambda: None)]), ) mcp.mount_http() @@ -476,3 +478,29 @@ async def test_http_notification_handling(http_client: httpx.AsyncClient, server assert response.status_code == 202 # Notifications should return empty body assert response.content == b"" or response.text == "null" + + +def test_http_fails_closed_without_auth_config(simple_fastapi_app: FastAPI) -> None: + mcp = FastApiMCP(simple_fastapi_app) + mcp.mount_http() + + route = next(route for route in simple_fastapi_app.routes if isinstance(route, APIRoute) and route.path == "/mcp") + assert route.dependant.dependencies + + +@pytest.mark.anyio +async def test_http_rejects_unauthenticated_request(simple_fastapi_app: FastAPI) -> None: + mcp = FastApiMCP(simple_fastapi_app) + mcp.mount_http() + + transport = httpx.ASGITransport(app=simple_fastapi_app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.post( + "/mcp", + json={"jsonrpc": "2.0", "method": "tools/list", "id": 1}, + headers={"Accept": "application/json, text/event-stream", "Content-Type": "application/json"}, + ) + + assert response.status_code == 401 + assert response.headers["www-authenticate"] == "Bearer" + assert "Authentication required" in response.json()["detail"]