Skip to content
Open
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
15 changes: 13 additions & 2 deletions fastapi_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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[
Expand Down Expand Up @@ -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()
Expand Down
32 changes: 30 additions & 2 deletions tests/test_http_real_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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"]