Spaces:
Running
Running
File size: 9,012 Bytes
5f2c09d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | """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"
)
|