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."""