From c6501c10604433b3ffd05dd1db1907d0dc443a0c Mon Sep 17 00:00:00 2001 From: William Easton Date: Sun, 13 Jul 2025 15:19:57 -0500 Subject: [PATCH 1/6] Support running fastmcp directly with an MCPConfig --- src/fastmcp/cli/cli.py | 5 +++-- src/fastmcp/cli/run.py | 18 +++++++++++++++- tests/cli/test_run.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index a7ecf771f..466b64aca 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -291,16 +291,17 @@ def run( ) -> None: """Run an MCP server or connect to a remote one. - The server can be specified in three ways: + The server can be specified in four ways: 1. Module approach: server.py - runs the module directly, looking for an object named 'mcp', 'server', or 'app' 2. Import approach: server.py:app - imports and runs the specified server object 3. URL approach: http://server-url - connects to a remote server and creates a proxy + 4. MCPConfig file: mcp.json - runs the MCPConfig file directly Server arguments can be passed after -- : fastmcp run server.py -- --config config.json --debug Args: - server_spec: Python file, object specification (file:obj), or URL + server_spec: Python file, object specification (file:obj), MCPConfig file, or URL """ # TODO: Handle server_args from extra context server_args = [] # Will need to handle this with Cyclopts context diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 373d9cfbb..1fa173307 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -6,6 +6,7 @@ import sys from pathlib import Path from typing import Any, Literal +from fastmcp.server.server import FastMCP from fastmcp.utilities.logging import get_logger logger = get_logger("cli.run") @@ -142,6 +143,19 @@ def create_client_server(url: str) -> Any: sys.exit(1) +def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]: + """Create a FastMCP server from a MCPConfig.""" + from fastmcp import FastMCP + from fastmcp.client import Client + from fastmcp.client.transports import MCPConfigTransport + from fastmcp.mcp_config import MCPConfig + + mcp_config = MCPConfig.from_file(mcp_config_path) + client = Client[MCPConfigTransport](mcp_config) + server = FastMCP.as_proxy(client) + return server + + def import_server_with_args( file: Path, server_object: str | None = None, server_args: list[str] | None = None ) -> Any: @@ -179,7 +193,7 @@ def run_command( """Run a MCP server or connect to a remote one. Args: - server_spec: Python file, object specification (file:obj), or URL + server_spec: Python file, object specification (file:obj), MCPConfig file, or URL transport: Transport protocol to use host: Host to bind to when using http transport port: Port to bind to when using http transport @@ -192,6 +206,8 @@ def run_command( # Handle URL case server = create_client_server(server_spec) logger.debug(f"Created client proxy server for {server_spec}") + elif server_spec.endswith(".json"): + server = create_mcp_config_server(Path(server_spec)) else: # Handle file case file, server_object = parse_file_path(server_spec) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 7a685ce09..af4f36208 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,10 +1,16 @@ +import inspect + import pytest from fastmcp.cli.run import ( + create_mcp_config_server, import_server, is_url, parse_file_path, ) +from fastmcp.client.client import Client +from fastmcp.client.transports import FastMCPTransport +from fastmcp.server.server import FastMCP class TestUrlDetection: @@ -80,6 +86,49 @@ class TestFilePathParsing: assert exc_info.value.code == 1 +class TestMCPConfig: + """Test MCPConfig functionality.""" + + async def test_run_mcp_config(self, tmp_path): + """Test creating a server from an MCPConfig file.""" + server_script = inspect.cleandoc(""" + from fastmcp import FastMCP + + mcp = FastMCP() + + @mcp.tool + def add(a: int, b: int) -> int: + return a + b + + if __name__ == '__main__': + mcp.run() + """) + + script_path = tmp_path / "test.py" + script_path.write_text(server_script) + + mcp_config_path = tmp_path / "mcp_config.json" + mcp_config_str = f""" + {{ + "mcpServers": {{ + "test_server": {{ + "command": "python", + "args": ["{str(script_path)}"] + }} + }} + }} + """ + mcp_config_path.write_text(mcp_config_str) + + server: FastMCP[None] = create_mcp_config_server(mcp_config_path) + + client = Client[FastMCPTransport](server) + + async with client: + tools = await client.list_tools() + assert len(tools) == 1 + + class TestServerImport: """Test server import functionality using real files.""" From 91903bcbe684f4a38e64b25325acde5a288c0a64 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sun, 13 Jul 2025 15:21:06 -0500 Subject: [PATCH 2/6] update docstring --- src/fastmcp/cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 466b64aca..4e82efae2 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -295,7 +295,7 @@ def run( 1. Module approach: server.py - runs the module directly, looking for an object named 'mcp', 'server', or 'app' 2. Import approach: server.py:app - imports and runs the specified server object 3. URL approach: http://server-url - connects to a remote server and creates a proxy - 4. MCPConfig file: mcp.json - runs the MCPConfig file directly + 4. MCPConfig file: mcp.json - runs as a proxy server for the MCP Servers in the MCPConfig file Server arguments can be passed after -- : fastmcp run server.py -- --config config.json --debug From 7532e1a8ee61d0dffeef9827390cfb83dfb23935 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sun, 13 Jul 2025 15:52:24 -0500 Subject: [PATCH 3/6] update for windows? --- tests/cli/test_run.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index af4f36208..9ea0fde73 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,4 +1,5 @@ import inspect +from pathlib import Path import pytest @@ -10,6 +11,7 @@ from fastmcp.cli.run import ( ) from fastmcp.client.client import Client from fastmcp.client.transports import FastMCPTransport +from fastmcp.mcp_config import MCPConfig, StdioMCPServer from fastmcp.server.server import FastMCP @@ -89,7 +91,7 @@ class TestFilePathParsing: class TestMCPConfig: """Test MCPConfig functionality.""" - async def test_run_mcp_config(self, tmp_path): + async def test_run_mcp_config(self, tmp_path: Path): """Test creating a server from an MCPConfig file.""" server_script = inspect.cleandoc(""" from fastmcp import FastMCP @@ -104,21 +106,17 @@ class TestMCPConfig: mcp.run() """) - script_path = tmp_path / "test.py" + script_path: Path = tmp_path / "test.py" script_path.write_text(server_script) mcp_config_path = tmp_path / "mcp_config.json" - mcp_config_str = f""" - {{ - "mcpServers": {{ - "test_server": {{ - "command": "python", - "args": ["{str(script_path)}"] - }} - }} - }} - """ - mcp_config_path.write_text(mcp_config_str) + + mcp_config = MCPConfig( + mcpServers={ + "test_server": StdioMCPServer(command="python", args=[str(script_path)]) + } + ) + mcp_config.write_to_file(mcp_config_path) server: FastMCP[None] = create_mcp_config_server(mcp_config_path) From ed57b4cd42698582c754e197850b8d3a96ff031f Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:08:18 -0400 Subject: [PATCH 4/6] Simplify proxy load --- src/fastmcp/cli/cli.py | 8 ++++---- src/fastmcp/cli/run.py | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 4e82efae2..3f06e507b 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -292,10 +292,10 @@ def run( """Run an MCP server or connect to a remote one. The server can be specified in four ways: - 1. Module approach: server.py - runs the module directly, looking for an object named 'mcp', 'server', or 'app' - 2. Import approach: server.py:app - imports and runs the specified server object - 3. URL approach: http://server-url - connects to a remote server and creates a proxy - 4. MCPConfig file: mcp.json - runs as a proxy server for the MCP Servers in the MCPConfig file + 1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app' + 2. Import approach: "server.py:app" - imports and runs the specified server object + 3. URL approach: "http://server-url" - connects to a remote server and creates a proxy + 4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file Server arguments can be passed after -- : fastmcp run server.py -- --config config.json --debug diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 1fa173307..1efd34e30 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -1,6 +1,7 @@ """FastMCP run command implementation with enhanced type hints.""" import importlib.util +import json import re import sys from pathlib import Path @@ -146,13 +147,11 @@ def create_client_server(url: str) -> Any: def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]: """Create a FastMCP server from a MCPConfig.""" from fastmcp import FastMCP - from fastmcp.client import Client - from fastmcp.client.transports import MCPConfigTransport - from fastmcp.mcp_config import MCPConfig - mcp_config = MCPConfig.from_file(mcp_config_path) - client = Client[MCPConfigTransport](mcp_config) - server = FastMCP.as_proxy(client) + with mcp_config_path.open() as src: + mcp_config = json.load(src) + + server = FastMCP.as_proxy(mcp_config) return server From c9255f483fc588ab2c802225ede529f522402d84 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:11:14 -0400 Subject: [PATCH 5/6] Add validation test --- tests/cli/test_run.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 9ea0fde73..6ae06f1eb 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,7 +1,9 @@ import inspect +import json from pathlib import Path import pytest +from pydantic import ValidationError from fastmcp.cli.run import ( create_mcp_config_server, @@ -126,6 +128,18 @@ class TestMCPConfig: tools = await client.list_tools() assert len(tools) == 1 + async def test_validate_mcp_config(self, tmp_path: Path): + """Test creating a server from an MCPConfig file.""" + + mcp_config_path = tmp_path / "mcp_config.json" + + mcp_config = {"mcpServers": {"test_server": dict(x=1, y=2)}} + with mcp_config_path.open("w") as f: + json.dump(mcp_config, f) + + with pytest.raises(ValidationError, match="validation errors for MCPConfig"): + create_mcp_config_server(mcp_config_path) + class TestServerImport: """Test server import functionality using real files.""" From 74518fb7b26263b14ab8d61563fb7d1ddd8f39b4 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:15:38 -0400 Subject: [PATCH 6/6] Add docs --- docs/patterns/cli.mdx | 41 ++++++++++++++++++++++++++++++++++++++- src/fastmcp/mcp_config.py | 2 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index dce1b5b32..0f8bfdb99 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -53,10 +53,11 @@ This command runs the server directly in your current Python environment. You ar #### Server Specification -The server can be specified in three ways: +The server can be specified in four ways: 1. `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found. 2. `server.py:custom_name` - imports and uses the specified server object 3. `http://server-url/path` or `https://server-url/path` - connects to a remote server and creates a proxy +4. `mcp.json` - runs servers defined in a standard MCP configuration file When using `fastmcp run` with a local file, it **ignores** the `if __name__ == "__main__"` block entirely. Instead, it finds your server object and calls its `run()` method directly with the transport options you specify. This means you can use `fastmcp run` to override the transport specified in your code. @@ -98,6 +99,44 @@ fastmcp run https://example.com/mcp-server fastmcp run https://example.com/mcp-server --log-level DEBUG ``` +#### Running MCP Configuration Files + +FastMCP can run servers defined in standard MCP configuration files (typically named `mcp.json`). When you run an mcp.json file, FastMCP creates a proxy server that runs all the servers referenced in the configuration. + +**Example mcp.json:** +```json +{ + "mcpServers": { + "fetch": { + "command": "uvx", + "args": [ + "mcp-server-fetch" + ] + }, + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/Users/username/Documents" + ] + } + } +} +``` + +**Run the configuration:** +```bash +# Run with default stdio transport +fastmcp run mcp.json + +# Run with HTTP transport on custom port +fastmcp run mcp.json --transport http --port 8080 + +# Run with SSE transport +fastmcp run mcp.json --transport sse +``` + ### `dev` Run a MCP server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for testing. diff --git a/src/fastmcp/mcp_config.py b/src/fastmcp/mcp_config.py index 8d5576ec5..e6758e0c7 100644 --- a/src/fastmcp/mcp_config.py +++ b/src/fastmcp/mcp_config.py @@ -270,7 +270,7 @@ class MCPConfig(BaseModel): if content := file_path.read_text().strip(): return cls.model_validate_json(content) - return cls(mcpServers={}) + raise ValueError(f"No MCP servers defined in the config: {file_path}") class CanonicalMCPConfig(MCPConfig):