Files
wehub-resource-sync 60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

462 lines
17 KiB
Python

"""Tests for FastMCP configuration file support with nested structure."""
import json
import os
from pathlib import Path
import pytest
from pydantic import ValidationError
from fastmcp.utilities.mcp_server_config import (
Deployment,
MCPServerConfig,
)
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
class TestFileSystemSource:
"""Test FileSystemSource class."""
def test_dict_source_minimal(self):
"""Test that dict source is converted to FileSystemSource."""
config = MCPServerConfig(source={"path": "server.py"})
# Dict is converted to FileSystemSource
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert config.source.type == "filesystem"
def test_dict_source_with_entrypoint(self):
"""Test dict source with entrypoint field."""
config = MCPServerConfig(source={"path": "server.py", "entrypoint": "app"})
# Dict with entrypoint is converted to FileSystemSource
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint == "app"
assert config.source.type == "filesystem"
def test_filesystem_source_entrypoint(self):
"""Test FileSystemSource entrypoint format."""
config = MCPServerConfig(
source=FileSystemSource(path="src/server.py", entrypoint="mcp")
)
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "mcp"
assert config.source.type == "filesystem"
class TestEnvironment:
"""Test Environment class."""
def test_environment_config_fields(self):
"""Test all Environment fields."""
config = MCPServerConfig(
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["requests", "numpy>=2.0"],
"requirements": "requirements.txt",
"project": ".",
"editable": ["../my-package"],
},
)
env = config.environment
assert env.python == "3.12"
assert env.dependencies == ["requests", "numpy>=2.0"]
# Paths are stored as Path objects
assert env.requirements == Path("requirements.txt")
assert env.project == Path(".")
assert env.editable == [Path("../my-package")]
def test_needs_uv(self):
"""Test needs_uv() method."""
# No environment config - doesn't need UV
config = MCPServerConfig(source={"path": "server.py"})
assert not config.environment._must_run_with_uv()
# Empty environment - doesn't need UV
config = MCPServerConfig(source={"path": "server.py"}, environment={})
assert not config.environment._must_run_with_uv()
# With dependencies - needs UV
config = MCPServerConfig(
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
)
assert config.environment._must_run_with_uv()
# With Python version - needs UV
config = MCPServerConfig(
source={"path": "server.py"}, environment={"python": "3.12"}
)
assert config.environment._must_run_with_uv()
def test_build_uv_run_command(self):
"""Test build_uv_run_command() method."""
config = MCPServerConfig(
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["requests", "numpy"],
"requirements": "requirements.txt",
"project": ".",
},
)
cmd = config.environment.build_command(["fastmcp", "run", "server.py"])
assert cmd[0] == "uv"
assert cmd[1] == "run"
# Python version not added when project is specified (project defines its own Python)
assert "--python" not in cmd
assert "3.12" not in cmd
assert "--project" in cmd
# Project path should be resolved to absolute path
project_idx = cmd.index("--project")
assert Path(cmd[project_idx + 1]).is_absolute()
assert "--with" in cmd
assert "requests" in cmd
assert "numpy" in cmd
assert "--with-requirements" in cmd
# Requirements path should be resolved to absolute path
req_idx = cmd.index("--with-requirements")
assert Path(cmd[req_idx + 1]).is_absolute()
# Command args should be at the end
assert "fastmcp" in cmd[-3:]
assert "run" in cmd[-2:]
assert "server.py" in cmd[-1:]
class TestDeployment:
"""Test Deployment class."""
def test_deployment_config_fields(self):
"""Test all Deployment fields."""
config = MCPServerConfig(
source={"path": "server.py"},
deployment={
"transport": "http",
"host": "0.0.0.0",
"port": 8000,
"path": "/api/",
"log_level": "DEBUG",
"env": {"API_KEY": "secret"},
"cwd": "./work",
"args": ["--debug"],
},
)
deploy = config.deployment
assert deploy.transport == "http"
assert deploy.host == "0.0.0.0"
assert deploy.port == 8000
assert deploy.path == "/api/"
assert deploy.log_level == "DEBUG"
assert deploy.env == {"API_KEY": "secret"}
assert deploy.cwd == "./work"
assert deploy.args == ["--debug"]
def test_apply_runtime_settings(self, tmp_path):
"""Test apply_runtime_settings() method."""
import os
# Create config with env vars and cwd
work_dir = tmp_path / "work"
work_dir.mkdir()
config = MCPServerConfig(
source={"path": "server.py"},
deployment={
"env": {"TEST_VAR": "test_value"},
"cwd": "work",
},
)
original_cwd = os.getcwd()
original_env = os.environ.get("TEST_VAR")
try:
config.deployment.apply_runtime_settings(tmp_path / "fastmcp.json")
# Check environment variable was set
assert os.environ["TEST_VAR"] == "test_value"
# Check working directory was changed
assert Path.cwd() == work_dir.resolve()
finally:
# Restore original state
os.chdir(original_cwd)
if original_env is None:
os.environ.pop("TEST_VAR", None)
else:
os.environ["TEST_VAR"] = original_env
def test_env_var_interpolation(self, tmp_path):
"""Test environment variable interpolation in deployment env."""
import os
# Set up test environment variables
os.environ["BASE_URL"] = "example.com"
os.environ["ENV_NAME"] = "production"
config = MCPServerConfig(
source={"path": "server.py"},
deployment={
"env": {
"API_URL": "https://api.${BASE_URL}/v1",
"DATABASE": "postgres://${ENV_NAME}.db",
"PREFIXED": "MY_${ENV_NAME}_SERVER",
"MISSING": "value_${NONEXISTENT}_here",
"STATIC": "no_interpolation",
}
},
)
original_values = {
key: os.environ.get(key)
for key in ["API_URL", "DATABASE", "PREFIXED", "MISSING", "STATIC"]
}
try:
config.deployment.apply_runtime_settings()
# Check interpolated values
assert os.environ["API_URL"] == "https://api.example.com/v1"
assert os.environ["DATABASE"] == "postgres://production.db"
assert os.environ["PREFIXED"] == "MY_production_SERVER"
# Missing variables should keep the placeholder
assert os.environ["MISSING"] == "value_${NONEXISTENT}_here"
# Static values should remain unchanged
assert os.environ["STATIC"] == "no_interpolation"
finally:
# Clean up
os.environ.pop("BASE_URL", None)
os.environ.pop("ENV_NAME", None)
for key, value in original_values.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
class TestMCPServerConfig:
"""Test MCPServerConfig root configuration."""
def test_minimal_config(self):
"""Test creating a config with only required fields."""
config = MCPServerConfig(source={"path": "server.py"})
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
# Environment and deployment are now always present but empty
assert isinstance(config.environment, UVEnvironment)
assert isinstance(config.deployment, Deployment)
# Check they have no values set
assert not config.environment._must_run_with_uv()
assert all(
getattr(config.deployment, field, None) is None
for field in Deployment.model_fields
)
def test_nested_structure(self):
"""Test the nested configuration structure."""
config = MCPServerConfig(
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["fastmcp"],
},
deployment={
"transport": "stdio",
"log_level": "INFO",
},
)
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert isinstance(config.environment, UVEnvironment)
assert isinstance(config.deployment, Deployment)
def test_from_file(self, tmp_path):
"""Test loading config from JSON file with nested structure."""
config_data = {
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {"path": "src/server.py", "entrypoint": "app"},
"environment": {"python": "3.12", "dependencies": ["requests"]},
"deployment": {"transport": "http", "port": 8000},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
config = MCPServerConfig.from_file(config_file)
# When loaded from JSON with entrypoint format, it becomes EntrypointConfig
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "app"
assert config.environment.python == "3.12"
assert config.environment.dependencies == ["requests"]
assert config.deployment.transport == "http"
assert config.deployment.port == 8000
def test_from_file_with_string_entrypoint(self, tmp_path):
"""Test loading config with dict source format."""
config_data = {
"source": {"path": "server.py", "entrypoint": "mcp"},
"environment": {"dependencies": ["fastmcp"]},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
config = MCPServerConfig.from_file(config_file)
# String entrypoint with : should be converted to EntrypointConfig
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint == "mcp"
def test_string_entrypoint_with_entrypoint_and_environment(self, tmp_path):
"""Test that file.py:entrypoint syntax works with environment config."""
config_data = {
"source": {"path": "src/server.py", "entrypoint": "app"},
"environment": {"python": "3.12", "dependencies": ["fastmcp", "requests"]},
"deployment": {"transport": "http", "port": 8000},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
config = MCPServerConfig.from_file(config_file)
# Should be parsed into EntrypointConfig
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "app"
# Environment config should still work
assert config.environment.python == "3.12"
assert config.environment.dependencies == ["fastmcp", "requests"]
# Deployment config should still work
assert config.deployment.transport == "http"
assert config.deployment.port == 8000
def test_find_config_in_current_dir(self, tmp_path):
"""Test finding config in current directory."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
found = MCPServerConfig.find_config()
assert found == config_file
finally:
os.chdir(original_cwd)
def test_find_config_not_in_parent_dir(self, tmp_path):
"""Test that config is NOT found in parent directory."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
subdir = tmp_path / "subdir"
subdir.mkdir()
# Should NOT find config in parent directory
found = MCPServerConfig.find_config(subdir)
assert found is None
def test_find_config_in_specified_dir(self, tmp_path):
"""Test finding config in the specified directory."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should find config when looking in the directory that contains it
found = MCPServerConfig.find_config(tmp_path)
assert found == config_file
def test_find_config_not_found(self, tmp_path):
"""Test when config is not found."""
found = MCPServerConfig.find_config(tmp_path)
assert found is None
def test_invalid_transport(self, tmp_path):
"""Test loading config with invalid transport value."""
config_data = {
"source": {"path": "server.py"},
"deployment": {"transport": "invalid_transport"},
}
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps(config_data))
with pytest.raises(ValidationError):
MCPServerConfig.from_file(config_file)
def test_optional_sections(self):
"""Test that all config sections are optional except source."""
# Only source is required
config = MCPServerConfig(source={"path": "server.py"})
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
# Environment and deployment are now always present but may be empty
assert isinstance(config.environment, UVEnvironment)
assert isinstance(config.deployment, Deployment)
# Only environment with values
config = MCPServerConfig(
source={"path": "server.py"}, environment={"python": "3.12"}
)
assert config.environment.python == "3.12"
assert isinstance(config.deployment, Deployment)
assert all(
getattr(config.deployment, field, None) is None
for field in Deployment.model_fields
)
# Only deployment with values
config = MCPServerConfig(
source={"path": "server.py"}, deployment={"transport": "http"}
)
assert isinstance(config.environment, UVEnvironment)
# Check all fields except 'type' which has a default value
assert all(
getattr(config.environment, field, None) is None
for field in UVEnvironment.model_fields
if field != "type"
)
assert config.deployment.transport == "http"
class TestMCPServerConfigRoundtrip:
"""Test that MCPServerConfig survives model_dump() -> reconstruct pattern.
This is used by the CLI to apply overrides immutably.
"""
def test_roundtrip_preserves_schema(self):
"""Ensure schema_ field survives dump/reconstruct cycle."""
config = MCPServerConfig(source=FileSystemSource(path="server.py"))
config_dict = config.model_dump()
reconstructed = MCPServerConfig(**config_dict)
assert reconstructed.schema_ == config.schema_
def test_roundtrip_with_all_fields(self):
"""Full config survives dump/reconstruct."""
config = MCPServerConfig(
source=FileSystemSource(path="server.py", entrypoint="app"),
environment=UVEnvironment(python="3.11"),
deployment=Deployment(transport="http", port=8080),
)
config_dict = config.model_dump()
reconstructed = MCPServerConfig(**config_dict)
assert reconstructed.source.path == "server.py"
assert reconstructed.environment.python == "3.11"
assert reconstructed.deployment.port == 8080