xsecuremcp / tests /cli /test_run_config.py
Jeremiah Lowin
feat: introduce fastmcp.json configuration system (#1517)
5f2c09d unverified
Raw
History Blame Contribute Delete
9.01 kB
"""Integration tests for FastMCP configuration with run command."""
import json
import os
from pathlib import Path
import pytest
from fastmcp.cli.run import load_fastmcp_config
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
)
@pytest.fixture
def sample_config(tmp_path):
"""Create a sample fastmcp.json configuration file with nested structure."""
config_data = {
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "server.py",
"environment": {"python": "3.11", "dependencies": ["requests"]},
"deployment": {"transport": "stdio", "env": {"TEST_VAR": "test_value"}},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data, indent=2))
# Create a simple server file
server_file = tmp_path / "server.py"
server_file.write_text("""
from fastmcp import FastMCP
mcp = FastMCP("Test Server")
@mcp.tool
def test_tool(message: str) -> str:
return f"Echo: {message}"
""")
return config_file
def test_load_fastmcp_config(sample_config, monkeypatch):
"""Test loading configuration and returning config subsets."""
# Capture environment changes
original_env = dict(os.environ)
try:
entrypoint, deployment, environment = load_fastmcp_config(sample_config)
# Check that we got the right types
assert isinstance(entrypoint, EntrypointConfig)
assert isinstance(deployment, DeploymentConfig)
assert isinstance(environment, EnvironmentConfig)
# Check entrypoint
assert entrypoint.file.endswith("server.py")
assert Path(entrypoint.file).is_absolute()
assert entrypoint.object is None
# Check environment config
assert environment.python == "3.11"
assert environment.dependencies == ["requests"]
# Check deployment config
assert deployment.transport == "stdio"
assert deployment.env == {"TEST_VAR": "test_value"}
# Check that environment variables were applied
assert os.environ.get("TEST_VAR") == "test_value"
finally:
# Restore original environment
os.environ.clear()
os.environ.update(original_env)
def test_load_config_with_object_entrypoint(tmp_path):
"""Test loading config with object-format entrypoint."""
config_data = {
"entrypoint": {"file": "src/server.py", "object": "app"},
"deployment": {"transport": "http", "port": 8000},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create the server file in subdirectory
src_dir = tmp_path / "src"
src_dir.mkdir()
server_file = src_dir / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
# Check entrypoint resolution
assert entrypoint.file == str(server_file.resolve())
assert entrypoint.object == "app"
# Check deployment
assert deployment is not None
assert deployment.transport == "http"
assert deployment.port == 8000
# No environment config
assert environment is None
def test_load_config_with_cwd(tmp_path):
"""Test that DeploymentConfig applies working directory change."""
# Create a subdirectory
subdir = tmp_path / "subdir"
subdir.mkdir()
config_data = {"entrypoint": "server.py", "deployment": {"cwd": "subdir"}}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create server file in subdirectory
server_file = subdir / "server.py"
server_file.write_text("# Test server")
original_cwd = os.getcwd()
try:
entrypoint, deployment, environment = load_fastmcp_config(config_file)
# Check that working directory was changed
assert Path.cwd() == subdir.resolve()
finally:
# Restore original working directory
os.chdir(original_cwd)
def test_load_config_with_relative_cwd(tmp_path):
"""Test configuration with relative working directory."""
# Create nested subdirectories
subdir1 = tmp_path / "dir1"
subdir2 = subdir1 / "dir2"
subdir2.mkdir(parents=True)
config_data = {
"entrypoint": "server.py",
"deployment": {
"cwd": "../" # Relative to config file location
},
}
config_file = subdir2 / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create server file in parent directory
server_file = subdir1 / "server.py"
server_file.write_text("# Server")
original_cwd = os.getcwd()
try:
entrypoint, deployment, environment = load_fastmcp_config(config_file)
# Should change to parent directory of config file
assert Path.cwd() == subdir1.resolve()
finally:
os.chdir(original_cwd)
def test_load_minimal_config(tmp_path):
"""Test loading minimal configuration with only entrypoint."""
config_data = {"entrypoint": "server.py"}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create server file
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
# Check we got entrypoint
assert isinstance(entrypoint, EntrypointConfig)
assert entrypoint.file == str(server_file.resolve())
# No deployment or environment
assert deployment is None
assert environment is None
def test_load_config_with_server_args(tmp_path):
"""Test configuration with server arguments."""
config_data = {
"entrypoint": "server.py",
"deployment": {"args": ["--debug", "--config", "custom.json"]},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create server file
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
assert deployment is not None
assert deployment.args == ["--debug", "--config", "custom.json"]
def test_config_subset_independence(tmp_path):
"""Test that config subsets can be used independently."""
config_data = {
"entrypoint": "server.py",
"environment": {"python": "3.12", "dependencies": ["pandas"]},
"deployment": {"transport": "http", "host": "0.0.0.0", "port": 3000},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create server file
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
# Each subset should be independently usable
assert entrypoint.file == str(server_file.resolve())
assert entrypoint.object is None
assert environment is not None
assert environment.python == "3.12"
assert environment.dependencies == ["pandas"]
assert environment.needs_uv() # Has dependencies
assert deployment is not None
assert deployment.transport == "http"
assert deployment.host == "0.0.0.0"
assert deployment.port == 3000
# Can merge deployment config with CLI args
merged = deployment.merge_with_cli_args(
transport=None, # Keep config value
host="localhost", # Override
port=8080, # Override
path="/api", # New value
log_level=None,
server_args=None,
)
assert merged["transport"] == "http" # Kept from config
assert merged["host"] == "localhost" # CLI override
assert merged["port"] == 8080 # CLI override
assert merged["path"] == "/api" # CLI value
def test_environment_config_path_resolution(tmp_path):
"""Test that paths in environment config are resolved correctly."""
# Create requirements file
reqs_file = tmp_path / "requirements.txt"
reqs_file.write_text("fastmcp>=2.0")
config_data = {
"entrypoint": "server.py",
"environment": {
"requirements": "requirements.txt",
"project": ".",
"editable": "../other-project",
},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
# Create server file
server_file = tmp_path / "server.py"
server_file.write_text("# Server")
entrypoint, deployment, environment = load_fastmcp_config(config_file)
# Check that UV args are built with resolved paths
assert environment is not None
uv_args = environment.build_uv_args(["fastmcp", "run", "server.py"])
assert "--with-requirements" in uv_args
assert "--project" in uv_args
# Path should be resolved relative to config file
req_idx = uv_args.index("--with-requirements") + 1
assert (
Path(uv_args[req_idx]).is_absolute() or uv_args[req_idx] == "requirements.txt"
)