Repository: jlowin/fastmcp

Issue:
Allow command line arguments for CLI
### Enhancement Description

Currently, the fastmcp CLI does not allow command line arguments. Providing a way to specify command line arguments would be quite useful. I cannot find any best practices guide for MCP that suggests that args are somehow contraindicated, and some of the sample MCP servers support them. 

Alternatives using environment variables are much less ergonomic for things like server config files. 

### Use Case

CLI arguments would be particularly useful for specifying config files and other configuration variables. 

### Proposed Implementation

```Python
typer or click would be interesting choices given the decorator nature of fastmcp. That's similar to what flask does.
```


Discussion:
What arguments do you want to supply? 
The example servers in https://github.com/modelcontextprotocol/servers provide some argument examples:

* filesystem server: allowed file access locations (one or multiple directories)
* sqlite servers: database paths
* git server: permitted repository access
* database servers: connection string arguments
* multiple: --config configuration_filename

The configuration file option is the most important one for me at this point, since most of the other options can be implemented using config files (though they shouldn't need to). 

Configuration files can be encoded directly in the server. However, that makes switching between development and testing configurations harder and less transparent to inspect (someone has to review the server source or read the docs rather than look at the command line string). 
Oh I see, you mean supply arguments to the underlying server. Got it, yes.



Patch:
diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx
index 48cf389d..5f5d758d 100644
--- a/docs/deployment/running-server.mdx
+++ b/docs/deployment/running-server.mdx
@@ -61,6 +61,17 @@ fastmcp dev server.py
 
 See the [CLI documentation](/patterns/cli) for detailed information about all available commands and options.
 
+### Passing Arguments to Servers
+
+When servers accept command line arguments (using argparse, click, or other libraries), you can pass them after `--`:
+
+```bash
+fastmcp run config_server.py -- --config config.json
+fastmcp run database_server.py -- --database-path /tmp/db.sqlite --debug
+```
+
+This is useful for servers that need configuration files, database paths, API keys, or other runtime options.
+
 ## Transport Options
 
 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:
 
 if __name__ == "__main__":
     mcp.run()
-```
+```
\ No newline at end of file
diff --git a/examples/config_server.py b/examples/config_server.py
new file mode 100644
index 00000000..d7964bc1
--- /dev/null
+++ b/examples/config_server.py
@@ -0,0 +1,46 @@
+"""
+Simple example showing FastMCP server with command line argument support.
+
+Usage:
+    fastmcp run examples/config_server.py -- --name MyServer --debug
+"""
+
+import argparse
+
+from fastmcp import FastMCP
+
+parser = argparse.ArgumentParser(description="Simple configurable MCP server")
+parser.add_argument(
+    "--name", type=str, default="ConfigurableServer", help="Server name"
+)
+parser.add_argument("--debug", action="store_true", help="Enable debug mode")
+
+args = parser.parse_args()
+
+server_name = args.name
+if args.debug:
+    server_name += " (Debug)"
+
+mcp = FastMCP(server_name)
+
+
+@mcp.tool()
+def get_status() -> dict[str, str | bool]:
+    """Get the current server configuration and status."""
+    return {
+        "server_name": server_name,
+        "debug_mode": args.debug,
+        "original_name": args.name,
+    }
+
+
+@mcp.tool()
+def echo_message(message: str) -> str:
+    """Echo a message, with debug info if debug mode is enabled."""
+    if args.debug:
+        return f"[DEBUG] Echoing: {message}"
+    return message
+
+
+if __name__ == "__main__":
+    mcp.run()
diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py
index 9e2dfd71..8470fad6 100644
--- a/src/fastmcp/cli/cli.py
+++ b/src/fastmcp/cli/cli.py
@@ -219,8 +219,9 @@ def dev(
         sys.exit(1)
 
 
-@app.command()
+@app.command(context_settings={"allow_extra_args": True})
 def run(
+    ctx: typer.Context,
     server_spec: str = typer.Argument(
         ...,
         help="Python file, object specification (file:obj), or URL",
@@ -266,7 +267,12 @@ def run(
 
     Note: This command runs the server directly. You are responsible for ensuring
     all dependencies are available.
+
+    Server arguments can be passed after -- :
+    fastmcp run server.py -- --config config.json --debug
     """
+    server_args = ctx.args  # extra args after --
+
     logger.debug(
         "Running server or client",
         extra={
@@ -275,6 +281,7 @@ def run(
             "host": host,
             "port": port,
             "log_level": log_level,
+            "server_args": server_args,
         },
     )
 
@@ -285,6 +292,7 @@ def run(
             host=host,
             port=port,
             log_level=log_level,
+            server_args=server_args,
         )
     except Exception as e:
         logger.error(
diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py
index fc3bf178..2cb790c0 100644
--- a/src/fastmcp/cli/run.py
+++ b/src/fastmcp/cli/run.py
@@ -71,6 +71,9 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
         logger.error("Could not load module", extra={"file": str(file)})
         sys.exit(1)
 
+    assert spec is not None
+    assert spec.loader is not None
+
     module = importlib.util.module_from_spec(spec)
     spec.loader.exec_module(module)
 
@@ -89,6 +92,8 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
         )
         sys.exit(1)
 
+    assert server_object is not None
+
     # Handle module:object syntax
     if ":" in server_object:
         module_name, object_name = server_object.split(":", 1)
@@ -135,12 +140,37 @@ def create_client_server(url: str) -> Any:
         sys.exit(1)
 
 
+def import_server_with_args(
+    file: Path, server_object: str | None = None, server_args: list[str] | None = None
+) -> Any:
+    """Import a server with optional command line arguments.
+
+    Args:
+        file: Path to the server file
+        server_object: Optional server object name
+        server_args: Optional command line arguments to inject
+
+    Returns:
+        The imported server object
+    """
+    if server_args:
+        original_argv = sys.argv[:]
+        try:
+            sys.argv = [str(file)] + server_args
+            return import_server(file, server_object)
+        finally:
+            sys.argv = original_argv
+    else:
+        return import_server(file, server_object)
+
+
 def run_command(
     server_spec: str,
     transport: str | None = None,
     host: str | None = None,
     port: int | None = None,
     log_level: str | None = None,
+    server_args: list[str] | None = None,
 ) -> None:
     """Run a MCP server or connect to a remote one.
 
@@ -150,6 +180,7 @@ def run_command(
         host: Host to bind to when using http transport
         port: Port to bind to when using http transport
         log_level: Log level
+        server_args: Additional arguments to pass to the server
     """
     if is_url(server_spec):
         # Handle URL case
@@ -158,7 +189,7 @@ def run_command(
     else:
         # Handle file case
         file, server_object = parse_file_path(server_spec)
-        server = import_server(file, server_object)
+        server = import_server_with_args(file, server_object, server_args)
         logger.debug(f'Found server "{server.name}" in {file}')
 
     # Run the server


Test patch:
diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py
index 2620c6c2..4897cc6d 100644
--- a/tests/cli/test_cli.py
+++ b/tests/cli/test_cli.py
@@ -409,3 +409,28 @@ def test_run_command_with_multiple_options(self, temp_python_file):
             mock_server.run.assert_called_once_with(
                 transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
             )
+
+    def test_run_command_with_server_args(self, temp_python_file):
+        """Test run command with server arguments using -- pattern."""
+        with (
+            patch("fastmcp.cli.run.run_command") as mock_run_command,
+        ):
+            result = runner.invoke(
+                cli.app,
+                [
+                    "run",
+                    str(temp_python_file),
+                    "--",
+                    "--config",
+                    "config.json",
+                ],
+            )
+            assert result.exit_code == 0
+            mock_run_command.assert_called_once_with(
+                server_spec=str(temp_python_file),
+                transport=None,
+                host=None,
+                port=None,
+                log_level=None,
+                server_args=["--config", "config.json"],
+            )
diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
index ea4bcefb..33b5906f 100644
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -260,3 +260,39 @@ def test_run_command_with_multiple_options(self, temp_python_file):
             mock_server.run.assert_called_once_with(
                 transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
             )
+
+
+class TestImportServerWithArgs:
+    """Tests for the import_server_with_args function."""
+
+    def test_import_server_with_args_no_args(self, temp_python_file):
+        """Test importing server without arguments."""
+        with patch("fastmcp.cli.run.import_server") as mock_import:
+            mock_server = MagicMock()
+            mock_import.return_value = mock_server
+
+            result = fastmcp.cli.run.import_server_with_args(
+                temp_python_file, None, None
+            )
+
+            assert result == mock_server
+            mock_import.assert_called_once_with(temp_python_file, None)
+
+    def test_import_server_with_args_with_args(self, temp_python_file):
+        """Test importing server with arguments."""
+        import sys
+
+        with patch("fastmcp.cli.run.import_server") as mock_import:
+            mock_server = MagicMock()
+            mock_import.return_value = mock_server
+
+            original_argv = sys.argv[:]
+
+            result = fastmcp.cli.run.import_server_with_args(
+                temp_python_file, "custom_server", ["--config", "test.json", "--debug"]
+            )
+
+            assert result == mock_server
+            mock_import.assert_called_once_with(temp_python_file, "custom_server")
+            # Verify sys.argv was restored
+            assert sys.argv == original_argv