From 731366f74699bac4a1c9c6eafac2a3a9a72420f4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:08:24 -0400 Subject: [PATCH 1/2] add transport kwargs --- src/fastmcp/server/server.py | 14 +++++++++----- uv.lock | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index a6e7119b2..4ac62bcfe 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -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.""" diff --git a/uv.lock b/uv.lock index d95c7312d..7f89175dd 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, From ce9500f87e18baa8be16bc1bf0d9a0db8e2bad58 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:12:48 -0400 Subject: [PATCH 2/2] Add CLI args --- docs/servers/fastmcp.mdx | 51 ++++++++++++++++++++++---- src/fastmcp/cli/cli.py | 32 +++++++++++++++++ tests/cli/test_run.py | 77 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 tests/cli/test_run.py diff --git a/docs/servers/fastmcp.mdx b/docs/servers/fastmcp.mdx index cf0fef707..c86f22ed5 100644 --- a/docs/servers/fastmcp.mdx +++ b/docs/servers/fastmcp.mdx @@ -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. diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 4e2a61818..4ee9fdbb1 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -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) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py new file mode 100644 index 000000000..58bd3c097 --- /dev/null +++ b/tests/cli/test_run.py @@ -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