Add type field to Environment base class (#1676)

This commit is contained in:
Jeremiah Lowin 2025-08-30 07:47:24 -04:00 committed by GitHub
commit 183275c8c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 86 additions and 78 deletions

View file

@ -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.
<Card icon="code" title="Environment Fields">
<Card icon="code" title="Environment">
<ParamField body="environment" type="object">
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.
<Expandable title="Environment Fields">
<ParamField body="type" type="string" default="uv">
The environment type identifier that determines which implementation to use. Currently supports `"uv"` for Python environments managed by uv. If omitted, defaults to `"uv"`.
</ParamField>
<Expandable title="UVEnvironment">
When `type` is `"uv"` (or omitted), the environment uses uv to manage Python dependencies:
<ParamField body="python" type="string">
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"]
```
</ParamField>
**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.
</Expandable>
</ParamField>
</Card>
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.
<Note>
**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.
</Note>
### 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": "."

View file

@ -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",

View file

@ -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.

View file

@ -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",

View file

@ -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:

View file

@ -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")

View file

@ -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,

View file

@ -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"

View file

@ -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