Skip to content

Commit ba04a0f

Browse files
ihrprdsp-ant
authored andcommitted
SSE FastMCP - do not go though auth when it's not needed (#619)
(cherry picked from commit 58c5e72)
1 parent 2210c1b commit ba04a0f

File tree

2 files changed

+146
-11
lines changed

2 files changed

+146
-11
lines changed

src/mcp/server/fastmcp/server.py

+34-11
Original file line numberDiff line numberDiff line change
@@ -624,19 +624,42 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send):
624624
)
625625
)
626626

627-
routes.append(
628-
Route(
629-
self.settings.sse_path,
630-
endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
631-
methods=["GET"],
627+
# When auth is not configured, we shouldn't require auth
628+
if self._auth_server_provider:
629+
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
630+
routes.append(
631+
Route(
632+
self.settings.sse_path,
633+
endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
634+
methods=["GET"],
635+
)
632636
)
633-
)
634-
routes.append(
635-
Mount(
636-
self.settings.message_path,
637-
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
637+
routes.append(
638+
Mount(
639+
self.settings.message_path,
640+
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
641+
)
642+
)
643+
else:
644+
# Auth is disabled, no need for RequireAuthMiddleware
645+
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
646+
async def sse_endpoint(request: Request) -> None:
647+
# Convert the Starlette request to ASGI parameters
648+
await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
649+
650+
routes.append(
651+
Route(
652+
self.settings.sse_path,
653+
endpoint=sse_endpoint,
654+
methods=["GET"],
655+
)
656+
)
657+
routes.append(
658+
Mount(
659+
self.settings.message_path,
660+
app=sse.handle_post_message,
661+
)
638662
)
639-
)
640663
# mount these routes last, so they have the lowest route matching precedence
641664
routes.extend(self._custom_starlette_routes)
642665

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""
2+
Integration tests for FastMCP server functionality.
3+
4+
These tests validate the proper functioning of FastMCP in various configurations,
5+
including with and without authentication.
6+
"""
7+
8+
import multiprocessing
9+
import socket
10+
import time
11+
from collections.abc import Generator
12+
13+
import pytest
14+
import uvicorn
15+
16+
from mcp.client.session import ClientSession
17+
from mcp.client.sse import sse_client
18+
from mcp.server.fastmcp import FastMCP
19+
from mcp.types import InitializeResult, TextContent
20+
21+
22+
@pytest.fixture
23+
def server_port() -> int:
24+
"""Get a free port for testing."""
25+
with socket.socket() as s:
26+
s.bind(("127.0.0.1", 0))
27+
return s.getsockname()[1]
28+
29+
30+
@pytest.fixture
31+
def server_url(server_port: int) -> str:
32+
"""Get the server URL for testing."""
33+
return f"http://127.0.0.1:{server_port}"
34+
35+
36+
# Create a function to make the FastMCP server app
37+
def make_fastmcp_app():
38+
"""Create a FastMCP server without auth settings."""
39+
from starlette.applications import Starlette
40+
41+
mcp = FastMCP(name="NoAuthServer")
42+
43+
# Add a simple tool
44+
@mcp.tool(description="A simple echo tool")
45+
def echo(message: str) -> str:
46+
return f"Echo: {message}"
47+
48+
# Create the SSE app
49+
app: Starlette = mcp.sse_app()
50+
51+
return mcp, app
52+
53+
54+
def run_server(server_port: int) -> None:
55+
"""Run the server."""
56+
_, app = make_fastmcp_app()
57+
server = uvicorn.Server(
58+
config=uvicorn.Config(
59+
app=app, host="127.0.0.1", port=server_port, log_level="error"
60+
)
61+
)
62+
print(f"Starting server on port {server_port}")
63+
server.run()
64+
65+
66+
@pytest.fixture()
67+
def server(server_port: int) -> Generator[None, None, None]:
68+
"""Start the server in a separate process and clean up after the test."""
69+
proc = multiprocessing.Process(target=run_server, args=(server_port,), daemon=True)
70+
print("Starting server process")
71+
proc.start()
72+
73+
# Wait for server to be running
74+
max_attempts = 20
75+
attempt = 0
76+
print("Waiting for server to start")
77+
while attempt < max_attempts:
78+
try:
79+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
80+
s.connect(("127.0.0.1", server_port))
81+
break
82+
except ConnectionRefusedError:
83+
time.sleep(0.1)
84+
attempt += 1
85+
else:
86+
raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
87+
88+
yield
89+
90+
print("Killing server")
91+
proc.kill()
92+
proc.join(timeout=2)
93+
if proc.is_alive():
94+
print("Server process failed to terminate")
95+
96+
97+
@pytest.mark.anyio
98+
async def test_fastmcp_without_auth(server: None, server_url: str) -> None:
99+
"""Test that FastMCP works when auth settings are not provided."""
100+
# Connect to the server
101+
async with sse_client(server_url + "/sse") as streams:
102+
async with ClientSession(*streams) as session:
103+
# Test initialization
104+
result = await session.initialize()
105+
assert isinstance(result, InitializeResult)
106+
assert result.serverInfo.name == "NoAuthServer"
107+
108+
# Test that we can call tools without authentication
109+
tool_result = await session.call_tool("echo", {"message": "hello"})
110+
assert len(tool_result.content) == 1
111+
assert isinstance(tool_result.content[0], TextContent)
112+
assert tool_result.content[0].text == "Echo: hello"

0 commit comments

Comments
 (0)