Jeremiah Lowin commited on
Commit
1880137
·
unverified ·
2 Parent(s): dd7600a67965fd

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

Browse files
docs/patterns/cli.mdx CHANGED
@@ -57,10 +57,11 @@ By default, this command runs the server directly in your current Python environ
57
  #### Server Specification
58
  <VersionBadge version="2.3.5" />
59
 
60
- The server can be specified in three ways:
61
  1. `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
62
  2. `server.py:custom_name` - imports and uses the specified server object
63
  3. `http://server-url/path` or `https://server-url/path` - connects to a remote server and creates a proxy
 
64
 
65
  <Tip>
66
  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
114
  fastmcp run server.py --with-requirements requirements.txt
115
  ```
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  ### `dev`
118
 
119
  Run a MCP server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for testing.
 
57
  #### Server Specification
58
  <VersionBadge version="2.3.5" />
59
 
60
+ The server can be specified in four ways:
61
  1. `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
62
  2. `server.py:custom_name` - imports and uses the specified server object
63
  3. `http://server-url/path` or `https://server-url/path` - connects to a remote server and creates a proxy
64
+ 4. `mcp.json` - runs servers defined in a standard MCP configuration file
65
 
66
  <Tip>
67
  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.
 
115
  fastmcp run server.py --with-requirements requirements.txt
116
  ```
117
 
118
+ #### Running MCP Configuration Files
119
+
120
+ 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.
121
+
122
+ **Example mcp.json:**
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "fetch": {
127
+ "command": "uvx",
128
+ "args": [
129
+ "mcp-server-fetch"
130
+ ]
131
+ },
132
+ "filesystem": {
133
+ "command": "npx",
134
+ "args": [
135
+ "-y",
136
+ "@modelcontextprotocol/server-filesystem",
137
+ "/Users/username/Documents"
138
+ ]
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ **Run the configuration:**
145
+ ```bash
146
+ # Run with default stdio transport
147
+ fastmcp run mcp.json
148
+
149
+ # Run with HTTP transport on custom port
150
+ fastmcp run mcp.json --transport http --port 8080
151
+
152
+ # Run with SSE transport
153
+ fastmcp run mcp.json --transport sse
154
+ ```
155
+
156
  ### `dev`
157
 
158
  Run a MCP server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) for testing.
src/fastmcp/cli/cli.py CHANGED
@@ -361,16 +361,17 @@ def run(
361
  ) -> None:
362
  """Run an MCP server or connect to a remote one.
363
 
364
- The server can be specified in three ways:
365
- 1. Module approach: server.py - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
366
- 2. Import approach: server.py:app - imports and runs the specified server object
367
- 3. URL approach: http://server-url - connects to a remote server and creates a proxy
 
368
 
369
  Server arguments can be passed after -- :
370
  fastmcp run server.py -- --config config.json --debug
371
 
372
  Args:
373
- server_spec: Python file, object specification (file:obj), or URL
374
  """
375
  # TODO: Handle server_args from extra context
376
  server_args = [] # Will need to handle this with Cyclopts context
 
361
  ) -> None:
362
  """Run an MCP server or connect to a remote one.
363
 
364
+ The server can be specified in four ways:
365
+ 1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
366
+ 2. Import approach: "server.py:app" - imports and runs the specified server object
367
+ 3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
368
+ 4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
369
 
370
  Server arguments can be passed after -- :
371
  fastmcp run server.py -- --config config.json --debug
372
 
373
  Args:
374
+ server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
375
  """
376
  # TODO: Handle server_args from extra context
377
  server_args = [] # Will need to handle this with Cyclopts context
src/fastmcp/cli/run.py CHANGED
@@ -1,12 +1,14 @@
1
  """FastMCP run command implementation with enhanced type hints."""
2
 
3
  import importlib.util
 
4
  import re
5
  import subprocess
6
  import sys
7
  from pathlib import Path
8
  from typing import Any, Literal
9
 
 
10
  from fastmcp.utilities.logging import get_logger
11
 
12
  logger = get_logger("cli.run")
@@ -221,6 +223,17 @@ def create_client_server(url: str) -> Any:
221
  sys.exit(1)
222
 
223
 
 
 
 
 
 
 
 
 
 
 
 
224
  def import_server_with_args(
225
  file: Path, server_object: str | None = None, server_args: list[str] | None = None
226
  ) -> Any:
@@ -259,7 +272,7 @@ def run_command(
259
  """Run a MCP server or connect to a remote one.
260
 
261
  Args:
262
- server_spec: Python file, object specification (file:obj), or URL
263
  transport: Transport protocol to use
264
  host: Host to bind to when using http transport
265
  port: Port to bind to when using http transport
@@ -273,6 +286,8 @@ def run_command(
273
  # Handle URL case
274
  server = create_client_server(server_spec)
275
  logger.debug(f"Created client proxy server for {server_spec}")
 
 
276
  else:
277
  # Handle file case
278
  file, server_object = parse_file_path(server_spec)
 
1
  """FastMCP run command implementation with enhanced type hints."""
2
 
3
  import importlib.util
4
+ import json
5
  import re
6
  import subprocess
7
  import sys
8
  from pathlib import Path
9
  from typing import Any, Literal
10
 
11
+ from fastmcp.server.server import FastMCP
12
  from fastmcp.utilities.logging import get_logger
13
 
14
  logger = get_logger("cli.run")
 
223
  sys.exit(1)
224
 
225
 
226
+ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
227
+ """Create a FastMCP server from a MCPConfig."""
228
+ from fastmcp import FastMCP
229
+
230
+ with mcp_config_path.open() as src:
231
+ mcp_config = json.load(src)
232
+
233
+ server = FastMCP.as_proxy(mcp_config)
234
+ return server
235
+
236
+
237
  def import_server_with_args(
238
  file: Path, server_object: str | None = None, server_args: list[str] | None = None
239
  ) -> Any:
 
272
  """Run a MCP server or connect to a remote one.
273
 
274
  Args:
275
+ server_spec: Python file, object specification (file:obj), MCPConfig file, or URL
276
  transport: Transport protocol to use
277
  host: Host to bind to when using http transport
278
  port: Port to bind to when using http transport
 
286
  # Handle URL case
287
  server = create_client_server(server_spec)
288
  logger.debug(f"Created client proxy server for {server_spec}")
289
+ elif server_spec.endswith(".json"):
290
+ server = create_mcp_config_server(Path(server_spec))
291
  else:
292
  # Handle file case
293
  file, server_object = parse_file_path(server_spec)
src/fastmcp/mcp_config.py CHANGED
@@ -270,7 +270,7 @@ class MCPConfig(BaseModel):
270
  if content := file_path.read_text().strip():
271
  return cls.model_validate_json(content)
272
 
273
- return cls(mcpServers={})
274
 
275
 
276
  class CanonicalMCPConfig(MCPConfig):
 
270
  if content := file_path.read_text().strip():
271
  return cls.model_validate_json(content)
272
 
273
+ raise ValueError(f"No MCP servers defined in the config: {file_path}")
274
 
275
 
276
  class CanonicalMCPConfig(MCPConfig):
tests/cli/test_run.py CHANGED
@@ -1,10 +1,20 @@
 
 
 
 
1
  import pytest
 
2
 
3
  from fastmcp.cli.run import (
 
4
  import_server,
5
  is_url,
6
  parse_file_path,
7
  )
 
 
 
 
8
 
9
 
10
  class TestUrlDetection:
@@ -80,6 +90,57 @@ class TestFilePathParsing:
80
  assert exc_info.value.code == 1
81
 
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  class TestServerImport:
84
  """Test server import functionality using real files."""
85
 
 
1
+ import inspect
2
+ import json
3
+ from pathlib import Path
4
+
5
  import pytest
6
+ from pydantic import ValidationError
7
 
8
  from fastmcp.cli.run import (
9
+ create_mcp_config_server,
10
  import_server,
11
  is_url,
12
  parse_file_path,
13
  )
14
+ from fastmcp.client.client import Client
15
+ from fastmcp.client.transports import FastMCPTransport
16
+ from fastmcp.mcp_config import MCPConfig, StdioMCPServer
17
+ from fastmcp.server.server import FastMCP
18
 
19
 
20
  class TestUrlDetection:
 
90
  assert exc_info.value.code == 1
91
 
92
 
93
+ class TestMCPConfig:
94
+ """Test MCPConfig functionality."""
95
+
96
+ async def test_run_mcp_config(self, tmp_path: Path):
97
+ """Test creating a server from an MCPConfig file."""
98
+ server_script = inspect.cleandoc("""
99
+ from fastmcp import FastMCP
100
+
101
+ mcp = FastMCP()
102
+
103
+ @mcp.tool
104
+ def add(a: int, b: int) -> int:
105
+ return a + b
106
+
107
+ if __name__ == '__main__':
108
+ mcp.run()
109
+ """)
110
+
111
+ script_path: Path = tmp_path / "test.py"
112
+ script_path.write_text(server_script)
113
+
114
+ mcp_config_path = tmp_path / "mcp_config.json"
115
+
116
+ mcp_config = MCPConfig(
117
+ mcpServers={
118
+ "test_server": StdioMCPServer(command="python", args=[str(script_path)])
119
+ }
120
+ )
121
+ mcp_config.write_to_file(mcp_config_path)
122
+
123
+ server: FastMCP[None] = create_mcp_config_server(mcp_config_path)
124
+
125
+ client = Client[FastMCPTransport](server)
126
+
127
+ async with client:
128
+ tools = await client.list_tools()
129
+ assert len(tools) == 1
130
+
131
+ async def test_validate_mcp_config(self, tmp_path: Path):
132
+ """Test creating a server from an MCPConfig file."""
133
+
134
+ mcp_config_path = tmp_path / "mcp_config.json"
135
+
136
+ mcp_config = {"mcpServers": {"test_server": dict(x=1, y=2)}}
137
+ with mcp_config_path.open("w") as f:
138
+ json.dump(mcp_config, f)
139
+
140
+ with pytest.raises(ValidationError, match="validation errors for MCPConfig"):
141
+ create_mcp_config_server(mcp_config_path)
142
+
143
+
144
  class TestServerImport:
145
  """Test server import functionality using real files."""
146