Merge pull request #1138 from strawgate/run-mcp-config

This commit is contained in:
Jeremiah Lowin 2025-07-19 21:18:40 -04:00 committed by GitHub
commit 1ea3ee82fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 124 additions and 8 deletions

View file

@ -57,10 +57,11 @@ By default, this command runs the server directly in your current Python environ
#### Server Specification
<VersionBadge version="2.3.5" />
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
<Tip>
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.

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

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