From 7f4119dcf6237c45d19794d039608da20ce830cb Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Tue, 14 Oct 2025 14:24:51 -0400 Subject: [PATCH] Fix asyncio error when running FastMCP 1.x servers (#2084) Co-authored-by: Claude --- src/fastmcp/cli/run.py | 21 +++-- tests/cli/test_run.py | 204 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 8 deletions(-) diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 5c4f386db..f03578a11 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -172,7 +172,7 @@ async def run_command( # handle v1 servers if isinstance(server, FastMCP1x): - run_v1_server(server, host=host, port=port, transport=transport) + await run_v1_server_async(server, host=host, port=port, transport=transport) return kwargs = {} @@ -197,24 +197,29 @@ async def run_command( sys.exit(1) -def run_v1_server( +async def run_v1_server_async( server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None, ) -> None: - from functools import partial + """Run a FastMCP 1.x server using async methods. + Args: + server: FastMCP 1.x server instance + host: Host to bind to + port: Port to bind to + transport: Transport protocol to use + """ if host: server.settings.host = host if port: server.settings.port = port + match transport: case "stdio": - runner = partial(server.run) + await server.run_stdio_async() case "http" | "streamable-http" | None: - runner = partial(server.run, transport="streamable-http") + await server.run_streamable_http_async() case "sse": - runner = partial(server.run, transport="sse") - - runner() + await server.run_sse_async() diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 7773684a7..e067568fd 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -274,6 +274,210 @@ mcp = fastmcp.FastMCP("TestServer") assert exc_info.value.code == 1 +class TestV1ServerAsync: + """Test FastMCP 1.x server async support.""" + + async def test_run_v1_server_stdio(self, tmp_path): + """Test that v1 server uses async stdio method.""" + from unittest.mock import AsyncMock, patch + + from mcp.server.fastmcp import FastMCP as FastMCP1x + + from fastmcp.cli.run import run_command + + # Create a v1 FastMCP server file with both sync and async tools + test_file = tmp_path / "v1_server.py" + test_file.write_text(""" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("V1Server") + +@mcp.tool() +def sync_echo(text: str) -> str: + '''Sync tool for testing''' + return f"sync: {text}" + +@mcp.tool() +async def async_echo(text: str) -> str: + '''Async tool for testing''' + return f"async: {text}" +""") + + # Mock the async run method + with patch.object( + FastMCP1x, "run_stdio_async", new_callable=AsyncMock + ) as run_mock: + await run_command(str(test_file), transport="stdio") + run_mock.assert_called_once() + + async def test_run_v1_server_http(self, tmp_path): + """Test that v1 server uses async http method.""" + from unittest.mock import AsyncMock, patch + + from mcp.server.fastmcp import FastMCP as FastMCP1x + + from fastmcp.cli.run import run_command + + # Create a v1 FastMCP server file with both sync and async tools + test_file = tmp_path / "v1_server.py" + test_file.write_text(""" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("V1Server") + +@mcp.tool() +def sync_echo(text: str) -> str: + '''Sync tool for testing''' + return f"sync: {text}" + +@mcp.tool() +async def async_echo(text: str) -> str: + '''Async tool for testing''' + return f"async: {text}" +""") + + # Mock the async run method + with patch.object( + FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock + ) as run_mock: + await run_command(str(test_file), transport="http") + run_mock.assert_called_once() + + async def test_run_v1_server_streamable_http(self, tmp_path): + """Test that v1 server uses async streamable-http method.""" + from unittest.mock import AsyncMock, patch + + from mcp.server.fastmcp import FastMCP as FastMCP1x + + from fastmcp.cli.run import run_command + + # Create a v1 FastMCP server file with both sync and async tools + test_file = tmp_path / "v1_server.py" + test_file.write_text(""" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("V1Server") + +@mcp.tool() +def sync_echo(text: str) -> str: + '''Sync tool for testing''' + return f"sync: {text}" + +@mcp.tool() +async def async_echo(text: str) -> str: + '''Async tool for testing''' + return f"async: {text}" +""") + + # Mock the async run method + with patch.object( + FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock + ) as run_mock: + await run_command(str(test_file), transport="streamable-http") + run_mock.assert_called_once() + + async def test_run_v1_server_sse(self, tmp_path): + """Test that v1 server uses async sse method.""" + from unittest.mock import AsyncMock, patch + + from mcp.server.fastmcp import FastMCP as FastMCP1x + + from fastmcp.cli.run import run_command + + # Create a v1 FastMCP server file with both sync and async tools + test_file = tmp_path / "v1_server.py" + test_file.write_text(""" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("V1Server") + +@mcp.tool() +def sync_echo(text: str) -> str: + '''Sync tool for testing''' + return f"sync: {text}" + +@mcp.tool() +async def async_echo(text: str) -> str: + '''Async tool for testing''' + return f"async: {text}" +""") + + # Mock the async run method + with patch.object( + FastMCP1x, "run_sse_async", new_callable=AsyncMock + ) as run_mock: + await run_command(str(test_file), transport="sse") + run_mock.assert_called_once() + + async def test_run_v1_server_default_transport(self, tmp_path): + """Test that v1 server uses streamable-http by default.""" + from unittest.mock import AsyncMock, patch + + from mcp.server.fastmcp import FastMCP as FastMCP1x + + from fastmcp.cli.run import run_command + + # Create a v1 FastMCP server file with both sync and async tools + test_file = tmp_path / "v1_server.py" + test_file.write_text(""" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("V1Server") + +@mcp.tool() +def sync_echo(text: str) -> str: + '''Sync tool for testing''' + return f"sync: {text}" + +@mcp.tool() +async def async_echo(text: str) -> str: + '''Async tool for testing''' + return f"async: {text}" +""") + + # Mock the async run method + with patch.object( + FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock + ) as run_mock: + await run_command(str(test_file)) + run_mock.assert_called_once() + + async def test_run_v1_server_with_host_port(self, tmp_path): + """Test that v1 server receives host/port settings.""" + from unittest.mock import AsyncMock, patch + + from mcp.server.fastmcp import FastMCP as FastMCP1x + + from fastmcp.cli.run import run_command + + # Create a v1 FastMCP server file with both sync and async tools + test_file = tmp_path / "v1_server.py" + test_file.write_text(""" +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("V1Server") + +@mcp.tool() +def sync_echo(text: str) -> str: + '''Sync tool for testing''' + return f"sync: {text}" + +@mcp.tool() +async def async_echo(text: str) -> str: + '''Async tool for testing''' + return f"async: {text}" +""") + + # Mock the async run method + with patch.object( + FastMCP1x, "run_streamable_http_async", new_callable=AsyncMock + ) as run_mock: + await run_command( + str(test_file), transport="http", host="0.0.0.0", port=9000 + ) + run_mock.assert_called_once() + + class TestSkipSource: """Test the --skip-source functionality."""