Merge pull request #161 from jlowin/transport-kwargs

Add transport kwargs for mcp.run() and fastmcp run
This commit is contained in:
Jeremiah Lowin 2025-04-14 22:15:11 -04:00 committed by GitHub
commit 059c0889fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 163 additions and 13 deletions

View file

@ -111,7 +111,12 @@ def greet(name: str) -> str:
if __name__ == "__main__":
# This code only runs when the file is executed directly
# Basic run with default settings (stdio transport)
mcp.run()
# Or with specific transport and parameters
# mcp.run(transport="sse", host="127.0.0.1", port=9000)
```
This pattern is important because:
@ -156,18 +161,47 @@ With SSE:
- The server stays running until explicitly terminated
- This is ideal for remote access to services
You can configure the host, port, and log level when running the server:
You can configure transport parameters directly when running the server:
```python
# Configure with parameters
mcp.run(transport="sse", host="127.0.0.1", port=8888)
# Configure with specific parameters
mcp.run(
transport="sse",
host="127.0.0.1", # Override default host
port=8888, # Override default port
log_level="debug" # Set logging level
)
# Or run asynchronously with the same parameters
# You can also run asynchronously with the same parameters
import asyncio
asyncio.run(mcp.run_sse_async(host="127.0.0.1", port=8888, log_level="debug"))
asyncio.run(
mcp.run_sse_async(
host="127.0.0.1",
port=8888,
log_level="debug"
)
)
```
These parameters override any settings defined when creating the FastMCP instance.
Transport parameters passed to `run()` or `run_sse_async()` override any settings defined when creating the FastMCP instance. The most common parameters for SSE transport are:
- `host`: Host to bind to (default: "0.0.0.0")
- `port`: Port to bind to (default: 8000)
- `log_level`: Logging level (default: "INFO")
#### Advanced Transport Configuration
Under the hood, FastMCP's `run()` method accepts arbitrary keyword arguments (`**transport_kwargs`) that are passed to the transport-specific run methods:
```python
# For SSE transport, kwargs are passed to run_sse_async()
mcp.run(transport="sse", **transport_kwargs)
# For stdio transport, kwargs are passed to run_stdio_async()
mcp.run(transport="stdio", **transport_kwargs)
```
This means that any future transport-specific options will be automatically available through the same interface without requiring changes to your code.
### Using the FastMCP CLI
@ -180,8 +214,11 @@ fastmcp run my_server.py:mcp
# Explicitly specify a transport
fastmcp run my_server.py:mcp --transport sse
# Configure SSE transport
# Configure SSE transport with host and port
fastmcp run my_server.py:mcp --transport sse --host 127.0.0.1 --port 8888
# With log level
fastmcp run my_server.py:mcp --transport sse --log-level DEBUG
```
The CLI can dynamically find and run FastMCP server objects in your files, but including the `if __name__ == "__main__":` block ensures compatibility with all clients.

View file

@ -297,6 +297,29 @@ def run(
help="Transport protocol to use (stdio or sse)",
),
] = None,
host: Annotated[
str | None,
typer.Option(
"--host",
help="Host to bind to when using sse transport (default: 0.0.0.0)",
),
] = None,
port: Annotated[
int | None,
typer.Option(
"--port",
"-p",
help="Port to bind to when using sse transport (default: 8000)",
),
] = None,
log_level: Annotated[
str | None,
typer.Option(
"--log-level",
"-l",
help="Log level for sse transport (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
),
] = None,
) -> None:
"""Run a MCP server.
@ -316,6 +339,9 @@ def run(
"file": str(file),
"server_object": server_object,
"transport": transport,
"host": host,
"port": port,
"log_level": log_level,
},
)
@ -329,6 +355,12 @@ def run(
kwargs = {}
if transport:
kwargs["transport"] = transport
if host:
kwargs["host"] = host
if port:
kwargs["port"] = port
if log_level:
kwargs["log_level"] = log_level
server.run(**kwargs)

View file

@ -146,7 +146,9 @@ class FastMCP(Generic[LifespanResultT]):
def instructions(self) -> str | None:
return self._mcp_server.instructions
async def run_async(self, transport: Literal["stdio", "sse"] | None = None) -> None:
async def run_async(
self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
) -> None:
"""Run the FastMCP server asynchronously.
Args:
@ -158,18 +160,20 @@ class FastMCP(Generic[LifespanResultT]):
raise ValueError(f"Unknown transport: {transport}")
if transport == "stdio":
await self.run_stdio_async()
await self.run_stdio_async(**transport_kwargs)
else: # transport == "sse"
await self.run_sse_async()
await self.run_sse_async(**transport_kwargs)
def run(self, transport: Literal["stdio", "sse"] | None = None) -> None:
def run(
self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
) -> None:
"""Run the FastMCP server. Note this is a synchronous function.
Args:
transport: Transport protocol to use ("stdio" or "sse")
"""
logger.info(f'Starting server "{self.name}"...')
anyio.run(self.run_async, transport)
anyio.run(self.run_async, transport, **transport_kwargs)
def _setup_handlers(self) -> None:
"""Set up core MCP protocol handlers."""

77
tests/cli/test_run.py Normal file
View file

@ -0,0 +1,77 @@
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from typer.testing import CliRunner
from fastmcp import FastMCP
from fastmcp.cli.cli import app
@pytest.fixture
def server_file(tmp_path):
"""Create a simple server file for testing"""
server_path = tmp_path / "test_server.py"
server_path.write_text(
"""
from fastmcp import FastMCP
mcp = FastMCP(name="TestServer")
@mcp.tool()
def hello(name: str) -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()
"""
)
return server_path
def test_cli_run_transport_kwargs():
"""Test that transport_kwargs are correctly passed from CLI to server.run()"""
runner = CliRunner()
# Need to mock both the file parsing and the server import
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse_file_path,
patch("fastmcp.cli.cli._import_server") as mock_import_server,
):
# Make _parse_file_path return a fake path and server object
mock_parse_file_path.return_value = (Path("fake_server.py"), "mcp")
# Create a mock server with a mock run method
mock_server = FastMCP(name="MockServer")
mock_server.run = Mock()
# Make _import_server return our mock server
mock_import_server.return_value = mock_server
# Run the CLI command with transport_kwargs
result = runner.invoke(
app,
[
"run",
"fake_server.py",
"--transport",
"sse",
"--host",
"127.0.0.1",
"--port",
"9000",
"--log-level",
"DEBUG",
],
)
# Check that the run method was called with the correct kwargs
mock_server.run.assert_called_once_with(
transport="sse",
host="127.0.0.1",
port=9000,
log_level="DEBUG",
)
# Check CLI command succeeded
assert result.exit_code == 0

2
uv.lock generated
View file

@ -254,7 +254,7 @@ wheels = [
[[package]]
name = "fastmcp"
version = "2.1.2.dev2+c0de75e"
version = "2.1.2.dev9+55f3666"
source = { editable = "." }
dependencies = [
{ name = "dotenv" },