diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index e394b99f5..69ef01131 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -57,10 +57,11 @@ By default, this command runs the server directly in your current Python environ #### 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. @@ -114,6 +115,44 @@ fastmcp run server.py --project /path/to/project fastmcp run server.py --with-requirements requirements.txt ``` +#### 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/cli/cli.py b/src/fastmcp/cli/cli.py index 1d0a45973..11730d6b1 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -361,16 +361,17 @@ def run( ) -> None: """Run an MCP server or connect to a remote one. - The server can be specified in three 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 + 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 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 f0464d2e4..a31b41e16 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -1,12 +1,14 @@ """FastMCP run command implementation with enhanced type hints.""" import importlib.util +import json import re import subprocess 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") @@ -221,6 +223,17 @@ 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 + + with mcp_config_path.open() as src: + mcp_config = json.load(src) + + server = FastMCP.as_proxy(mcp_config) + return server + + def import_server_with_args( file: Path, server_object: str | None = None, server_args: list[str] | None = None ) -> Any: @@ -259,7 +272,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 @@ -273,6 +286,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/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): diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 7a685ce09..6ae06f1eb 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,10 +1,20 @@ +import inspect +import json +from pathlib import Path + import pytest +from pydantic import ValidationError 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.mcp_config import MCPConfig, StdioMCPServer +from fastmcp.server.server import FastMCP class TestUrlDetection: @@ -80,6 +90,57 @@ class TestFilePathParsing: assert exc_info.value.code == 1 +class TestMCPConfig: + """Test MCPConfig functionality.""" + + 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 + + mcp = FastMCP() + + @mcp.tool + def add(a: int, b: int) -> int: + return a + b + + if __name__ == '__main__': + mcp.run() + """) + + script_path: Path = tmp_path / "test.py" + script_path.write_text(server_script) + + mcp_config_path = tmp_path / "mcp_config.json" + + 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) + + client = Client[FastMCPTransport](server) + + async with client: + 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."""