mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
Add -m/--module flag to fastmcp run and dev inspector
This commit is contained in:
parent
d7674b3c0c
commit
7a582ff5a0
3 changed files with 218 additions and 0 deletions
|
|
@ -212,6 +212,13 @@ async def inspector(
|
|||
help="Directories to watch for changes (default: current directory)",
|
||||
),
|
||||
] = None,
|
||||
module: Annotated[
|
||||
bool,
|
||||
cyclopts.Parameter(
|
||||
name=["--module", "-m"],
|
||||
help="Run a Python module (python -m <module>) instead of importing a server object",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Run an MCP server with the MCP Inspector for development.
|
||||
|
||||
|
|
@ -278,6 +285,10 @@ async def inspector(
|
|||
# Build the fastmcp run command
|
||||
fastmcp_cmd = ["fastmcp", "run", server_spec, "--no-banner"]
|
||||
|
||||
# Forward module mode flag
|
||||
if module:
|
||||
fastmcp_cmd.append("--module")
|
||||
|
||||
# Add reload flags if enabled - the server will handle reloading
|
||||
if reload:
|
||||
fastmcp_cmd.append("--reload")
|
||||
|
|
@ -424,6 +435,13 @@ async def run(
|
|||
help="Run in stateless mode (no session, used internally for reload)",
|
||||
),
|
||||
] = False,
|
||||
module: Annotated[
|
||||
bool,
|
||||
cyclopts.Parameter(
|
||||
name=["--module", "-m"],
|
||||
help="Run a Python module (python -m <module>) instead of importing a server object",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Run an MCP server or connect to a remote one.
|
||||
|
||||
|
|
@ -434,6 +452,7 @@ async def run(
|
|||
4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
|
||||
5. FastMCP config: "fastmcp.json" - runs server using FastMCP configuration
|
||||
6. No argument: looks for fastmcp.json in current directory
|
||||
7. Module mode: "my_module -m" - runs the module directly via python -m
|
||||
|
||||
Server arguments can be passed after -- :
|
||||
fastmcp run server.py -- --config config.json --debug
|
||||
|
|
@ -442,6 +461,54 @@ async def run(
|
|||
server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
|
||||
"""
|
||||
|
||||
# --- Module mode: delegate to python -m and exit early ---
|
||||
if module:
|
||||
if server_spec is None:
|
||||
logger.error("A module name is required when using --module / -m")
|
||||
sys.exit(1)
|
||||
|
||||
# Warn about options that are ignored in module mode
|
||||
ignored_options: list[str] = []
|
||||
if transport:
|
||||
ignored_options.append("--transport")
|
||||
if host:
|
||||
ignored_options.append("--host")
|
||||
if port:
|
||||
ignored_options.append("--port")
|
||||
if path:
|
||||
ignored_options.append("--path")
|
||||
if reload:
|
||||
ignored_options.append("--reload")
|
||||
if ignored_options:
|
||||
logger.warning(
|
||||
f"Options {', '.join(ignored_options)} are ignored in module mode "
|
||||
f"(-m). The module manages its own server startup."
|
||||
)
|
||||
|
||||
# Build environment wrapper if needed
|
||||
env_builder = None
|
||||
if not skip_env and not is_already_in_uv_subprocess():
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import (
|
||||
UVEnvironment,
|
||||
)
|
||||
|
||||
env = UVEnvironment(
|
||||
python=python,
|
||||
dependencies=with_packages or None,
|
||||
requirements=with_requirements,
|
||||
project=project,
|
||||
)
|
||||
test_cmd = ["test"]
|
||||
if env.build_command(test_cmd) != test_cmd:
|
||||
env_builder = env.build_command
|
||||
|
||||
run_module.run_module_command(
|
||||
server_spec,
|
||||
env_command_builder=env_builder,
|
||||
extra_args=list(server_args) if server_args else None,
|
||||
)
|
||||
return
|
||||
|
||||
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
||||
if skip_env or is_already_in_uv_subprocess():
|
||||
skip_env = True
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import json
|
|||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
|
@ -255,6 +256,41 @@ async def run_command(
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
def run_module_command(
|
||||
module_name: str,
|
||||
*,
|
||||
env_command_builder: Any | None = None,
|
||||
extra_args: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Run a Python module directly using ``python -m <module>``.
|
||||
|
||||
When ``-m`` is used, the module manages its own server startup.
|
||||
No server-object discovery or transport overrides are applied.
|
||||
|
||||
Args:
|
||||
module_name: Dotted module name (e.g. ``my_package``).
|
||||
env_command_builder: An optional callable that wraps a command list
|
||||
with environment setup (e.g. ``UVEnvironment.build_command``).
|
||||
extra_args: Extra arguments forwarded after the module name.
|
||||
"""
|
||||
cmd: list[str] = [sys.executable, "-m", module_name]
|
||||
if extra_args:
|
||||
cmd.extend(extra_args)
|
||||
|
||||
# Wrap with environment (e.g. uv run) if configured
|
||||
if env_command_builder is not None:
|
||||
cmd = env_command_builder(cmd)
|
||||
|
||||
logger.debug(f"Running module: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
process = subprocess.run(cmd, check=True)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Module {module_name} exited with code {e.returncode}")
|
||||
sys.exit(e.returncode)
|
||||
|
||||
|
||||
async def run_v1_server_async(
|
||||
server: FastMCP1x,
|
||||
host: str | None = None,
|
||||
|
|
|
|||
|
|
@ -695,3 +695,118 @@ class TestReloadFunctionality:
|
|||
}
|
||||
for ext in expected:
|
||||
assert ext in WATCHED_EXTENSIONS, f"Expected {ext} in WATCHED_EXTENSIONS"
|
||||
|
||||
|
||||
class TestRunModuleCommand:
|
||||
"""Test run_module_command functionality."""
|
||||
|
||||
def test_runs_python_m_module(self):
|
||||
"""Test that run_module_command invokes python -m <module>."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_module_command
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
|
||||
with (
|
||||
patch(
|
||||
"fastmcp.cli.run.subprocess.run", return_value=mock_result
|
||||
) as mock_run,
|
||||
pytest.raises(SystemExit) as exc_info,
|
||||
):
|
||||
run_module_command("my_package")
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
call_args = mock_run.call_args
|
||||
cmd = call_args[0][0]
|
||||
assert "-m" in cmd
|
||||
assert "my_package" in cmd
|
||||
|
||||
def test_forwards_extra_args(self):
|
||||
"""Test that extra arguments are forwarded after the module name."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_module_command
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
|
||||
with (
|
||||
patch(
|
||||
"fastmcp.cli.run.subprocess.run", return_value=mock_result
|
||||
) as mock_run,
|
||||
pytest.raises(SystemExit),
|
||||
):
|
||||
run_module_command("my_package", extra_args=["--host", "0.0.0.0"])
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--host" in cmd
|
||||
assert "0.0.0.0" in cmd
|
||||
|
||||
def test_uses_env_command_builder(self):
|
||||
"""Test that env_command_builder wraps the command."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_module_command
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
|
||||
def fake_builder(cmd: list[str]) -> list[str]:
|
||||
return ["uv", "run", *cmd]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"fastmcp.cli.run.subprocess.run", return_value=mock_result
|
||||
) as mock_run,
|
||||
pytest.raises(SystemExit),
|
||||
):
|
||||
run_module_command("my_package", env_command_builder=fake_builder)
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert cmd[0] == "uv"
|
||||
assert cmd[1] == "run"
|
||||
assert "-m" in cmd
|
||||
assert "my_package" in cmd
|
||||
|
||||
def test_exits_with_subprocess_error_code(self):
|
||||
"""Test that non-zero exit codes from the module are propagated."""
|
||||
import subprocess
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastmcp.cli.run import run_module_command
|
||||
|
||||
with (
|
||||
patch(
|
||||
"fastmcp.cli.run.subprocess.run",
|
||||
side_effect=subprocess.CalledProcessError(42, ["python", "-m", "bad"]),
|
||||
),
|
||||
pytest.raises(SystemExit) as exc_info,
|
||||
):
|
||||
run_module_command("bad")
|
||||
|
||||
assert exc_info.value.code == 42
|
||||
|
||||
def test_no_env_builder_runs_plain_python(self):
|
||||
"""Test that without env_command_builder, plain python is used."""
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_module_command
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
|
||||
with (
|
||||
patch(
|
||||
"fastmcp.cli.run.subprocess.run", return_value=mock_result
|
||||
) as mock_run,
|
||||
pytest.raises(SystemExit),
|
||||
):
|
||||
run_module_command("my_module", env_command_builder=None)
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert cmd[0] == sys.executable
|
||||
assert cmd[1] == "-m"
|
||||
assert cmd[2] == "my_module"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue