"""Integration tests for fastmcp.json configuration system.""" import json import sys from pathlib import Path import pytest from fastmcp.client import Client from fastmcp.utilities.mcp_server_config import MCPServerConfig @pytest.fixture def server_with_config(tmp_path): """Create a complete server setup with fastmcp.json config.""" # Create server file server_file = tmp_path / "server.py" server_file.write_text(""" from fastmcp import FastMCP mcp = FastMCP("Config Test Server") @mcp.tool def hello(name: str = "World") -> str: '''Say hello to someone''' return f"Hello, {name}!" @mcp.resource("resource://greeting") def get_greeting() -> str: '''Get a greeting message''' return "Welcome to FastMCP!" if __name__ == "__main__": import asyncio asyncio.run(mcp.run_async()) """) # Create config file config_data = { "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", "source": {"path": "server.py"}, "environment": { "python": sys.version.split()[0], # Use current Python version "dependencies": ["fastmcp"], }, "deployment": {"transport": "stdio", "log_level": "INFO"}, } config_file = tmp_path / "fastmcp.json" config_file.write_text(json.dumps(config_data, indent=2)) return tmp_path class TestConfigFileDetection: """Test configuration file detection patterns.""" def test_detect_standard_fastmcp_json(self, tmp_path): """Test detection of standard fastmcp.json file.""" config_file = tmp_path / "fastmcp.json" config_file.write_text(json.dumps({"source": {"path": "server.py"}})) # Should be detected as fastmcp config assert "fastmcp.json" in config_file.name assert config_file.name.endswith("fastmcp.json") def test_detect_prefixed_fastmcp_json(self, tmp_path): """Test detection of prefixed fastmcp.json files.""" config_file = tmp_path / "my.fastmcp.json" config_file.write_text(json.dumps({"source": {"path": "server.py"}})) # Should be detected as fastmcp config assert "fastmcp.json" in config_file.name def test_detect_test_fastmcp_json(self, tmp_path): """Test detection of test_fastmcp.json file.""" config_file = tmp_path / "test_fastmcp.json" config_file.write_text(json.dumps({"source": {"path": "server.py"}})) # Should be detected as fastmcp config assert "fastmcp.json" in config_file.name class TestConfigWithClient: """Test fastmcp.json configuration with client connections.""" async def test_config_server_with_client(self, server_with_config): """Test that a server loaded from config works with a client.""" # Load the config config_file = server_with_config / "fastmcp.json" config = MCPServerConfig.from_file(config_file) # Import the server using the source import importlib.util import sys # Resolve the path from the source source_path = Path(config.source.path) if not source_path.is_absolute(): source_path = (config_file.parent / source_path).resolve() spec = importlib.util.spec_from_file_location("test_server", str(source_path)) if spec is None or spec.loader is None: raise RuntimeError(f"Could not load module from {source_path}") module = importlib.util.module_from_spec(spec) sys.modules["test_server"] = module spec.loader.exec_module(module) assert hasattr(module, "mcp") server = module.mcp # Connect client to server async with Client(server) as client: # Test tool result = await client.call_tool("hello", {"name": "FastMCP"}) assert result.data == "Hello, FastMCP!" # Use .data for string result # Test resource results = await client.read_resource("resource://greeting") assert len(results) == 1 # Resource results should have text content assert hasattr(results[0], "text") or hasattr(results[0], "contents") # Get the text content from the resource text = getattr(results[0], "text", None) or getattr( results[0], "contents", "" ) assert "Welcome to FastMCP!" in str(text) class TestEnvironmentExecution: """Test environment configuration execution paths.""" def test_needs_uv_with_dependencies(self): """Test that environment with dependencies needs UV.""" config = MCPServerConfig( source={"path": "server.py"}, environment={"dependencies": ["requests", "numpy"]}, ) assert config.environment is not None assert config.environment._must_run_with_uv() def test_needs_uv_with_python_version(self): """Test that environment with Python version needs UV.""" config = MCPServerConfig( source={"path": "server.py"}, environment={"python": "3.12"}, ) assert config.environment is not None assert config.environment._must_run_with_uv() def test_no_uv_needed_without_environment(self): """Test that no UV is needed without environment config.""" config = MCPServerConfig(source={"path": "server.py"}) # Environment is now always present but may be empty assert config.environment is not None assert not config.environment._must_run_with_uv() def test_no_uv_needed_with_empty_environment(self): """Test that no UV is needed with empty environment config.""" config = MCPServerConfig( source={"path": "server.py"}, environment={}, ) assert config.environment is not None assert not config.environment._must_run_with_uv() class TestPathResolution: """Test path resolution in configurations.""" def test_source_path_resolution(self, tmp_path): """Test that source paths are resolved relative to config.""" # Create nested directory structure config_dir = tmp_path / "config" config_dir.mkdir() src_dir = tmp_path / "src" src_dir.mkdir() # Server is in src, config is in config server_file = src_dir / "server.py" server_file.write_text("# Server") config = MCPServerConfig(source={"path": "../src/server.py"}) # The source path is resolved during load_server # For now, just check that the source is created correctly assert config.source.path == "../src/server.py" def test_cwd_path_resolution(self, tmp_path): """Test that working directory is resolved relative to config.""" import os # Create directory structure work_dir = tmp_path / "work" work_dir.mkdir() config = MCPServerConfig( source={"path": "server.py"}, deployment={"cwd": "work"}, ) original_cwd = os.getcwd() try: # Apply runtime settings relative to config location assert config.deployment is not None config.deployment.apply_runtime_settings(tmp_path / "fastmcp.json") # Should change to work directory assert Path.cwd() == work_dir.resolve() finally: os.chdir(original_cwd) def test_requirements_path_resolution(self, tmp_path): """Test that requirements path is resolved correctly.""" # Create requirements file reqs_file = tmp_path / "requirements.txt" reqs_file.write_text("fastmcp>=2.0") config = MCPServerConfig( source={"path": "server.py"}, environment={"requirements": "requirements.txt"}, ) # Build UV command assert config.environment is not None uv_cmd = config.environment.build_command(["fastmcp", "run"]) # Should include requirements file with absolute path assert "--with-requirements" in uv_cmd req_idx = uv_cmd.index("--with-requirements") + 1 assert Path(uv_cmd[req_idx]).is_absolute() assert Path(uv_cmd[req_idx]).name == "requirements.txt" class TestConfigValidation: """Test configuration validation.""" def test_invalid_transport_rejected(self): """Test that invalid transport values are rejected.""" with pytest.raises(ValueError): MCPServerConfig( source={"path": "server.py"}, deployment={"transport": "invalid_transport"}, ) def test_streamable_http_transport_accepted(self): """Test that streamable-http transport is accepted as a valid value.""" config = MCPServerConfig( source={"path": "server.py"}, deployment={"transport": "streamable-http"}, ) assert config.deployment.transport == "streamable-http" def test_invalid_log_level_rejected(self): """Test that invalid log level values are rejected.""" with pytest.raises(ValueError): MCPServerConfig( source={"path": "server.py"}, deployment={"log_level": "INVALID"}, ) def test_missing_source_rejected(self): """Test that config without source is rejected.""" with pytest.raises(ValueError): MCPServerConfig() # type: ignore[call-arg] # ty:ignore[no-matching-overload] def test_valid_transport_values(self): """Test that all valid transport values are accepted.""" for transport in ["stdio", "http", "sse"]: config = MCPServerConfig( source={"path": "server.py"}, deployment={"transport": transport}, ) assert config.deployment is not None assert config.deployment.transport == transport def test_valid_log_levels(self): """Test that all valid log levels are accepted.""" for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: config = MCPServerConfig( source={"path": "server.py"}, deployment={"log_level": level}, ) assert config.deployment is not None assert config.deployment.log_level == level