diff --git a/docs/deployment/server-configuration.mdx b/docs/deployment/server-configuration.mdx index 44cbfbd3c..789a8005b 100644 --- a/docs/deployment/server-configuration.mdx +++ b/docs/deployment/server-configuration.mdx @@ -44,14 +44,20 @@ This conceptual model helps you understand the purpose of each configuration sec "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", "source": { // WHERE: Location of your server code + "type": "filesystem", // Optional, defaults to "filesystem" "path": "server.py", "entrypoint": "mcp" }, "environment": { - // WHAT: Python environment and dependencies + // WHAT: Environment setup and dependencies + "type": "uv", // Optional, defaults to "uv" + "python": ">=3.10", + "dependencies": ["pandas", "numpy"] }, "deployment": { // HOW: Runtime configuration + "transport": "stdio", + "log_level": "INFO" } } ``` @@ -128,15 +134,21 @@ Future releases will support additional source types: ### Environment Configuration -The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment using `uv`'s powerful dependency management. This section ensures your server runs with the exact Python version and dependencies it requires, creating isolated, reproducible environments across different systems. +The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment, ensuring your server runs with the exact Python version and dependencies it requires. This section creates isolated, reproducible environments across different systems. -These settings leverage standard `uv` arguments for environment creation. When any environment field is specified, FastMCP automatically creates an isolated environment before running your server. This build-time configuration happens once when the server starts, not during runtime execution. +FastMCP uses an extensible environment system with a base `Environment` class that can be implemented by different environment providers. Currently, FastMCP supports the `UVEnvironment` for Python environment management using `uv`'s powerful dependency resolver. - + - Optional Python environment configuration. When any field is specified, FastMCP automatically creates an isolated environment using `uv`. + Optional environment configuration. When specified, FastMCP uses the appropriate environment implementation to set up your server's runtime. - + + The environment type identifier that determines which implementation to use. Currently supports `"uv"` for Python environments managed by uv. If omitted, defaults to `"uv"`. + + + + When `type` is `"uv"` (or omitted), the environment uses uv to manage Python dependencies: + Python version constraint. Examples: - Exact version: `"3.12"` @@ -175,17 +187,36 @@ These settings leverage standard `uv` arguments for environment creation. When a "editable": [".", "../shared-lib", "/path/to/another-package"] ``` + + **Example:** + ```json + "environment": { + "type": "uv", + "python": ">=3.10", + "dependencies": ["pandas", "numpy"], + "editable": ["."] + } + ``` + + Note: When any UVEnvironment field is specified, FastMCP automatically creates an isolated environment using `uv` before running your server. When environment configuration is provided, FastMCP: -1. Creates an isolated Python environment using `uv` -2. Installs the specified dependencies -3. Runs your server in this clean environment +1. Detects the environment type (defaults to `"uv"` if not specified) +2. Creates an isolated environment using the appropriate provider +3. Installs the specified dependencies +4. Runs your server in this clean environment This build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects. + +**Future Environment Types** + +Similar to source types, future releases may support additional environment types for different runtime requirements, such as Docker containers or language-specific environments beyond Python. + + ### Deployment Configuration The deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels. @@ -437,6 +468,7 @@ A configuration optimized for local development: }, // WHAT dependencies does it need? "environment": { + "type": "uv", "python": "3.12", "dependencies": ["fastmcp[dev]"], "editable": "." diff --git a/src/fastmcp/utilities/mcp_server_config/__init__.py b/src/fastmcp/utilities/mcp_server_config/__init__.py index a51dbcf6d..cbbfe5aa3 100644 --- a/src/fastmcp/utilities/mcp_server_config/__init__.py +++ b/src/fastmcp/utilities/mcp_server_config/__init__.py @@ -11,11 +11,11 @@ from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import ( MCPServerConfig, generate_schema, ) -from fastmcp.utilities.mcp_server_config.v1.sources.base import BaseSource +from fastmcp.utilities.mcp_server_config.v1.sources.base import Source from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource __all__ = [ - "BaseSource", + "Source", "Deployment", "Environment", "UVEnvironment", diff --git a/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py b/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py index c4a23a716..8209c7f4f 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py @@ -1,12 +1,14 @@ from abc import ABC, abstractmethod from pathlib import Path -from pydantic import BaseModel +from pydantic import BaseModel, Field class Environment(BaseModel, ABC): """Base class for environment configuration.""" + type: str = Field(description="Environment type identifier") + @abstractmethod def build_command(self, command: list[str]) -> list[str]: """Build the full command with environment setup. diff --git a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py index a0500ca22..3531dc569 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py @@ -3,6 +3,7 @@ import shutil import subprocess import sys from pathlib import Path +from typing import Literal from pydantic import Field @@ -15,6 +16,8 @@ logger = get_logger("cli.config") class UVEnvironment(Environment): """Configuration for Python environment setup.""" + type: Literal["uv"] = "uv" + python: str | None = Field( default=None, description="Python version constraint", diff --git a/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py index 690f7bf48..4c871cb11 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py @@ -11,12 +11,13 @@ import json import os import re from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, TypeAlias, overload +from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast, overload from pydantic import BaseModel, Field, field_validator from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment +from fastmcp.utilities.mcp_server_config.v1.sources.base import Source from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource logger = get_logger("cli.config") @@ -27,6 +28,7 @@ FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json # Type alias for source union (will expand with GitSource, etc in future) SourceType: TypeAlias = FileSystemSource + # Type alias for environment union (will expand with other environments in future) EnvironmentType: TypeAlias = UVEnvironment @@ -178,7 +180,7 @@ class MCPServerConfig(BaseModel): @field_validator("source", mode="before") @classmethod - def validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource: + def validate_source(cls, v: dict | Source) -> SourceType: """Validate and convert source to proper format. Supports: @@ -188,32 +190,20 @@ class MCPServerConfig(BaseModel): No string parsing happens here - that's only at CLI boundaries. MCPServerConfig works only with properly typed objects. """ - if isinstance(v, FileSystemSource): - # Already a FileSystemSource instance, return as-is - return v - elif isinstance(v, dict): - # Dict can have type field or not (filesystem is default) - if "type" not in v: - v["type"] = "filesystem" + if isinstance(v, dict): return FileSystemSource(**v) - else: - raise ValueError("source must be a dict or FileSystemSource instance") + return v @field_validator("environment", mode="before") @classmethod - def validate_environment(cls, v: dict | UVEnvironment) -> UVEnvironment: - """Validate and convert environment to Environment. + def validate_environment(cls, v: dict | Any) -> EnvironmentType: + """Ensure environment has a type field for discrimination. - Accepts: - - Environment instance - - dict that can be converted to Environment + For backward compatibility, if no type is specified, default to "uv". """ - if isinstance(v, UVEnvironment): - return v - elif isinstance(v, dict): - return UVEnvironment(**v) # type: ignore[arg-type] - else: - raise ValueError("environment must be a dict, Environment instance") + if isinstance(v, dict): + return UVEnvironment(**v) + return v @field_validator("deployment", mode="before") @classmethod @@ -225,12 +215,9 @@ class MCPServerConfig(BaseModel): - dict that can be converted to Deployment """ - if isinstance(v, Deployment): - return v - elif isinstance(v, dict): + if isinstance(v, dict): return Deployment(**v) # type: ignore[arg-type] - else: - raise ValueError("deployment must be a dict, Deployment instance") + return cast(Deployment, v) @classmethod def from_file(cls, file_path: Path) -> MCPServerConfig: diff --git a/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py b/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py index 1eeb593d9..fa6509353 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py @@ -4,7 +4,7 @@ from typing import Any from pydantic import BaseModel, Field -class BaseSource(BaseModel, ABC): +class Source(BaseModel, ABC): """Abstract base class for all source types.""" type: str = Field(description="Source type identifier") diff --git a/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py b/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py index 29c2aeede..bddffe955 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py @@ -7,15 +7,16 @@ from typing import Any, Literal from pydantic import Field, field_validator from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config.v1.sources.base import BaseSource +from fastmcp.utilities.mcp_server_config.v1.sources.base import Source logger = get_logger(__name__) -class FileSystemSource(BaseSource): +class FileSystemSource(Source): """Source for local Python files.""" - type: Literal["filesystem"] = Field(default="filesystem", description="Source type") + type: Literal["filesystem"] = "filesystem" + path: str = Field(description="Path to Python file containing the server") entrypoint: str | None = Field( default=None, diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 4ea56fdf6..a3cc76fd3 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -435,8 +435,10 @@ class TestMCPServerConfig: 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" diff --git a/tests/cli/test_mcp_server_config_schema.py b/tests/cli/test_mcp_server_config_schema.py index 1fda6e91b..d4f739911 100644 --- a/tests/cli/test_mcp_server_config_schema.py +++ b/tests/cli/test_mcp_server_config_schema.py @@ -1,41 +1,8 @@ -"""Test that the JSON schema file matches the Pydantic model.""" - -import json -from pathlib import Path +"""Test that the generated JSON schema has the correct structure.""" from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import generate_schema -def test_schema_file_matches_pydantic_model(): - """Test that the schema.json file matches what the Pydantic model generates.""" - # Path to the schema file - schema_file = ( - Path(__file__).parent.parent.parent - / "src" - / "fastmcp" - / "utilities" - / "mcp_server_config" - / "v1" - / "schema.json" - ) - - # Load the schema file - with open(schema_file) as f: - file_schema = json.load(f) - - # Generate schema from Pydantic model - generated_schema = generate_schema() - - # They should be identical - assert file_schema == generated_schema, ( - "The schema.json file does not match the Pydantic model schema. " - "Please regenerate the schema file by running:\n" - 'uv run python -c "from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import generate_schema; ' - 'import json; print(json.dumps(generate_schema(), indent=2))" > ' - f"{schema_file}" - ) - - def test_schema_has_correct_id(): """Test that the schema has the correct $id field.""" generated_schema = generate_schema() @@ -72,8 +39,22 @@ def test_schema_nested_structure(): # Check environment section assert "environment" in properties env_schema = properties["environment"] - if "properties" in env_schema: + # Environment can be in anyOf or direct properties + if "anyOf" in env_schema: + # Find the UVEnvironment in anyOf + for option in env_schema["anyOf"]: + if option.get("type") == "object" and "properties" in option: + env_props = option["properties"] + assert "type" in env_props # New type field + assert "python" in env_props + assert "dependencies" in env_props + assert "requirements" in env_props + assert "project" in env_props + assert "editable" in env_props + break + elif "properties" in env_schema: env_props = env_schema["properties"] + assert "type" in env_props # New type field assert "python" in env_props assert "dependencies" in env_props assert "requirements" in env_props