Jeremiah Lowin commited on
Commit
211f2c8
·
unverified ·
2 Parent(s): cefca54bb0021a

Merge pull request #690 from jlowin/server-args-minimal

Browse files
docs/deployment/running-server.mdx CHANGED
@@ -61,6 +61,17 @@ 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:
@@ -270,4 +281,4 @@ async def health_check(request: Request) -> PlainTextResponse:
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
+ When servers accept command line arguments (using argparse, click, or other libraries), you can pass them after `--`:
67
+
68
+ ```bash
69
+ fastmcp run config_server.py -- --config config.json
70
+ fastmcp run database_server.py -- --database-path /tmp/db.sqlite --debug
71
+ ```
72
+
73
+ This is useful for servers that need configuration files, database paths, API keys, or other runtime options.
74
+
75
  ## Transport Options
76
 
77
  Below is a comparison of available transport options to help you choose the right one for your needs:
 
281
 
282
  if __name__ == "__main__":
283
  mcp.run()
284
+ ```
examples/config_server.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple example showing FastMCP server with command line argument support.
3
+
4
+ Usage:
5
+ fastmcp run examples/config_server.py -- --name MyServer --debug
6
+ """
7
+
8
+ import argparse
9
+
10
+ from fastmcp import FastMCP
11
+
12
+ parser = argparse.ArgumentParser(description="Simple configurable MCP server")
13
+ parser.add_argument(
14
+ "--name", type=str, default="ConfigurableServer", help="Server name"
15
+ )
16
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode")
17
+
18
+ args = parser.parse_args()
19
+
20
+ server_name = args.name
21
+ if args.debug:
22
+ server_name += " (Debug)"
23
+
24
+ mcp = FastMCP(server_name)
25
+
26
+
27
+ @mcp.tool()
28
+ def get_status() -> dict[str, str | bool]:
29
+ """Get the current server configuration and status."""
30
+ return {
31
+ "server_name": server_name,
32
+ "debug_mode": args.debug,
33
+ "original_name": args.name,
34
+ }
35
+
36
+
37
+ @mcp.tool()
38
+ def echo_message(message: str) -> str:
39
+ """Echo a message, with debug info if debug mode is enabled."""
40
+ if args.debug:
41
+ return f"[DEBUG] Echoing: {message}"
42
+ return message
43
+
44
+
45
+ if __name__ == "__main__":
46
+ mcp.run()
src/fastmcp/cli/cli.py CHANGED
@@ -219,8 +219,9 @@ def dev(
219
  sys.exit(1)
220
 
221
 
222
- @app.command()
223
  def run(
 
224
  server_spec: str = typer.Argument(
225
  ...,
226
  help="Python file, object specification (file:obj), or URL",
@@ -266,7 +267,12 @@ 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",
272
  extra={
@@ -275,6 +281,7 @@ def run(
275
  "host": host,
276
  "port": port,
277
  "log_level": log_level,
 
278
  },
279
  )
280
 
@@ -285,6 +292,7 @@ def run(
285
  host=host,
286
  port=port,
287
  log_level=log_level,
 
288
  )
289
  except Exception as e:
290
  logger.error(
 
219
  sys.exit(1)
220
 
221
 
222
+ @app.command(context_settings={"allow_extra_args": True})
223
  def run(
224
+ ctx: typer.Context,
225
  server_spec: str = typer.Argument(
226
  ...,
227
  help="Python file, object specification (file:obj), or URL",
 
267
 
268
  Note: This command runs the server directly. You are responsible for ensuring
269
  all dependencies are available.
270
+
271
+ Server arguments can be passed after -- :
272
+ fastmcp run server.py -- --config config.json --debug
273
  """
274
+ server_args = ctx.args # extra args after --
275
+
276
  logger.debug(
277
  "Running server or client",
278
  extra={
 
281
  "host": host,
282
  "port": port,
283
  "log_level": log_level,
284
+ "server_args": server_args,
285
  },
286
  )
287
 
 
292
  host=host,
293
  port=port,
294
  log_level=log_level,
295
+ server_args=server_args,
296
  )
297
  except Exception as e:
298
  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,28 @@ 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 using -- pattern."""
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
+ "--",
424
+ "--config",
425
+ "config.json",
426
+ ],
427
+ )
428
+ assert result.exit_code == 0
429
+ mock_run_command.assert_called_once_with(
430
+ server_spec=str(temp_python_file),
431
+ transport=None,
432
+ host=None,
433
+ port=None,
434
+ log_level=None,
435
+ server_args=["--config", "config.json"],
436
+ )
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