zzstoatzz commited on
Commit
fc7e104
·
1 Parent(s): cefca54

allow passing one or many `--server-arg` flags

Browse files
docs/deployment/running-server.mdx CHANGED
@@ -61,6 +61,19 @@ fastmcp dev server.py
61
 
62
  See the [CLI documentation](/patterns/cli) for detailed information about all available commands and options.
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  ## Transport Options
65
 
66
  Below is a comparison of available transport options to help you choose the right one for your needs:
@@ -157,117 +170,4 @@ New applications should use Streamable HTTP transport instead.
157
 
158
  Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects.
159
 
160
- To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse`) and message path (`/messages/`).
161
-
162
- <CodeGroup>
163
- ```python {6} server.py
164
- from fastmcp import FastMCP
165
-
166
- mcp = FastMCP()
167
-
168
- if __name__ == "__main__":
169
- mcp.run(transport="sse")
170
- ```
171
- ```python {3,7} client.py
172
- import asyncio
173
- from fastmcp import Client
174
- from fastmcp.client.transports import SSETransport
175
-
176
- async def example():
177
- async with Client(
178
- transport=SSETransport("http://127.0.0.1:8000/sse")
179
- ) as client:
180
- await client.ping()
181
-
182
- if __name__ == "__main__":
183
- asyncio.run(example())
184
- ```
185
- </CodeGroup>
186
-
187
- <Tip>
188
- Notice that the client in the above example uses an explicit `SSETransport` to connect to the server. FastMCP will attempt to infer the appropriate transport from the provided configuration, but HTTP URLs are assumed to be Streamable HTTP (as of FastMCP 2.3.0).
189
- </Tip>
190
-
191
- To customize the host, port, or log level, provide appropriate keyword arguments to the `run()` method. You can also adjust the SSE path (which clients should connect to) and the message POST endpoint (which clients use to send subsequent messages).
192
-
193
- <CodeGroup>
194
- ```python {8-12} server.py
195
- from fastmcp import FastMCP
196
-
197
- mcp = FastMCP()
198
-
199
- if __name__ == "__main__":
200
- mcp.run(
201
- transport="sse",
202
- host="127.0.0.1",
203
- port=4200,
204
- log_level="debug",
205
- path="/my-custom-sse-path",
206
- )
207
- ```
208
- ```python {7} client.py
209
- import asyncio
210
- from fastmcp import Client
211
- from fastmcp.client.transports import SSETransport
212
-
213
- async def example():
214
- async with Client(
215
- transport=SSETransport("http://127.0.0.1:4200/my-custom-sse-path")
216
- ) as client:
217
- await client.ping()
218
-
219
- if __name__ == "__main__":
220
- asyncio.run(example())
221
- ```
222
- </CodeGroup>
223
-
224
-
225
-
226
- ## Async Usage
227
-
228
- FastMCP provides both synchronous and asynchronous APIs for running your server. The `run()` method seen in previous examples is a synchronous method that internally uses `anyio.run()` to run the asynchronous server. For applications that are already running in an async context, FastMCP provides the `run_async()` method.
229
-
230
- ```python {10-12}
231
- from fastmcp import FastMCP
232
- import asyncio
233
-
234
- mcp = FastMCP(name="MyServer")
235
-
236
- @mcp.tool()
237
- def hello(name: str) -> str:
238
- return f"Hello, {name}!"
239
-
240
- async def main():
241
- # Use run_async() in async contexts
242
- await mcp.run_async(transport="streamable-http")
243
-
244
- if __name__ == "__main__":
245
- asyncio.run(main())
246
- ```
247
-
248
- <Warning>
249
- The `run()` method cannot be called from inside an async function because it already creates its own async event loop internally. If you attempt to call `run()` from inside an async function, you'll get an error about the event loop already running.
250
-
251
- Always use `run_async()` inside async functions and `run()` in synchronous contexts.
252
- </Warning>
253
-
254
- Both `run()` and `run_async()` accept the same transport arguments, so all the examples above apply to both methods.
255
-
256
- ## Custom Routes
257
-
258
- You can also add custom web routes to your FastMCP server, which will be exposed alongside the MCP endpoint. To do so, use the `@custom_route` decorator. Note that this is less flexible than using a full ASGI framework, but can be useful for adding simple endpoints like health checks to your standalone server.
259
-
260
- ```python
261
- from fastmcp import FastMCP
262
- from starlette.requests import Request
263
- from starlette.responses import PlainTextResponse
264
-
265
- mcp = FastMCP("MyServer")
266
-
267
- @mcp.custom_route("/health", methods=["GET"])
268
- async def health_check(request: Request) -> PlainTextResponse:
269
- return PlainTextResponse("OK")
270
-
271
- if __name__ == "__main__":
272
- mcp.run()
273
- ```
 
61
 
62
  See the [CLI documentation](/patterns/cli) for detailed information about all available commands and options.
63
 
64
+ ### Passing Arguments to Servers
65
+
66
+ <VersionBadge version="2.6.2" />
67
+
68
+ When servers accept command line arguments (using argparse, click, or other libraries), you can pass them using the `--server-arg` option:
69
+
70
+ ```bash
71
+ fastmcp run config_server.py --server-arg="--config" --server-arg="config.json"
72
+ fastmcp run database_server.py --server-arg="--database-path" --server-arg="/tmp/db.sqlite"
73
+ ```
74
+
75
+ This is useful for servers that need configuration files, database paths, API keys, or other runtime options.
76
+
77
  ## Transport Options
78
 
79
  Below is a comparison of available transport options to help you choose the right one for your needs:
 
170
 
171
  Server-Sent Events (SSE) is an HTTP-based protocol for server-to-client streaming. While FastMCP still supports SSE, it is deprecated and Streamable HTTP is preferred for new projects.
172
 
173
+ To run a server using SSE, you can use the `run()` method with the `transport` argument set to `"sse"`. This will start a Uvicorn server on the default host (`127.0.0.1`), port (`8000`), and with default SSE path (`/sse`) and message path (`/messages/`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/config_server.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple example showing FastMCP server with command line argument support.
3
+
4
+ Usage:
5
+ fastmcp run examples/config_server.py --server-arg="--name" --server-arg="MyServer"
6
+ fastmcp run examples/config_server.py --server-arg="--debug"
7
+ """
8
+
9
+ import argparse
10
+
11
+ from fastmcp import FastMCP
12
+
13
+ parser = argparse.ArgumentParser(description="Simple configurable MCP server")
14
+ parser.add_argument(
15
+ "--name", type=str, default="ConfigurableServer", help="Server name"
16
+ )
17
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode")
18
+
19
+ args = parser.parse_args()
20
+
21
+ server_name = args.name
22
+ if args.debug:
23
+ server_name += " (Debug)"
24
+
25
+ mcp = FastMCP(server_name)
26
+
27
+
28
+ @mcp.tool()
29
+ def get_status() -> dict[str, str | bool]:
30
+ """Get the current server configuration and status."""
31
+ return {
32
+ "server_name": server_name,
33
+ "debug_mode": args.debug,
34
+ "original_name": args.name,
35
+ }
36
+
37
+
38
+ @mcp.tool()
39
+ def echo_message(message: str) -> str:
40
+ """Echo a message, with debug info if debug mode is enabled."""
41
+ if args.debug:
42
+ return f"[DEBUG] Echoing: {message}"
43
+ return message
44
+
45
+
46
+ if __name__ == "__main__":
47
+ mcp.run()
src/fastmcp/cli/cli.py CHANGED
@@ -256,6 +256,13 @@ def run(
256
  help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
257
  ),
258
  ] = None,
 
 
 
 
 
 
 
259
  ) -> None:
260
  """Run a MCP server or connect to a remote one.
261
 
@@ -266,6 +273,9 @@ def run(
266
 
267
  Note: This command runs the server directly. You are responsible for ensuring
268
  all dependencies are available.
 
 
 
269
  """
270
  logger.debug(
271
  "Running server or client",
@@ -275,6 +285,7 @@ def run(
275
  "host": host,
276
  "port": port,
277
  "log_level": log_level,
 
278
  },
279
  )
280
 
@@ -285,6 +296,7 @@ def run(
285
  host=host,
286
  port=port,
287
  log_level=log_level,
 
288
  )
289
  except Exception as e:
290
  logger.error(
 
256
  help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
257
  ),
258
  ] = None,
259
+ server_args: Annotated[
260
+ list[str],
261
+ typer.Option(
262
+ "--server-arg",
263
+ help="Additional arguments to pass to the server",
264
+ ),
265
+ ] = [],
266
  ) -> None:
267
  """Run a MCP server or connect to a remote one.
268
 
 
273
 
274
  Note: This command runs the server directly. You are responsible for ensuring
275
  all dependencies are available.
276
+
277
+ Server arguments can be passed using --server-arg:
278
+ fastmcp run server.py --server-arg="--config" --server-arg="config.json"
279
  """
280
  logger.debug(
281
  "Running server or client",
 
285
  "host": host,
286
  "port": port,
287
  "log_level": log_level,
288
+ "server_args": server_args,
289
  },
290
  )
291
 
 
296
  host=host,
297
  port=port,
298
  log_level=log_level,
299
+ server_args=server_args,
300
  )
301
  except Exception as e:
302
  logger.error(
src/fastmcp/cli/run.py CHANGED
@@ -71,6 +71,9 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
71
  logger.error("Could not load module", extra={"file": str(file)})
72
  sys.exit(1)
73
 
 
 
 
74
  module = importlib.util.module_from_spec(spec)
75
  spec.loader.exec_module(module)
76
 
@@ -89,6 +92,8 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
89
  )
90
  sys.exit(1)
91
 
 
 
92
  # Handle module:object syntax
93
  if ":" in server_object:
94
  module_name, object_name = server_object.split(":", 1)
@@ -135,12 +140,37 @@ def create_client_server(url: str) -> Any:
135
  sys.exit(1)
136
 
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  def run_command(
139
  server_spec: str,
140
  transport: str | None = None,
141
  host: str | None = None,
142
  port: int | None = None,
143
  log_level: str | None = None,
 
144
  ) -> None:
145
  """Run a MCP server or connect to a remote one.
146
 
@@ -150,6 +180,7 @@ def run_command(
150
  host: Host to bind to when using http transport
151
  port: Port to bind to when using http transport
152
  log_level: Log level
 
153
  """
154
  if is_url(server_spec):
155
  # Handle URL case
@@ -158,7 +189,7 @@ def run_command(
158
  else:
159
  # Handle file case
160
  file, server_object = parse_file_path(server_spec)
161
- server = import_server(file, server_object)
162
  logger.debug(f'Found server "{server.name}" in {file}')
163
 
164
  # Run the server
 
71
  logger.error("Could not load module", extra={"file": str(file)})
72
  sys.exit(1)
73
 
74
+ assert spec is not None
75
+ assert spec.loader is not None
76
+
77
  module = importlib.util.module_from_spec(spec)
78
  spec.loader.exec_module(module)
79
 
 
92
  )
93
  sys.exit(1)
94
 
95
+ assert server_object is not None
96
+
97
  # Handle module:object syntax
98
  if ":" in server_object:
99
  module_name, object_name = server_object.split(":", 1)
 
140
  sys.exit(1)
141
 
142
 
143
+ def import_server_with_args(
144
+ file: Path, server_object: str | None = None, server_args: list[str] | None = None
145
+ ) -> Any:
146
+ """Import a server with optional command line arguments.
147
+
148
+ Args:
149
+ file: Path to the server file
150
+ server_object: Optional server object name
151
+ server_args: Optional command line arguments to inject
152
+
153
+ Returns:
154
+ The imported server object
155
+ """
156
+ if server_args:
157
+ original_argv = sys.argv[:]
158
+ try:
159
+ sys.argv = [str(file)] + server_args
160
+ return import_server(file, server_object)
161
+ finally:
162
+ sys.argv = original_argv
163
+ else:
164
+ return import_server(file, server_object)
165
+
166
+
167
  def run_command(
168
  server_spec: str,
169
  transport: str | None = None,
170
  host: str | None = None,
171
  port: int | None = None,
172
  log_level: str | None = None,
173
+ server_args: list[str] | None = None,
174
  ) -> None:
175
  """Run a MCP server or connect to a remote one.
176
 
 
180
  host: Host to bind to when using http transport
181
  port: Port to bind to when using http transport
182
  log_level: Log level
183
+ server_args: Additional arguments to pass to the server
184
  """
185
  if is_url(server_spec):
186
  # Handle URL case
 
189
  else:
190
  # Handle file case
191
  file, server_object = parse_file_path(server_spec)
192
+ server = import_server_with_args(file, server_object, server_args)
193
  logger.debug(f'Found server "{server.name}" in {file}')
194
 
195
  # Run the server
tests/cli/test_cli.py CHANGED
@@ -409,3 +409,29 @@ class TestRunCommand:
409
  mock_server.run.assert_called_once_with(
410
  transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
411
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  mock_server.run.assert_called_once_with(
410
  transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
411
  )
412
+
413
+ def test_run_command_with_server_args(self, temp_python_file):
414
+ """Test run command with server arguments."""
415
+ with (
416
+ patch("fastmcp.cli.run.run_command") as mock_run_command,
417
+ ):
418
+ result = runner.invoke(
419
+ cli.app,
420
+ [
421
+ "run",
422
+ str(temp_python_file),
423
+ "--server-arg",
424
+ "--config",
425
+ "--server-arg",
426
+ "config.json",
427
+ ],
428
+ )
429
+ assert result.exit_code == 0
430
+ mock_run_command.assert_called_once_with(
431
+ server_spec=str(temp_python_file),
432
+ transport=None,
433
+ host=None,
434
+ port=None,
435
+ log_level=None,
436
+ server_args=["--config", "config.json"],
437
+ )
tests/cli/test_run.py CHANGED
@@ -260,3 +260,39 @@ class TestRunCommand:
260
  mock_server.run.assert_called_once_with(
261
  transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
262
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  mock_server.run.assert_called_once_with(
261
  transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
262
  )
263
+
264
+
265
+ class TestImportServerWithArgs:
266
+ """Tests for the import_server_with_args function."""
267
+
268
+ def test_import_server_with_args_no_args(self, temp_python_file):
269
+ """Test importing server without arguments."""
270
+ with patch("fastmcp.cli.run.import_server") as mock_import:
271
+ mock_server = MagicMock()
272
+ mock_import.return_value = mock_server
273
+
274
+ result = fastmcp.cli.run.import_server_with_args(
275
+ temp_python_file, None, None
276
+ )
277
+
278
+ assert result == mock_server
279
+ mock_import.assert_called_once_with(temp_python_file, None)
280
+
281
+ def test_import_server_with_args_with_args(self, temp_python_file):
282
+ """Test importing server with arguments."""
283
+ import sys
284
+
285
+ with patch("fastmcp.cli.run.import_server") as mock_import:
286
+ mock_server = MagicMock()
287
+ mock_import.return_value = mock_server
288
+
289
+ original_argv = sys.argv[:]
290
+
291
+ result = fastmcp.cli.run.import_server_with_args(
292
+ temp_python_file, "custom_server", ["--config", "test.json", "--debug"]
293
+ )
294
+
295
+ assert result == mock_server
296
+ mock_import.assert_called_once_with(temp_python_file, "custom_server")
297
+ # Verify sys.argv was restored
298
+ assert sys.argv == original_argv