From 5fe8e2fe6b457d13a91e22ffdbba846712851468 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 29 Aug 2025 21:14:56 -0400 Subject: [PATCH 01/12] Fix OAuth token expiry handling (#1649) (#1671) --- src/fastmcp/client/auth/oauth.py | 65 ++++++-- tests/client/auth/test_oauth_token_expiry.py | 152 +++++++++++++++++++ 2 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 tests/client/auth/test_oauth_token_expiry.py diff --git a/src/fastmcp/client/auth/oauth.py b/src/fastmcp/client/auth/oauth.py index 88a8a9d33..b589c2873 100644 --- a/src/fastmcp/client/auth/oauth.py +++ b/src/fastmcp/client/auth/oauth.py @@ -4,6 +4,7 @@ import asyncio import json import webbrowser from asyncio import Future +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Literal from urllib.parse import urlparse @@ -18,7 +19,7 @@ from mcp.shared.auth import ( from mcp.shared.auth import ( OAuthToken as OAuthToken, ) -from pydantic import AnyHttpUrl, ValidationError +from pydantic import AnyHttpUrl, BaseModel, TypeAdapter, ValidationError from uvicorn.server import Server from fastmcp import settings as fastmcp_global_settings @@ -33,6 +34,17 @@ __all__ = ["OAuth"] logger = get_logger(__name__) +class StoredToken(BaseModel): + """Token storage format with absolute expiry time.""" + + token_payload: OAuthToken + expires_at: datetime | None + + +# Create TypeAdapter at module level for efficient parsing +stored_token_adapter = TypeAdapter(StoredToken) + + def default_cache_dir() -> Path: return fastmcp_global_settings.home / "oauth-mcp-client-cache" @@ -77,13 +89,28 @@ class FileTokenStorage(TokenStorage): path = self._get_file_path("tokens") try: - tokens = OAuthToken.model_validate_json(path.read_text()) - # now = datetime.datetime.now(datetime.timezone.utc) - # if tokens.expires_at is not None and tokens.expires_at <= now: - # logger.debug(f"Token expired for {self.get_base_url(self.server_url)}") - # return None - return tokens - except (FileNotFoundError, json.JSONDecodeError, ValidationError) as e: + # Parse JSON and validate as StoredToken + stored = stored_token_adapter.validate_json(path.read_text()) + + # Check if token is expired + if stored.expires_at is not None: + now = datetime.now(timezone.utc) + if now >= stored.expires_at: + logger.debug( + f"Token expired for {self.get_base_url(self.server_url)}" + ) + return None + + # Recalculate expires_in to be correct relative to now + if stored.token_payload.expires_in is not None: + remaining = stored.expires_at - now + stored.token_payload.expires_in = max( + 0, int(remaining.total_seconds()) + ) + + return stored.token_payload + + except (FileNotFoundError, ValidationError) as e: logger.debug( f"Could not load tokens for {self.get_base_url(self.server_url)}: {e}" ) @@ -92,7 +119,18 @@ class FileTokenStorage(TokenStorage): async def set_tokens(self, tokens: OAuthToken) -> None: """Save tokens to file storage.""" path = self._get_file_path("tokens") - path.write_text(tokens.model_dump_json(indent=2)) + + # Calculate absolute expiry time if expires_in is present + expires_at = None + if tokens.expires_in is not None: + expires_at = datetime.now(timezone.utc) + timedelta( + seconds=tokens.expires_in + ) + + # Create StoredToken and save using Pydantic serialization + stored = StoredToken(token_payload=tokens, expires_at=expires_at) + + path.write_text(stored.model_dump_json(indent=2)) logger.debug(f"Saved tokens for {self.get_base_url(self.server_url)}") async def get_client_info(self) -> OAuthClientInformationFull | None: @@ -252,6 +290,15 @@ class OAuth(OAuthClientProvider): callback_handler=self.callback_handler, ) + async def _initialize(self) -> None: + """Load stored tokens and client info, properly setting token expiry.""" + # Call parent's _initialize to load tokens and client info + await super()._initialize() + + # If tokens were loaded and have expires_in, update the context's token_expiry_time + if self.context.current_tokens and self.context.current_tokens.expires_in: + self.context.update_token_expiry(self.context.current_tokens) + async def redirect_handler(self, authorization_url: str) -> None: """Open browser for authorization.""" logger.info(f"OAuth authorization URL: {authorization_url}") diff --git a/tests/client/auth/test_oauth_token_expiry.py b/tests/client/auth/test_oauth_token_expiry.py new file mode 100644 index 000000000..77e5d552e --- /dev/null +++ b/tests/client/auth/test_oauth_token_expiry.py @@ -0,0 +1,152 @@ +"""Test OAuth token expiry handling with absolute timestamps.""" + +import json +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import pytest +from mcp.shared.auth import OAuthToken + +from fastmcp.client.auth.oauth import FileTokenStorage + + +@pytest.mark.asyncio +async def test_token_storage_with_expiry(tmp_path: Path): + """Test that tokens are stored with absolute expiry time and loaded correctly.""" + storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path) + + # Create a token with 3600 seconds expiry + token = OAuthToken( + access_token="test_token", + token_type="Bearer", + expires_in=3600, + refresh_token="refresh_token", + ) + + # Save the token + await storage.set_tokens(token) + + # Check that the file contains the dataclass format + token_file = storage._get_file_path("tokens") + data = json.loads(token_file.read_text()) + + assert "token_payload" in data + assert "expires_at" in data + assert data["expires_at"] is not None + # expires_at should be approximately now + 3600 seconds + expires_at = datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00")) + expected = datetime.now(timezone.utc) + timedelta(seconds=3600) + assert abs((expires_at - expected).total_seconds()) < 2 + + # Load the token back + loaded_token = await storage.get_tokens() + assert loaded_token is not None + assert loaded_token.access_token == "test_token" + # expires_in should be recalculated to be approximately 3600 (minus loading time) + assert loaded_token.expires_in is not None + assert 3595 <= loaded_token.expires_in <= 3600 + + +@pytest.mark.asyncio +async def test_expired_token_returns_none(tmp_path: Path): + """Test that expired tokens return None when loaded.""" + storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path) + + # Manually create an already-expired token file + token_file = storage._get_file_path("tokens") + past_expiry = datetime.now(timezone.utc) - timedelta( + seconds=10 + ) # Expired 10 seconds ago + + expired_token = { + "token_payload": { + "access_token": "test_token", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "refresh_token", + }, + "expires_at": past_expiry.isoformat(), + } + token_file.write_text(json.dumps(expired_token, indent=2, default=str)) + + # Load the token - should return None since it's expired + loaded_token = await storage.get_tokens() + assert loaded_token is None + + +@pytest.mark.asyncio +async def test_token_without_expiry(tmp_path: Path): + """Test that tokens without expires_in are handled correctly.""" + storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path) + + # Create a token without expires_in (perpetual token) + token = OAuthToken( + access_token="test_token", + token_type="Bearer", + expires_in=None, + refresh_token="refresh_token", + ) + + # Save the token + await storage.set_tokens(token) + + # Check that expires_at is None in the file + token_file = storage._get_file_path("tokens") + data = json.loads(token_file.read_text()) + assert data["expires_at"] is None + + # Load the token back - should work since no expiry + loaded_token = await storage.get_tokens() + assert loaded_token is not None + assert loaded_token.access_token == "test_token" + assert loaded_token.expires_in is None + + +@pytest.mark.asyncio +async def test_invalid_format_returns_none(tmp_path: Path): + """Test that invalid token format returns None.""" + storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path) + + # Manually write an invalid format token file (missing required fields) + token_file = storage._get_file_path("tokens") + invalid_token = { + "access_token": "invalid_token", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "refresh_token", + } + token_file.write_text(json.dumps(invalid_token, indent=2)) + + # Try to load - should return None + loaded_token = await storage.get_tokens() + assert loaded_token is None + + +@pytest.mark.asyncio +async def test_token_expiry_recalculated_on_load(tmp_path: Path): + """Test that expires_in is correctly recalculated when loading tokens.""" + storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path) + + # Manually create a token file with a specific expires_at + token_file = storage._get_file_path("tokens") + future_expiry = datetime.now(timezone.utc) + timedelta( + seconds=1800 + ) # 30 minutes from now + + stored_token = { + "token_payload": { + "access_token": "test_token", + "token_type": "Bearer", + "expires_in": 3600, # Original value (will be recalculated) + "refresh_token": "refresh_token", + }, + "expires_at": future_expiry.isoformat(), + } + token_file.write_text(json.dumps(stored_token, indent=2, default=str)) + + # Load the token + loaded_token = await storage.get_tokens() + assert loaded_token is not None + # expires_in should be recalculated to approximately 1800 seconds + assert loaded_token.expires_in is not None + assert 1795 <= loaded_token.expires_in <= 1800 From 6d46192659200eb1d93f1741c4cf42be432e291b Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 29 Aug 2025 21:37:39 -0400 Subject: [PATCH 02/12] Internal refactor: fastmcp config to mcp server config (#1672) --- .github/workflows/update-config-schema.yml | 18 ++--- .../latest.json | 0 .../v1.json | 0 docs/docs.json | 42 ++++++------ docs/python-sdk/fastmcp-cli-cli.mdx | 12 ++-- .../fastmcp-cli-install-claude_code.mdx | 2 +- .../fastmcp-cli-install-claude_desktop.mdx | 2 +- .../python-sdk/fastmcp-cli-install-cursor.mdx | 4 +- .../fastmcp-cli-install-mcp_json.mdx | 2 +- docs/python-sdk/fastmcp-cli-run.mdx | 23 ++++--- docs/python-sdk/fastmcp-client-auth-oauth.mdx | 34 ++++++---- docs/python-sdk/fastmcp-client-client.mdx | 66 +++++++++---------- .../fastmcp-server-auth-oauth_proxy.mdx | 20 +++--- .../fastmcp-server-auth-providers-workos.mdx | 6 +- ...astmcp-server-auth-redirect_validation.mdx | 5 +- .../fastmcp-server-middleware-logging.mdx | 24 +++++-- docs/python-sdk/fastmcp-utilities-cli.mdx | 32 ++++++++- ...-utilities-mcp_server_config-__init__.mdx} | 2 +- ...ilities-mcp_server_config-v1-__init__.mdx} | 2 +- ...cp_server_config-v1-mcp_server_config.mdx} | 62 ++++++++--------- ...mcp_server_config-v1-sources-__init__.mdx} | 2 +- ...ies-mcp_server_config-v1-sources-base.mdx} | 8 +-- ...p_server_config-v1-sources-filesystem.mdx} | 8 +-- examples/fastmcp_config_demo/README.md | 6 +- src/fastmcp/cli/claude.py | 2 +- src/fastmcp/cli/cli.py | 6 +- src/fastmcp/cli/install/claude_code.py | 2 +- src/fastmcp/cli/install/claude_desktop.py | 2 +- src/fastmcp/cli/install/cursor.py | 2 +- src/fastmcp/cli/install/mcp_json.py | 2 +- src/fastmcp/cli/install/shared.py | 12 ++-- src/fastmcp/cli/run.py | 22 +++---- src/fastmcp/client/transports.py | 2 +- src/fastmcp/utilities/cli.py | 20 +++--- .../__init__.py | 10 +-- .../v1/__init__.py | 0 .../v1/mcp_server_config.py} | 16 ++--- .../v1/schema.json | 0 .../v1/sources/__init__.py | 0 .../v1/sources/base.py | 0 .../v1/sources/filesystem.py | 2 +- tests/cli/test_cli.py | 2 +- tests/cli/test_config.py | 62 ++++++++--------- ... => test_mcp_server_config_integration.py} | 30 ++++----- ...ma.py => test_mcp_server_config_schema.py} | 6 +- tests/cli/test_project_prepare.py | 42 ++++++------ tests/cli/test_run.py | 6 +- tests/cli/test_run_config.py | 28 ++++---- tests/cli/test_server_args.py | 12 ++-- tests/utilities/test_cli.py | 2 +- 50 files changed, 363 insertions(+), 309 deletions(-) rename docs/assets/schemas/{fastmcp_config => mcp_server_config}/latest.json (100%) rename docs/assets/schemas/{fastmcp_config => mcp_server_config}/v1.json (100%) rename docs/python-sdk/{fastmcp-utilities-fastmcp_config-__init__.mdx => fastmcp-utilities-mcp_server_config-__init__.mdx} (84%) rename docs/python-sdk/{fastmcp-utilities-fastmcp_config-v1-__init__.mdx => fastmcp-utilities-mcp_server_config-v1-__init__.mdx} (74%) rename docs/python-sdk/{fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx => fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx} (65%) rename docs/python-sdk/{fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx => fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx} (70%) rename docs/python-sdk/{fastmcp-utilities-fastmcp_config-v1-sources-base.mdx => fastmcp-utilities-mcp_server_config-v1-sources-base.mdx} (63%) rename docs/python-sdk/{fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx => fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx} (51%) rename src/fastmcp/utilities/{fastmcp_config => mcp_server_config}/__init__.py (56%) rename src/fastmcp/utilities/{fastmcp_config => mcp_server_config}/v1/__init__.py (100%) rename src/fastmcp/utilities/{fastmcp_config/v1/fastmcp_config.py => mcp_server_config/v1/mcp_server_config.py} (98%) rename src/fastmcp/utilities/{fastmcp_config => mcp_server_config}/v1/schema.json (100%) rename src/fastmcp/utilities/{fastmcp_config => mcp_server_config}/v1/sources/__init__.py (100%) rename src/fastmcp/utilities/{fastmcp_config => mcp_server_config}/v1/sources/base.py (100%) rename src/fastmcp/utilities/{fastmcp_config => mcp_server_config}/v1/sources/filesystem.py (99%) rename tests/cli/{test_fastmcp_config_integration.py => test_mcp_server_config_integration.py} (93%) rename tests/cli/{test_fastmcp_config_schema.py => test_mcp_server_config_schema.py} (96%) diff --git a/.github/workflows/update-config-schema.yml b/.github/workflows/update-config-schema.yml index 9b3fa71a6..eb4115dd7 100644 --- a/.github/workflows/update-config-schema.yml +++ b/.github/workflows/update-config-schema.yml @@ -1,4 +1,4 @@ -name: Update FastMCPConfig Schema +name: Update MCPServerConfig Schema # This workflow runs on merges to main to automatically update the config schema # by creating a PR when changes are needed. @@ -7,8 +7,8 @@ on: push: branches: ["main"] paths: - - "src/fastmcp/utilities/fastmcp_config/**" - - "!src/fastmcp/utilities/fastmcp_config/v1/schema.json" # Exclude the local schema file + - "src/fastmcp/utilities/mcp_server_config/**" + - "!src/fastmcp/utilities/mcp_server_config/v1/schema.json" # Exclude the local schema file workflow_dispatch: permissions: @@ -45,23 +45,23 @@ jobs: # Generate schema in docs/public for web access uv run python -c " - from fastmcp.utilities.fastmcp_config import generate_schema + from fastmcp.utilities.mcp_server_config import generate_schema generate_schema('docs/public/schemas/fastmcp.json/latest.json') print('✅ Latest schema generated in docs/public') " # Also update the v1 schema in docs/public uv run python -c " - from fastmcp.utilities.fastmcp_config import generate_schema + from fastmcp.utilities.mcp_server_config import generate_schema generate_schema('docs/public/schemas/fastmcp.json/v1.json') print('✅ v1 schema generated in docs/public') " # Generate schema in the source directory for local development uv run python -c " - from fastmcp.utilities.fastmcp_config import generate_schema - generate_schema('src/fastmcp/utilities/fastmcp_config/v1/schema.json') - print('✅ Schema generated in utilities/fastmcp_config/v1/') + from fastmcp.utilities.mcp_server_config import generate_schema + generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json') + print('✅ Schema generated in utilities/mcp_server_config/v1/') " - name: Create Pull Request @@ -73,7 +73,7 @@ jobs: body: | This PR updates the fastmcp.json schema files to match the current source code. - The schema is automatically generated from `src/fastmcp/utilities/fastmcp_config/` to ensure consistency. + The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency. **Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means. Feel free to leave it open until you're ready to merge. diff --git a/docs/assets/schemas/fastmcp_config/latest.json b/docs/assets/schemas/mcp_server_config/latest.json similarity index 100% rename from docs/assets/schemas/fastmcp_config/latest.json rename to docs/assets/schemas/mcp_server_config/latest.json diff --git a/docs/assets/schemas/fastmcp_config/v1.json b/docs/assets/schemas/mcp_server_config/v1.json similarity index 100% rename from docs/assets/schemas/fastmcp_config/v1.json rename to docs/assets/schemas/mcp_server_config/v1.json diff --git a/docs/docs.json b/docs/docs.json index 9687185dc..3cd031270 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -380,33 +380,33 @@ "python-sdk/fastmcp-utilities-cli", "python-sdk/fastmcp-utilities-components", "python-sdk/fastmcp-utilities-exceptions", - { - "group": "fastmcp_config", - "pages": [ - "python-sdk/fastmcp-utilities-fastmcp_config-__init__", - { - "group": "v1", - "pages": [ - "python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__", - "python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config", - { - "group": "sources", - "pages": [ - "python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__", - "python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base", - "python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem" - ] - } - ] - } - ] - }, "python-sdk/fastmcp-utilities-http", "python-sdk/fastmcp-utilities-inspect", "python-sdk/fastmcp-utilities-json_schema", "python-sdk/fastmcp-utilities-json_schema_type", "python-sdk/fastmcp-utilities-logging", "python-sdk/fastmcp-utilities-mcp_config", + { + "group": "mcp_server_config", + "pages": [ + "python-sdk/fastmcp-utilities-mcp_server_config-__init__", + { + "group": "v1", + "pages": [ + "python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__", + "python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config", + { + "group": "sources", + "pages": [ + "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__", + "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base", + "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem" + ] + } + ] + } + ] + }, "python-sdk/fastmcp-utilities-openapi", "python-sdk/fastmcp-utilities-tests", "python-sdk/fastmcp-utilities-types" diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx index 60838c769..83677c671 100644 --- a/docs/python-sdk/fastmcp-cli-cli.mdx +++ b/docs/python-sdk/fastmcp-cli-cli.mdx @@ -10,7 +10,7 @@ FastMCP CLI tools using Cyclopts. ## Functions -### `with_argv` +### `with_argv` ```python with_argv(args: list[str] | None) @@ -27,7 +27,7 @@ Args are provided without the script name, so we preserve sys.argv[0] and replace the rest. -### `version` +### `version` ```python version() @@ -37,7 +37,7 @@ version() Display version information and platform details. -### `dev` +### `dev` ```python dev(server_spec: str | None = None) -> None @@ -50,7 +50,7 @@ Run an MCP server with the MCP Inspector for development. - `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json -### `run` +### `run` ```python run(server_spec: str | None = None, *server_args: str) -> None @@ -74,7 +74,7 @@ fastmcp run server.py -- --config config.json --debug - `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect -### `inspect` +### `inspect` ```python inspect(server_spec: str | None = None) -> None @@ -105,7 +105,7 @@ fastmcp inspect # auto-detect fastmcp.json - `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json -### `prepare` +### `prepare` ```python prepare(config_path: Annotated[str | None, cyclopts.Parameter(help='Path to fastmcp.json configuration file')] = None, output_dir: Annotated[str | None, cyclopts.Parameter(help='Directory to create the persistent environment in')] = None, skip_source: Annotated[bool, cyclopts.Parameter(help='Skip source preparation (e.g., git clone)')] = False) -> None diff --git a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx index fd1d888ff..3889da224 100644 --- a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx +++ b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx @@ -57,7 +57,7 @@ Install FastMCP server in Claude Code. - True if installation was successful, False otherwise -### `claude_code_command` +### `claude_code_command` ```python claude_code_command(server_spec: str) -> None diff --git a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx index e31704d32..145de8ea8 100644 --- a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx +++ b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx @@ -44,7 +44,7 @@ Install FastMCP server in Claude Desktop. - True if installation was successful, False otherwise -### `claude_desktop_command` +### `claude_desktop_command` ```python claude_desktop_command(server_spec: str) -> None diff --git a/docs/python-sdk/fastmcp-cli-install-cursor.mdx b/docs/python-sdk/fastmcp-cli-install-cursor.mdx index 218575f86..6c9ada03c 100644 --- a/docs/python-sdk/fastmcp-cli-install-cursor.mdx +++ b/docs/python-sdk/fastmcp-cli-install-cursor.mdx @@ -68,7 +68,7 @@ Install FastMCP server to workspace-specific Cursor configuration. - True if installation was successful, False otherwise -### `install_cursor` +### `install_cursor` ```python install_cursor(file: Path, server_object: str | None, name: str) -> bool @@ -93,7 +93,7 @@ Install FastMCP server in Cursor. - True if installation was successful, False otherwise -### `cursor_command` +### `cursor_command` ```python cursor_command(server_spec: str) -> None diff --git a/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx b/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx index 0d218623e..e13343bd7 100644 --- a/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx +++ b/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx @@ -35,7 +35,7 @@ Generate MCP configuration JSON for manual installation. - True if generation was successful, False otherwise -### `mcp_json_command` +### `mcp_json_command` ```python mcp_json_command(server_spec: str) -> None diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx index 9f352a1e1..f88d8f431 100644 --- a/docs/python-sdk/fastmcp-cli-run.mdx +++ b/docs/python-sdk/fastmcp-cli-run.mdx @@ -10,7 +10,7 @@ FastMCP run command implementation with enhanced type hints. ## Functions -### `is_url` +### `is_url` ```python is_url(path: str) -> bool @@ -20,7 +20,7 @@ is_url(path: str) -> bool Check if a string is a URL. -### `run_with_uv` +### `run_with_uv` ```python run_with_uv(server_spec: str, python_version: str | None = None, with_packages: list[str] | None = None, with_requirements: Path | None = None, project: Path | None = None, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, show_banner: bool = True, editable: str | list[str] | None = None) -> None @@ -29,6 +29,10 @@ run_with_uv(server_spec: str, python_version: str | None = None, with_packages: Run a MCP server using uv run subprocess. +This function is called when we need to set up a Python environment with specific +dependencies before running the server. The config parsing and merging should already +be done by the caller. + **Args:** - `server_spec`: Python file, object specification (file\:obj), config file, or URL - `python_version`: Python version to use (e.g. "3.10") @@ -41,9 +45,10 @@ Run a MCP server using uv run subprocess. - `path`: Path to bind to when using http transport - `log_level`: Log level - `show_banner`: Whether to show the server banner +- `editable`: Editable package paths -### `create_client_server` +### `create_client_server` ```python create_client_server(url: str) -> Any @@ -59,7 +64,7 @@ Create a FastMCP server from a client URL. - A FastMCP server instance -### `create_mcp_config_server` +### `create_mcp_config_server` ```python create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None] @@ -69,10 +74,10 @@ create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None] Create a FastMCP server from a MCPConfig. -### `load_fastmcp_config` +### `load_mcp_server_config` ```python -load_fastmcp_config(config_path: Path) -> FastMCPConfig +load_mcp_server_config(config_path: Path) -> MCPServerConfig ``` @@ -82,10 +87,10 @@ Load a FastMCP configuration from a fastmcp.json file. - `config_path`: Path to fastmcp.json file **Returns:** -- FastMCPConfig object +- MCPServerConfig object -### `run_command` +### `run_command` ```python run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False, skip_source: bool = False) -> None @@ -107,7 +112,7 @@ Run a MCP server or connect to a remote one. - `skip_source`: Whether to skip source preparation step -### `run_v1_server` +### `run_v1_server` ```python run_v1_server(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx index 0b261f9b0..1bc7c1704 100644 --- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx +++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx @@ -7,13 +7,13 @@ sidebarTitle: oauth ## Functions -### `default_cache_dir` +### `default_cache_dir` ```python default_cache_dir() -> Path ``` -### `check_if_auth_required` +### `check_if_auth_required` ```python check_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool @@ -28,7 +28,13 @@ Check if the MCP endpoint requires authentication by making a test request. ## Classes -### `FileTokenStorage` +### `StoredToken` + + +Token storage format with absolute expiry time. + + +### `FileTokenStorage` File-based token storage implementation for OAuth credentials and tokens. @@ -39,7 +45,7 @@ Each instance is tied to a specific server URL for proper token isolation. **Methods:** -#### `get_base_url` +#### `get_base_url` ```python get_base_url(url: str) -> str @@ -48,7 +54,7 @@ get_base_url(url: str) -> str Extract the base URL (scheme + host) from a URL. -#### `get_cache_key` +#### `get_cache_key` ```python get_cache_key(self) -> str @@ -57,7 +63,7 @@ get_cache_key(self) -> str Generate a safe filesystem key from the server's base URL. -#### `get_tokens` +#### `get_tokens` ```python get_tokens(self) -> OAuthToken | None @@ -66,7 +72,7 @@ get_tokens(self) -> OAuthToken | None Load tokens from file storage. -#### `set_tokens` +#### `set_tokens` ```python set_tokens(self, tokens: OAuthToken) -> None @@ -75,7 +81,7 @@ set_tokens(self, tokens: OAuthToken) -> None Save tokens to file storage. -#### `get_client_info` +#### `get_client_info` ```python get_client_info(self) -> OAuthClientInformationFull | None @@ -84,7 +90,7 @@ get_client_info(self) -> OAuthClientInformationFull | None Load client information from file storage. -#### `set_client_info` +#### `set_client_info` ```python set_client_info(self, client_info: OAuthClientInformationFull) -> None @@ -93,7 +99,7 @@ set_client_info(self, client_info: OAuthClientInformationFull) -> None Save client information to file storage. -#### `clear` +#### `clear` ```python clear(self) -> None @@ -102,7 +108,7 @@ clear(self) -> None Clear all cached data for this server. -#### `clear_all` +#### `clear_all` ```python clear_all(cls, cache_dir: Path | None = None) -> None @@ -111,7 +117,7 @@ clear_all(cls, cache_dir: Path | None = None) -> None Clear all cached data for all servers. -### `OAuth` +### `OAuth` OAuth client provider for MCP servers with browser-based authentication. @@ -122,7 +128,7 @@ a browser for user authorization and running a local callback server. **Methods:** -#### `redirect_handler` +#### `redirect_handler` ```python redirect_handler(self, authorization_url: str) -> None @@ -131,7 +137,7 @@ redirect_handler(self, authorization_url: str) -> None Open browser for authorization. -#### `callback_handler` +#### `callback_handler` ```python callback_handler(self) -> tuple[str, str | None] diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx index 406d0c329..cc92038eb 100644 --- a/docs/python-sdk/fastmcp-client-client.mdx +++ b/docs/python-sdk/fastmcp-client-client.mdx @@ -7,7 +7,7 @@ sidebarTitle: client ## Classes -### `ClientSessionState` +### `ClientSessionState` Holds all session-related state for a Client instance. @@ -16,7 +16,7 @@ This allows clean separation of configuration (which is copied) from session state (which should be fresh for each new client instance). -### `Client` +### `Client` MCP client that delegates connection management to a Transport instance. @@ -79,7 +79,7 @@ async with client: **Methods:** -#### `session` +#### `session` ```python session(self) -> ClientSession @@ -88,7 +88,7 @@ session(self) -> ClientSession Get the current active session. Raises RuntimeError if not connected. -#### `initialize_result` +#### `initialize_result` ```python initialize_result(self) -> mcp.types.InitializeResult @@ -97,7 +97,7 @@ initialize_result(self) -> mcp.types.InitializeResult Get the result of the initialization request. -#### `set_roots` +#### `set_roots` ```python set_roots(self, roots: RootsList | RootsHandler) -> None @@ -106,7 +106,7 @@ set_roots(self, roots: RootsList | RootsHandler) -> None Set the roots for the client. This does not automatically call `send_roots_list_changed`. -#### `set_sampling_callback` +#### `set_sampling_callback` ```python set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None @@ -115,7 +115,7 @@ set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None Set the sampling callback for the client. -#### `set_elicitation_callback` +#### `set_elicitation_callback` ```python set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None @@ -124,7 +124,7 @@ set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None Set the elicitation callback for the client. -#### `is_connected` +#### `is_connected` ```python is_connected(self) -> bool @@ -133,7 +133,7 @@ is_connected(self) -> bool Check if the client is currently connected. -#### `new` +#### `new` ```python new(self) -> Client[ClientTransportT] @@ -149,13 +149,13 @@ share state with the original client. - A new Client instance with the same configuration but disconnected state. -#### `close` +#### `close` ```python close(self) ``` -#### `ping` +#### `ping` ```python ping(self) -> bool @@ -164,7 +164,7 @@ ping(self) -> bool Send a ping request. -#### `cancel` +#### `cancel` ```python cancel(self, request_id: str | int, reason: str | None = None) -> None @@ -173,7 +173,7 @@ cancel(self, request_id: str | int, reason: str | None = None) -> None Send a cancellation notification for an in-progress request. -#### `progress` +#### `progress` ```python progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None @@ -182,7 +182,7 @@ progress(self, progress_token: str | int, progress: float, total: float | None = Send a progress notification. -#### `set_logging_level` +#### `set_logging_level` ```python set_logging_level(self, level: mcp.types.LoggingLevel) -> None @@ -191,7 +191,7 @@ set_logging_level(self, level: mcp.types.LoggingLevel) -> None Send a logging/setLevel request. -#### `send_roots_list_changed` +#### `send_roots_list_changed` ```python send_roots_list_changed(self) -> None @@ -200,7 +200,7 @@ send_roots_list_changed(self) -> None Send a roots/list_changed notification. -#### `list_resources_mcp` +#### `list_resources_mcp` ```python list_resources_mcp(self) -> mcp.types.ListResourcesResult @@ -216,7 +216,7 @@ containing the list of resources and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> list[mcp.types.Resource] @@ -231,7 +231,7 @@ Retrieve a list of resources available on the server. - `RuntimeError`: If called while the client is not connected. -#### `list_resource_templates_mcp` +#### `list_resource_templates_mcp` ```python list_resource_templates_mcp(self) -> mcp.types.ListResourceTemplatesResult @@ -247,7 +247,7 @@ containing the list of resource templates and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `list_resource_templates` +#### `list_resource_templates` ```python list_resource_templates(self) -> list[mcp.types.ResourceTemplate] @@ -262,7 +262,7 @@ Retrieve a list of resource templates available on the server. - `RuntimeError`: If called while the client is not connected. -#### `read_resource_mcp` +#### `read_resource_mcp` ```python read_resource_mcp(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult @@ -281,7 +281,7 @@ containing the resource contents and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] @@ -300,7 +300,7 @@ objects, typically containing either text or binary data. - `RuntimeError`: If called while the client is not connected. -#### `list_prompts_mcp` +#### `list_prompts_mcp` ```python list_prompts_mcp(self) -> mcp.types.ListPromptsResult @@ -316,7 +316,7 @@ containing the list of prompts and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self) -> list[mcp.types.Prompt] @@ -331,7 +331,7 @@ Retrieve a list of prompts available on the server. - `RuntimeError`: If called while the client is not connected. -#### `get_prompt_mcp` +#### `get_prompt_mcp` ```python get_prompt_mcp(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult @@ -351,7 +351,7 @@ containing the prompt messages and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult @@ -371,7 +371,7 @@ containing the prompt messages and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `complete_mcp` +#### `complete_mcp` ```python complete_mcp(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.CompleteResult @@ -391,7 +391,7 @@ containing the completion and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `complete` +#### `complete` ```python complete(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.Completion @@ -410,7 +410,7 @@ Send a completion request to the server. - `RuntimeError`: If called while the client is not connected. -#### `list_tools_mcp` +#### `list_tools_mcp` ```python list_tools_mcp(self) -> mcp.types.ListToolsResult @@ -426,7 +426,7 @@ containing the list of tools and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `list_tools` +#### `list_tools` ```python list_tools(self) -> list[mcp.types.Tool] @@ -441,7 +441,7 @@ Retrieve a list of tools available on the server. - `RuntimeError`: If called while the client is not connected. -#### `call_tool_mcp` +#### `call_tool_mcp` ```python call_tool_mcp(self, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.CallToolResult @@ -466,7 +466,7 @@ containing the tool result and any additional metadata. - `RuntimeError`: If called while the client is not connected. -#### `call_tool` +#### `call_tool` ```python call_tool(self, name: str, arguments: dict[str, Any] | None = None, timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, raise_on_error: bool = True) -> CallToolResult @@ -496,10 +496,10 @@ raw result object. - `RuntimeError`: If called while the client is not connected. -#### `generate_name` +#### `generate_name` ```python generate_name(cls, name: str | None = None) -> str ``` -### `CallToolResult` +### `CallToolResult` diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx index b592bdc8c..163843467 100644 --- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx +++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx @@ -182,7 +182,7 @@ Handles provider-specific requirements: **Methods:** -#### `get_client` +#### `get_client` ```python get_client(self, client_id: str) -> OAuthClientInformationFull | None @@ -199,7 +199,7 @@ handles the case where a client with cached tokens reconnects on a different port. -#### `register_client` +#### `register_client` ```python register_client(self, client_info: OAuthClientInformationFull) -> None @@ -226,7 +226,7 @@ The flow: 4. When client reconnects with a different port, ProxyDCRClient accepts it -#### `authorize` +#### `authorize` ```python authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str @@ -240,7 +240,7 @@ This implements the DCR-compliant proxy pattern: 3. Redirect to IdP with our fixed callback URL -#### `load_authorization_code` +#### `load_authorization_code` ```python load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None @@ -252,7 +252,7 @@ Look up our client code and return authorization code object with PKCE challenge for validation. -#### `exchange_authorization_code` +#### `exchange_authorization_code` ```python exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken @@ -264,7 +264,7 @@ For the DCR-compliant proxy flow, we return the IdP tokens that were obtained during the IdP callback exchange. PKCE validation is handled by the MCP framework. -#### `load_refresh_token` +#### `load_refresh_token` ```python load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None @@ -273,7 +273,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) Load refresh token from local storage. -#### `exchange_refresh_token` +#### `exchange_refresh_token` ```python exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken @@ -282,7 +282,7 @@ exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: Exchange refresh token for new access token using authlib. -#### `load_access_token` +#### `load_access_token` ```python load_access_token(self, token: str) -> AccessToken | None @@ -294,7 +294,7 @@ Delegates to the JWT verifier which handles signature validation, expiration checking, and claims validation using the upstream JWKS. -#### `revoke_token` +#### `revoke_token` ```python revoke_token(self, token: AccessToken | RefreshToken) -> None @@ -306,7 +306,7 @@ Removes tokens from local storage and attempts to revoke them with the upstream server if a revocation endpoint is configured. -#### `get_routes` +#### `get_routes` ```python get_routes(self) -> list[Route] diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx index 60781bf24..31d0145d2 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx @@ -65,9 +65,9 @@ Setup Requirements: 4. Note your Client ID and Client Secret -### `AuthKitProviderSettings` +### `AuthKitProviderSettings` -### `AuthKitProvider` +### `AuthKitProvider` AuthKit metadata provider for DCR (Dynamic Client Registration). @@ -93,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification **Methods:** -#### `get_routes` +#### `get_routes` ```python get_routes(self) -> list[Route] diff --git a/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx b/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx index ba44efebe..c7aa26786 100644 --- a/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx +++ b/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx @@ -44,8 +44,9 @@ Validate a redirect URI against allowed patterns. **Args:** - `redirect_uri`: The redirect URI to validate -- `allowed_patterns`: List of allowed patterns. If None, defaults to localhost. - If empty list, all URIs are allowed. +- `allowed_patterns`: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility). + If empty list, no URIs are allowed. + To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS. **Returns:** - True if the redirect URI is allowed diff --git a/docs/python-sdk/fastmcp-server-middleware-logging.mdx b/docs/python-sdk/fastmcp-server-middleware-logging.mdx index 9beb8300a..853725a01 100644 --- a/docs/python-sdk/fastmcp-server-middleware-logging.mdx +++ b/docs/python-sdk/fastmcp-server-middleware-logging.mdx @@ -8,9 +8,21 @@ sidebarTitle: logging Comprehensive logging middleware for FastMCP servers. +## Functions + +### `default_serializer` + +```python +default_serializer(data: Any) -> str +``` + + +The default serializer for Payloads in the logging middleware. + + ## Classes -### `LoggingMiddleware` +### `LoggingMiddleware` Middleware that provides comprehensive request and response logging. @@ -21,16 +33,16 @@ monitoring, and understanding server usage patterns. **Methods:** -#### `on_message` +#### `on_message` ```python -on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any +on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any ``` Log all messages. -### `StructuredLoggingMiddleware` +### `StructuredLoggingMiddleware` Middleware that provides structured JSON logging for better log analysis. @@ -41,10 +53,10 @@ aggregation tools like ELK stack, Splunk, or cloud logging services. **Methods:** -#### `on_message` +#### `on_message` ```python -on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any +on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any ``` Log structured message information. diff --git a/docs/python-sdk/fastmcp-utilities-cli.mdx b/docs/python-sdk/fastmcp-utilities-cli.mdx index 6d49ca6e0..eb4691358 100644 --- a/docs/python-sdk/fastmcp-utilities-cli.mdx +++ b/docs/python-sdk/fastmcp-utilities-cli.mdx @@ -7,7 +7,37 @@ sidebarTitle: cli ## Functions -### `log_server_banner` +### `is_already_in_uv_subprocess` + +```python +is_already_in_uv_subprocess() -> bool +``` + + +Check if we're already running in a FastMCP uv subprocess. + + +### `load_and_merge_config` + +```python +load_and_merge_config(server_spec: str | None, **cli_overrides) -> tuple[MCPServerConfig, str] +``` + + +Load config from server_spec and apply CLI overrides. + +This consolidates the config parsing logic that was duplicated across +run, inspect, and dev commands. + +**Args:** +- `server_spec`: Python file, config file, URL, or None to auto-detect +- `cli_overrides`: CLI arguments that override config values + +**Returns:** +- Tuple of (MCPServerConfig, resolved_server_spec) + + +### `log_server_banner` ```python log_server_banner(server: FastMCP[Any], transport: Literal['stdio', 'http', 'sse', 'streamable-http']) -> None diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-__init__.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-__init__.mdx similarity index 84% rename from docs/python-sdk/fastmcp-utilities-fastmcp_config-__init__.mdx rename to docs/python-sdk/fastmcp-utilities-mcp_server_config-__init__.mdx index f804aa19f..bc8fc9500 100644 --- a/docs/python-sdk/fastmcp-utilities-fastmcp_config-__init__.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-__init__.mdx @@ -3,7 +3,7 @@ title: __init__ sidebarTitle: __init__ --- -# `fastmcp.utilities.fastmcp_config` +# `fastmcp.utilities.mcp_server_config` FastMCP Configuration module. diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx similarity index 74% rename from docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__.mdx rename to docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx index 4195f2dc2..d29664280 100644 --- a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx @@ -3,6 +3,6 @@ title: __init__ sidebarTitle: __init__ --- -# `fastmcp.utilities.fastmcp_config.v1` +# `fastmcp.utilities.mcp_server_config.v1` *This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx similarity index 65% rename from docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx rename to docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx index d66382bc1..fe7815a9b 100644 --- a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx @@ -1,9 +1,9 @@ --- -title: fastmcp_config -sidebarTitle: fastmcp_config +title: mcp_server_config +sidebarTitle: mcp_server_config --- -# `fastmcp.utilities.fastmcp_config.v1.fastmcp_config` +# `fastmcp.utilities.mcp_server_config.v1.mcp_server_config` FastMCP Configuration File Support. @@ -15,7 +15,7 @@ command-line arguments. ## Functions -### `generate_schema` +### `generate_schema` ```python generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None @@ -38,7 +38,7 @@ validation and auto-completion. ## Classes -### `Environment` +### `Environment` Configuration for Python environment setup. @@ -46,22 +46,22 @@ Configuration for Python environment setup. **Methods:** -#### `build_uv_args` +#### `build_uv_run_command` ```python -build_uv_args(self, command: str | list[str] | None = None) -> list[str] +build_uv_run_command(self, command: list[str]) -> list[str] ``` -Build uv run arguments from this environment configuration. +Build complete uv run command with environment args and command to execute. **Args:** -- `command`: Optional command to append (string or list of args) +- `command`: Command to execute (e.g., ["fastmcp", "run", "server.py"]) **Returns:** -- List of arguments for uv run command +- Complete command ready for subprocess.run, including "uv" prefix -#### `run_with_uv` +#### `run_with_uv` ```python run_with_uv(self, command: list[str]) -> None @@ -73,7 +73,7 @@ Execute a command using uv run with this environment configuration. - `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"]) -#### `needs_uv` +#### `needs_uv` ```python needs_uv(self) -> bool @@ -85,7 +85,7 @@ Check if this environment config requires uv to set up. - True if any environment settings require uv run -#### `prepare` +#### `prepare` ```python prepare(self, output_dir: Path | None = None) -> None @@ -98,7 +98,7 @@ Prepare the Python environment using uv. If None, creates a temporary directory for ephemeral use. -### `Deployment` +### `Deployment` Configuration for server deployment and runtime settings. @@ -106,7 +106,7 @@ Configuration for server deployment and runtime settings. **Methods:** -#### `apply_runtime_settings` +#### `apply_runtime_settings` ```python apply_runtime_settings(self, config_path: Path | None = None) -> None @@ -122,7 +122,7 @@ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com" will substitute the value of the ENVIRONMENT variable at runtime. -### `FastMCPConfig` +### `MCPServerConfig` Configuration for a FastMCP server. @@ -133,7 +133,7 @@ a FastMCP server in a declarative format. **Methods:** -#### `validate_source` +#### `validate_source` ```python validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource @@ -146,10 +146,10 @@ Supports: - FileSystemSource instance (passed through) No string parsing happens here - that's only at CLI boundaries. -FastMCPConfig works only with properly typed objects. +MCPServerConfig works only with properly typed objects. -#### `validate_environment` +#### `validate_environment` ```python validate_environment(cls, v: dict | Environment) -> Environment @@ -162,7 +162,7 @@ Accepts: - dict that can be converted to Environment -#### `validate_deployment` +#### `validate_deployment` ```python validate_deployment(cls, v: dict | Deployment) -> Deployment @@ -175,10 +175,10 @@ Accepts: - dict that can be converted to Deployment -#### `from_file` +#### `from_file` ```python -from_file(cls, file_path: Path) -> FastMCPConfig +from_file(cls, file_path: Path) -> MCPServerConfig ``` Load configuration from a JSON file. @@ -187,7 +187,7 @@ Load configuration from a JSON file. - `file_path`: Path to the configuration file **Returns:** -- FastMCPConfig instance +- MCPServerConfig instance **Raises:** - `FileNotFoundError`: If the file doesn't exist @@ -195,10 +195,10 @@ Load configuration from a JSON file. - `pydantic.ValidationError`: If the configuration is invalid -#### `from_cli_args` +#### `from_cli_args` ```python -from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> FastMCPConfig +from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> MCPServerConfig ``` Create a config from CLI arguments. @@ -223,10 +223,10 @@ goes through a config object. - `args`: Server arguments **Returns:** -- FastMCPConfig instance +- MCPServerConfig instance -#### `find_config` +#### `find_config` ```python find_config(cls, start_path: Path | None = None) -> Path | None @@ -241,7 +241,7 @@ Find a fastmcp.json file in the specified directory. - Path to the configuration file, or None if not found -#### `prepare` +#### `prepare` ```python prepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None @@ -257,7 +257,7 @@ When output_dir is None, does ephemeral caching (for backwards compatibility). - `output_dir`: Directory to create the persistent uv project in (optional) -#### `prepare_environment` +#### `prepare_environment` ```python prepare_environment(self, output_dir: Path | None = None) -> None @@ -272,7 +272,7 @@ Prepare the Python environment. Delegates to the environment's prepare() method -#### `prepare_source` +#### `prepare_source` ```python prepare_source(self) -> None @@ -283,7 +283,7 @@ Prepare the source for loading. Delegates to the source's prepare() method. -#### `run_server` +#### `run_server` ```python run_server(self, **kwargs: Any) -> None diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx similarity index 70% rename from docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx rename to docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx index c7259c5a1..38d102b15 100644 --- a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx @@ -3,6 +3,6 @@ title: __init__ sidebarTitle: __init__ --- -# `fastmcp.utilities.fastmcp_config.v1.sources` +# `fastmcp.utilities.mcp_server_config.v1.sources` *This module is empty or contains only private/internal implementations.* diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx similarity index 63% rename from docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base.mdx rename to docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx index 15d2876f7..bf80ca1d6 100644 --- a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx @@ -3,11 +3,11 @@ title: base sidebarTitle: base --- -# `fastmcp.utilities.fastmcp_config.v1.sources.base` +# `fastmcp.utilities.mcp_server_config.v1.sources.base` ## Classes -### `BaseSource` +### `BaseSource` Abstract base class for all source types. @@ -15,7 +15,7 @@ Abstract base class for all source types. **Methods:** -#### `prepare` +#### `prepare` ```python prepare(self) -> None @@ -28,7 +28,7 @@ this method performs that preparation. For sources that don't need preparation (e.g., local files), this is a no-op. -#### `load_server` +#### `load_server` ```python load_server(self) -> Any diff --git a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx similarity index 51% rename from docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx rename to docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx index 68f3bd6fb..9ff83063b 100644 --- a/docs/python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx @@ -3,11 +3,11 @@ title: filesystem sidebarTitle: filesystem --- -# `fastmcp.utilities.fastmcp_config.v1.sources.filesystem` +# `fastmcp.utilities.mcp_server_config.v1.sources.filesystem` ## Classes -### `FileSystemSource` +### `FileSystemSource` Source for local Python files. @@ -15,7 +15,7 @@ Source for local Python files. **Methods:** -#### `parse_path_with_object` +#### `parse_path_with_object` ```python parse_path_with_object(cls, v: str) -> str @@ -27,7 +27,7 @@ This validator runs before the model is created, allowing us to handle the "file.py:object" syntax at the model boundary. -#### `load_server` +#### `load_server` ```python load_server(self) -> Any diff --git a/examples/fastmcp_config_demo/README.md b/examples/fastmcp_config_demo/README.md index 23c17975b..7abf11c3d 100644 --- a/examples/fastmcp_config_demo/README.md +++ b/examples/fastmcp_config_demo/README.md @@ -26,14 +26,14 @@ With the configuration file in place, you can run the server in several ways: ```bash # Auto-detect fastmcp.json in current directory -cd examples/fastmcp_config_demo +cd examples/mcp_server_config_demo fastmcp run # Or specify the config file explicitly -fastmcp run examples/fastmcp_config_demo/fastmcp.json +fastmcp run examples/mcp_server_config_demo/fastmcp.json # Or use development mode with the Inspector UI -fastmcp dev examples/fastmcp_config_demo/fastmcp.json +fastmcp dev examples/mcp_server_config_demo/fastmcp.json ``` ## Benefits diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py index af42da058..5004ccebd 100644 --- a/src/fastmcp/cli/claude.py +++ b/src/fastmcp/cli/claude.py @@ -6,8 +6,8 @@ import sys from pathlib import Path from typing import Any -from fastmcp.utilities.fastmcp_config import Environment from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import Environment logger = get_logger(__name__) diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 09de389db..dd7e65a01 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -20,13 +20,13 @@ import fastmcp from fastmcp.cli import run as run_module from fastmcp.cli.install import install_app from fastmcp.server.server import FastMCP -from fastmcp.utilities.fastmcp_config import Environment, FastMCPConfig from fastmcp.utilities.inspect import ( InspectFormat, format_info, inspect_fastmcp, ) from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import Environment, MCPServerConfig logger = get_logger("cli") console = Console() @@ -796,7 +796,7 @@ async def prepare( # Auto-detect fastmcp.json if not provided if config_path is None: - found_config = FastMCPConfig.find_config() + found_config = MCPServerConfig.find_config() if found_config: config_path = str(found_config) logger.info(f"Using configuration from {config_path}") @@ -816,7 +816,7 @@ async def prepare( try: # Load the configuration - config = FastMCPConfig.from_file(config_file) + config = MCPServerConfig.from_file(config_file) # Prepare environment and source await config.prepare( diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 0117f67b4..7c04c02e2 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -9,8 +9,8 @@ from typing import Annotated import cyclopts from rich import print -from fastmcp.utilities.fastmcp_config import Environment from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import Environment from .shared import process_common_args diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py index d4184f95d..df93a8856 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -9,8 +9,8 @@ import cyclopts from rich import print from fastmcp.mcp_config import StdioMCPServer, update_config_file -from fastmcp.utilities.fastmcp_config import Environment from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import Environment from .shared import process_common_args diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index f10192e16..650ef57ff 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -10,8 +10,8 @@ import cyclopts from rich import print from fastmcp.mcp_config import StdioMCPServer, update_config_file -from fastmcp.utilities.fastmcp_config import Environment from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import Environment from .shared import process_common_args diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py index 5ea466590..7fab1e067 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -9,8 +9,8 @@ import cyclopts import pyperclip from rich import print -from fastmcp.utilities.fastmcp_config import Environment from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import Environment from .shared import process_common_args diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py index 54b61977d..3544a2b6b 100644 --- a/src/fastmcp/cli/install/shared.py +++ b/src/fastmcp/cli/install/shared.py @@ -8,9 +8,9 @@ from dotenv import dotenv_values from pydantic import ValidationError from rich import print -from fastmcp.utilities.fastmcp_config import FastMCPConfig -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import MCPServerConfig +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource logger = get_logger(__name__) @@ -40,7 +40,7 @@ async def process_common_args( # Convert None to empty lists for list parameters with_packages = with_packages or [] env_vars = env_vars or [] - # Create FastMCPConfig from server_spec + # Create MCPServerConfig from server_spec config = None if server_spec.endswith(".json"): config_path = Path(server_spec).resolve() @@ -58,8 +58,8 @@ async def process_common_args( print("[red]MCPConfig files are not supported for installation[/red]") sys.exit(1) else: - # It's a FastMCPConfig - config = FastMCPConfig.from_file(config_path) + # It's a MCPServerConfig + config = MCPServerConfig.from_file(config_path) # Merge packages from config if not overridden if config.environment.dependencies: @@ -72,7 +72,7 @@ async def process_common_args( else: # Create config from file path source = FileSystemSource(path=server_spec) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) # Extract file and server_object from the source # The FileSystemSource handles parsing path:object syntax diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 2b812e625..c01ea0085 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -11,12 +11,12 @@ from typing import Any, Literal from mcp.server.fastmcp import FastMCP as FastMCP1x from fastmcp.server.server import FastMCP -from fastmcp.utilities.fastmcp_config import ( - Environment, - FastMCPConfig, -) -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import ( + Environment, + MCPServerConfig, +) +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource logger = get_logger("cli.run") @@ -143,16 +143,16 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]: return server -def load_fastmcp_config(config_path: Path) -> FastMCPConfig: +def load_mcp_server_config(config_path: Path) -> MCPServerConfig: """Load a FastMCP configuration from a fastmcp.json file. Args: config_path: Path to fastmcp.json file Returns: - FastMCPConfig object + MCPServerConfig object """ - config = FastMCPConfig.from_file(config_path) + config = MCPServerConfig.from_file(config_path) # Apply runtime settings from deployment config config.deployment.apply_runtime_settings(config_path) @@ -204,7 +204,7 @@ async def run_command( server = create_mcp_config_server(config_path) else: # It's a FastMCP config - load it properly - config = load_fastmcp_config(config_path) + config = load_mcp_server_config(config_path) # Merge deployment config with CLI arguments (CLI takes precedence) transport = transport or config.deployment.transport @@ -232,9 +232,9 @@ async def run_command( logger.debug(f'Found server "{server.name}" from config {config_path}') else: - # Regular file case - create a FastMCPConfig with FileSystemSource + # Regular file case - create a MCPServerConfig with FileSystemSource source = FileSystemSource(path=server_spec) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) # Prepare source only (environment is handled by uv run) await config.prepare_source() if not skip_source else None diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index ec94a1f17..c2b4c596f 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -36,8 +36,8 @@ from fastmcp.client.auth.oauth import OAuth from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url from fastmcp.server.dependencies import get_http_headers from fastmcp.server.server import FastMCP -from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import Environment from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import Environment logger = get_logger(__name__) diff --git a/src/fastmcp/utilities/cli.py b/src/fastmcp/utilities/cli.py index 55081a5ad..2ae540d89 100644 --- a/src/fastmcp/utilities/cli.py +++ b/src/fastmcp/utilities/cli.py @@ -14,9 +14,9 @@ from rich.table import Table from rich.text import Text import fastmcp -from fastmcp.utilities.fastmcp_config import FastMCPConfig -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config import MCPServerConfig +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource from fastmcp.utilities.types import get_cached_typeadapter if TYPE_CHECKING: @@ -33,7 +33,7 @@ def is_already_in_uv_subprocess() -> bool: def load_and_merge_config( server_spec: str | None, **cli_overrides, -) -> tuple[FastMCPConfig, str]: +) -> tuple[MCPServerConfig, str]: """Load config from server_spec and apply CLI overrides. This consolidates the config parsing logic that was duplicated across @@ -44,7 +44,7 @@ def load_and_merge_config( cli_overrides: CLI arguments that override config values Returns: - Tuple of (FastMCPConfig, resolved_server_spec) + Tuple of (MCPServerConfig, resolved_server_spec) """ config = None config_path = None @@ -53,7 +53,7 @@ def load_and_merge_config( if server_spec is None: config_path = Path("fastmcp.json") if not config_path.exists(): - found_config = FastMCPConfig.find_config() + found_config = MCPServerConfig.find_config() if found_config: config_path = found_config else: @@ -81,9 +81,9 @@ def load_and_merge_config( # MCPConfig - we don't process these here, just pass through pass else: - # Try to parse as FastMCPConfig + # Try to parse as MCPServerConfig try: - adapter = get_cached_typeadapter(FastMCPConfig) + adapter = get_cached_typeadapter(MCPServerConfig) config = adapter.validate_python(data) # Apply deployment settings @@ -91,7 +91,7 @@ def load_and_merge_config( config.deployment.apply_runtime_settings(config_path) except ValidationError: - # Not a valid FastMCPConfig, just pass through + # Not a valid MCPServerConfig, just pass through pass except (json.JSONDecodeError, FileNotFoundError): # Not a valid JSON file, just pass through @@ -100,7 +100,7 @@ def load_and_merge_config( # If we don't have a config object yet, create one from filesystem source if config is None: source = FileSystemSource(path=resolved_spec) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) # Convert to dict for immutable transformation config_dict = config.model_dump() @@ -134,7 +134,7 @@ def load_and_merge_config( config_dict["deployment"]["args"] = server_args_override # Create new config from modified dict - new_config = FastMCPConfig(**config_dict) + new_config = MCPServerConfig(**config_dict) return new_config, resolved_spec diff --git a/src/fastmcp/utilities/fastmcp_config/__init__.py b/src/fastmcp/utilities/mcp_server_config/__init__.py similarity index 56% rename from src/fastmcp/utilities/fastmcp_config/__init__.py rename to src/fastmcp/utilities/mcp_server_config/__init__.py index 28236fe42..363aa3b30 100644 --- a/src/fastmcp/utilities/fastmcp_config/__init__.py +++ b/src/fastmcp/utilities/mcp_server_config/__init__.py @@ -4,20 +4,20 @@ This module provides versioned configuration support for FastMCP servers. The current version is v1, which is re-exported here for convenience. """ -from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import ( +from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import ( Deployment, Environment, - FastMCPConfig, + MCPServerConfig, generate_schema, ) -from fastmcp.utilities.fastmcp_config.v1.sources.base import BaseSource -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource +from fastmcp.utilities.mcp_server_config.v1.sources.base import BaseSource +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource __all__ = [ "BaseSource", "Deployment", "Environment", - "FastMCPConfig", + "MCPServerConfig", "FileSystemSource", "generate_schema", ] diff --git a/src/fastmcp/utilities/fastmcp_config/v1/__init__.py b/src/fastmcp/utilities/mcp_server_config/v1/__init__.py similarity index 100% rename from src/fastmcp/utilities/fastmcp_config/v1/__init__.py rename to src/fastmcp/utilities/mcp_server_config/v1/__init__.py diff --git a/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py similarity index 98% rename from src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py rename to src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py index 01cd1b846..ca50470e7 100644 --- a/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py @@ -17,8 +17,8 @@ from typing import TYPE_CHECKING, Any, Literal, overload from pydantic import BaseModel, Field, field_validator -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource logger = get_logger("cli.config") @@ -406,7 +406,7 @@ class Deployment(BaseModel): return re.sub(r"\$\{([^}]+)\}", replace_var, value) -class FastMCPConfig(BaseModel): +class MCPServerConfig(BaseModel): """Configuration for a FastMCP server. This configuration file allows you to specify all settings needed to run @@ -463,7 +463,7 @@ class FastMCPConfig(BaseModel): - FileSystemSource instance (passed through) No string parsing happens here - that's only at CLI boundaries. - FastMCPConfig works only with properly typed objects. + MCPServerConfig works only with properly typed objects. """ if isinstance(v, FileSystemSource): # Already a FileSystemSource instance, return as-is @@ -510,14 +510,14 @@ class FastMCPConfig(BaseModel): raise ValueError("deployment must be a dict, Deployment instance") @classmethod - def from_file(cls, file_path: Path) -> FastMCPConfig: + def from_file(cls, file_path: Path) -> MCPServerConfig: """Load configuration from a JSON file. Args: file_path: Path to the configuration file Returns: - FastMCPConfig instance + MCPServerConfig instance Raises: FileNotFoundError: If the file doesn't exist @@ -550,7 +550,7 @@ class FastMCPConfig(BaseModel): env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None, - ) -> FastMCPConfig: + ) -> MCPServerConfig: """Create a config from CLI arguments. This allows us to have a single code path where everything @@ -573,7 +573,7 @@ class FastMCPConfig(BaseModel): args: Server arguments Returns: - FastMCPConfig instance + MCPServerConfig instance """ # Build environment config if any env args provided environment = None @@ -716,7 +716,7 @@ def generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | N Returns: JSON schema as a dictionary if output_path is None, otherwise None """ - schema = FastMCPConfig.model_json_schema() + schema = MCPServerConfig.model_json_schema() # Add some metadata schema["$id"] = FASTMCP_JSON_SCHEMA diff --git a/src/fastmcp/utilities/fastmcp_config/v1/schema.json b/src/fastmcp/utilities/mcp_server_config/v1/schema.json similarity index 100% rename from src/fastmcp/utilities/fastmcp_config/v1/schema.json rename to src/fastmcp/utilities/mcp_server_config/v1/schema.json diff --git a/src/fastmcp/utilities/fastmcp_config/v1/sources/__init__.py b/src/fastmcp/utilities/mcp_server_config/v1/sources/__init__.py similarity index 100% rename from src/fastmcp/utilities/fastmcp_config/v1/sources/__init__.py rename to src/fastmcp/utilities/mcp_server_config/v1/sources/__init__.py diff --git a/src/fastmcp/utilities/fastmcp_config/v1/sources/base.py b/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py similarity index 100% rename from src/fastmcp/utilities/fastmcp_config/v1/sources/base.py rename to src/fastmcp/utilities/mcp_server_config/v1/sources/base.py diff --git a/src/fastmcp/utilities/fastmcp_config/v1/sources/filesystem.py b/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py similarity index 99% rename from src/fastmcp/utilities/fastmcp_config/v1/sources/filesystem.py rename to src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py index 92fb7ae3e..29c2aeede 100644 --- a/src/fastmcp/utilities/fastmcp_config/v1/sources/filesystem.py +++ b/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py @@ -6,8 +6,8 @@ from typing import Any, Literal from pydantic import Field, field_validator -from fastmcp.utilities.fastmcp_config.v1.sources.base import BaseSource from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config.v1.sources.base import BaseSource logger = get_logger(__name__) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 1d0d078a6..0d8b57c08 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -458,7 +458,7 @@ class TestWindowsSpecific: """Test parsing Windows paths with drive letters and colons.""" from pathlib import Path - from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import ( + from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import ( FileSystemSource, ) diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index ec8b413c1..6d187f934 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -7,12 +7,12 @@ from pathlib import Path import pytest from pydantic import ValidationError -from fastmcp.utilities.fastmcp_config import ( +from fastmcp.utilities.mcp_server_config import ( Deployment, Environment, - FastMCPConfig, + MCPServerConfig, ) -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource class TestFileSystemSource: @@ -20,7 +20,7 @@ class TestFileSystemSource: def test_dict_source_minimal(self): """Test that dict source is converted to FileSystemSource.""" - config = FastMCPConfig(source={"path": "server.py"}) + config = MCPServerConfig(source={"path": "server.py"}) # Dict is converted to FileSystemSource assert isinstance(config.source, FileSystemSource) assert config.source.path == "server.py" @@ -29,7 +29,7 @@ class TestFileSystemSource: def test_dict_source_with_entrypoint(self): """Test dict source with entrypoint field.""" - config = FastMCPConfig(source={"path": "server.py", "entrypoint": "app"}) + 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" @@ -38,7 +38,7 @@ class TestFileSystemSource: def test_filesystem_source_entrypoint(self): """Test FileSystemSource entrypoint format.""" - config = FastMCPConfig( + config = MCPServerConfig( source=FileSystemSource(path="src/server.py", entrypoint="mcp") ) assert isinstance(config.source, FileSystemSource) @@ -52,7 +52,7 @@ class TestEnvironment: def test_environment_config_fields(self): """Test all Environment fields.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={ "python": "3.12", @@ -73,28 +73,28 @@ class TestEnvironment: def test_needs_uv(self): """Test needs_uv() method.""" # No environment config - doesn't need UV - config = FastMCPConfig(source={"path": "server.py"}) + config = MCPServerConfig(source={"path": "server.py"}) assert not config.environment.needs_uv() # Empty environment - doesn't need UV - config = FastMCPConfig(source={"path": "server.py"}, environment={}) + config = MCPServerConfig(source={"path": "server.py"}, environment={}) assert not config.environment.needs_uv() # With dependencies - needs UV - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"dependencies": ["requests"]} ) assert config.environment.needs_uv() # With Python version - needs UV - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"python": "3.12"} ) assert config.environment.needs_uv() def test_build_uv_run_command(self): """Test build_uv_run_command() method.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={ "python": "3.12", @@ -125,7 +125,7 @@ class TestEnvironment: def test_run_with_uv(self): """Test run_with_uv() subprocess execution.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"dependencies": ["requests"]} ) @@ -144,7 +144,7 @@ class TestDeployment: def test_deployment_config_fields(self): """Test all Deployment fields.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={ "transport": "http", @@ -176,7 +176,7 @@ class TestDeployment: work_dir = tmp_path / "work" work_dir.mkdir() - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={ "env": {"TEST_VAR": "test_value"}, @@ -212,7 +212,7 @@ class TestDeployment: os.environ["BASE_URL"] = "example.com" os.environ["ENV_NAME"] = "production" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={ "env": { @@ -253,12 +253,12 @@ class TestDeployment: os.environ[key] = value -class TestFastMCPConfig: - """Test FastMCPConfig root configuration.""" +class TestMCPServerConfig: + """Test MCPServerConfig root configuration.""" def test_minimal_config(self): """Test creating a config with only required fields.""" - config = FastMCPConfig(source={"path": "server.py"}) + config = MCPServerConfig(source={"path": "server.py"}) assert isinstance(config.source, FileSystemSource) assert config.source.path == "server.py" assert config.source.entrypoint is None @@ -274,7 +274,7 @@ class TestFastMCPConfig: def test_nested_structure(self): """Test the nested configuration structure.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={ "python": "3.12", @@ -304,7 +304,7 @@ class TestFastMCPConfig: config_file = tmp_path / "fastmcp.json" config_file.write_text(json.dumps(config_data)) - config = FastMCPConfig.from_file(config_file) + config = MCPServerConfig.from_file(config_file) # When loaded from JSON with entrypoint format, it becomes EntrypointConfig assert isinstance(config.source, FileSystemSource) @@ -325,7 +325,7 @@ class TestFastMCPConfig: config_file = tmp_path / "fastmcp.json" config_file.write_text(json.dumps(config_data)) - config = FastMCPConfig.from_file(config_file) + 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" @@ -342,7 +342,7 @@ class TestFastMCPConfig: config_file = tmp_path / "fastmcp.json" config_file.write_text(json.dumps(config_data)) - config = FastMCPConfig.from_file(config_file) + config = MCPServerConfig.from_file(config_file) # Should be parsed into EntrypointConfig assert isinstance(config.source, FileSystemSource) @@ -365,7 +365,7 @@ class TestFastMCPConfig: original_cwd = os.getcwd() try: os.chdir(tmp_path) - found = FastMCPConfig.find_config() + found = MCPServerConfig.find_config() assert found == config_file finally: os.chdir(original_cwd) @@ -379,7 +379,7 @@ class TestFastMCPConfig: subdir.mkdir() # Should NOT find config in parent directory - found = FastMCPConfig.find_config(subdir) + found = MCPServerConfig.find_config(subdir) assert found is None def test_find_config_in_specified_dir(self, tmp_path): @@ -388,12 +388,12 @@ class TestFastMCPConfig: config_file.write_text(json.dumps({"source": {"path": "server.py"}})) # Should find config when looking in the directory that contains it - found = FastMCPConfig.find_config(tmp_path) + 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 = FastMCPConfig.find_config(tmp_path) + found = MCPServerConfig.find_config(tmp_path) assert found is None def test_invalid_transport(self, tmp_path): @@ -407,12 +407,12 @@ class TestFastMCPConfig: config_file.write_text(json.dumps(config_data)) with pytest.raises(ValidationError): - FastMCPConfig.from_file(config_file) + MCPServerConfig.from_file(config_file) def test_optional_sections(self): """Test that all config sections are optional except source.""" # Only source is required - config = FastMCPConfig(source={"path": "server.py"}) + 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 @@ -420,7 +420,7 @@ class TestFastMCPConfig: assert isinstance(config.deployment, Deployment) # Only environment with values - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"python": "3.12"} ) assert config.environment.python == "3.12" @@ -431,7 +431,7 @@ class TestFastMCPConfig: ) # Only deployment with values - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={"transport": "http"} ) assert isinstance(config.environment, Environment) diff --git a/tests/cli/test_fastmcp_config_integration.py b/tests/cli/test_mcp_server_config_integration.py similarity index 93% rename from tests/cli/test_fastmcp_config_integration.py rename to tests/cli/test_mcp_server_config_integration.py index 800b23b08..e925f1bb4 100644 --- a/tests/cli/test_fastmcp_config_integration.py +++ b/tests/cli/test_mcp_server_config_integration.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest from fastmcp.client import Client -from fastmcp.utilities.fastmcp_config import FastMCPConfig +from fastmcp.utilities.mcp_server_config import MCPServerConfig @pytest.fixture @@ -89,7 +89,7 @@ class TestConfigWithClient: """Test that a server loaded from config works with a client.""" # Load the config config_file = server_with_config / "fastmcp.json" - config = FastMCPConfig.from_file(config_file) + config = MCPServerConfig.from_file(config_file) # Import the server using the source import importlib.util @@ -132,7 +132,7 @@ class TestEnvironmentExecution: def test_needs_uv_with_dependencies(self): """Test that environment with dependencies needs UV.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"dependencies": ["requests", "numpy"]}, # type: ignore[arg-type] ) @@ -142,7 +142,7 @@ class TestEnvironmentExecution: def test_needs_uv_with_python_version(self): """Test that environment with Python version needs UV.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"python": "3.12"}, # type: ignore[arg-type] ) @@ -152,7 +152,7 @@ class TestEnvironmentExecution: def test_no_uv_needed_without_environment(self): """Test that no UV is needed without environment config.""" - config = FastMCPConfig(source={"path": "server.py"}) + config = MCPServerConfig(source={"path": "server.py"}) # Environment is now always present but may be empty assert config.environment is not None @@ -160,7 +160,7 @@ class TestEnvironmentExecution: def test_no_uv_needed_with_empty_environment(self): """Test that no UV is needed with empty environment config.""" - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={}, # type: ignore[arg-type] ) @@ -184,7 +184,7 @@ class TestPathResolution: server_file = src_dir / "server.py" server_file.write_text("# Server") - config = FastMCPConfig(source={"path": "../src/server.py"}) + 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 @@ -198,7 +198,7 @@ class TestPathResolution: work_dir = tmp_path / "work" work_dir.mkdir() - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={"cwd": "work"}, # type: ignore[arg-type] ) @@ -222,7 +222,7 @@ class TestPathResolution: reqs_file = tmp_path / "requirements.txt" reqs_file.write_text("fastmcp>=2.0") - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, environment={"requirements": "requirements.txt"}, # type: ignore[arg-type] ) @@ -243,7 +243,7 @@ class TestConfigValidation: def test_invalid_transport_rejected(self): """Test that invalid transport values are rejected.""" with pytest.raises(ValueError): - FastMCPConfig( + MCPServerConfig( source={"path": "server.py"}, deployment={"transport": "invalid_transport"}, # type: ignore[arg-type] ) @@ -251,7 +251,7 @@ class TestConfigValidation: def test_streamable_http_transport_rejected(self): """Test that streamable-http transport is rejected in fastmcp.json config.""" with pytest.raises(ValueError): - FastMCPConfig( + MCPServerConfig( source={"path": "server.py"}, deployment={"transport": "streamable-http"}, # type: ignore[arg-type] ) @@ -259,7 +259,7 @@ class TestConfigValidation: def test_invalid_log_level_rejected(self): """Test that invalid log level values are rejected.""" with pytest.raises(ValueError): - FastMCPConfig( + MCPServerConfig( source={"path": "server.py"}, deployment={"log_level": "INVALID"}, # type: ignore[arg-type] ) @@ -267,12 +267,12 @@ class TestConfigValidation: def test_missing_source_rejected(self): """Test that config without source is rejected.""" with pytest.raises(ValueError): - FastMCPConfig() # type: ignore[call-arg] + MCPServerConfig() # type: ignore[call-arg] def test_valid_transport_values(self): """Test that all valid transport values are accepted.""" for transport in ["stdio", "http", "sse"]: - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={"transport": transport}, # type: ignore[arg-type] ) @@ -282,7 +282,7 @@ class TestConfigValidation: def test_valid_log_levels(self): """Test that all valid log levels are accepted.""" for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: - config = FastMCPConfig( + config = MCPServerConfig( source={"path": "server.py"}, deployment={"log_level": level}, # type: ignore[arg-type] ) diff --git a/tests/cli/test_fastmcp_config_schema.py b/tests/cli/test_mcp_server_config_schema.py similarity index 96% rename from tests/cli/test_fastmcp_config_schema.py rename to tests/cli/test_mcp_server_config_schema.py index 5c05cd65f..1fda6e91b 100644 --- a/tests/cli/test_fastmcp_config_schema.py +++ b/tests/cli/test_mcp_server_config_schema.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema +from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import generate_schema def test_schema_file_matches_pydantic_model(): @@ -14,7 +14,7 @@ def test_schema_file_matches_pydantic_model(): / "src" / "fastmcp" / "utilities" - / "fastmcp_config" + / "mcp_server_config" / "v1" / "schema.json" ) @@ -30,7 +30,7 @@ def test_schema_file_matches_pydantic_model(): 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.fastmcp_config.v1.fastmcp_config import generate_schema; ' + '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}" ) diff --git a/tests/cli/test_project_prepare.py b/tests/cli/test_project_prepare.py index d23f61f32..5a2ad15b1 100644 --- a/tests/cli/test_project_prepare.py +++ b/tests/cli/test_project_prepare.py @@ -6,24 +6,24 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from fastmcp.utilities.fastmcp_config import Environment, FastMCPConfig -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource +from fastmcp.utilities.mcp_server_config import Environment, MCPServerConfig +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource -class TestFastMCPConfigPrepare: - """Test the FastMCPConfig.prepare() method.""" +class TestMCPServerConfigPrepare: + """Test the MCPServerConfig.prepare() method.""" @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source", new_callable=AsyncMock, ) @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_environment", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment", new_callable=AsyncMock, ) async def test_prepare_calls_both_methods(self, mock_env, mock_src): """Test that prepare() calls both prepare_environment and prepare_source.""" - config = FastMCPConfig( + config = MCPServerConfig( source=FileSystemSource(path="server.py"), environment=Environment(python="3.10"), ) @@ -34,16 +34,16 @@ class TestFastMCPConfigPrepare: mock_src.assert_called_once() @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source", new_callable=AsyncMock, ) @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_environment", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment", new_callable=AsyncMock, ) async def test_prepare_with_output_dir(self, mock_env, mock_src): """Test that prepare() with output_dir calls prepare_environment with it.""" - config = FastMCPConfig( + config = MCPServerConfig( source=FileSystemSource(path="server.py"), environment=Environment(python="3.10"), ) @@ -55,16 +55,16 @@ class TestFastMCPConfigPrepare: mock_src.assert_called_once() @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source", new_callable=AsyncMock, ) @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_environment", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment", new_callable=AsyncMock, ) async def test_prepare_skip_source(self, mock_env, mock_src): """Test that prepare() skips source when skip_source=True.""" - config = FastMCPConfig( + config = MCPServerConfig( source=FileSystemSource(path="server.py"), environment=Environment(python="3.10"), ) @@ -75,16 +75,16 @@ class TestFastMCPConfigPrepare: mock_src.assert_not_called() @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source", new_callable=AsyncMock, ) @patch( - "fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment.prepare", + "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.Environment.prepare", new_callable=AsyncMock, ) async def test_prepare_no_environment_settings(self, mock_env_prepare, mock_src): """Test that prepare() works with default empty environment config.""" - config = FastMCPConfig( + config = MCPServerConfig( source=FileSystemSource(path="server.py"), # environment defaults to empty Environment() ) @@ -188,8 +188,8 @@ class TestEnvironmentPrepare: class TestProjectPrepareCommand: """Test the CLI project prepare command.""" - @patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.from_file") - @patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.find_config") + @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file") + @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config") async def test_project_prepare_auto_detect(self, mock_find, mock_from_file): """Test project prepare with auto-detected config.""" from fastmcp.cli.cli import prepare @@ -220,7 +220,7 @@ class TestProjectPrepareCommand: assert "Project prepared successfully" in success_call @patch("pathlib.Path.exists") - @patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.from_file") + @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file") async def test_project_prepare_explicit_path(self, mock_from_file, mock_exists): """Test project prepare with explicit config path.""" from fastmcp.cli.cli import prepare @@ -243,7 +243,7 @@ class TestProjectPrepareCommand: output_dir=Path("./test-env"), ) - @patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.find_config") + @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config") async def test_project_prepare_no_config_found(self, mock_find): """Test project prepare when no config is found.""" from fastmcp.cli.cli import prepare @@ -280,7 +280,7 @@ class TestProjectPrepareCommand: assert "--output-dir parameter is required" in error_msg @patch("pathlib.Path.exists") - @patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.from_file") + @patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file") async def test_project_prepare_failure(self, mock_from_file, mock_exists): """Test project prepare when prepare() fails.""" from fastmcp.cli.cli import prepare diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index f5375f7e0..685e8aff0 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -13,7 +13,7 @@ from fastmcp.client.client import Client from fastmcp.client.transports import FastMCPTransport from fastmcp.mcp_config import MCPConfig, StdioMCPServer from fastmcp.server.server import FastMCP -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource class TestUrlDetection: @@ -339,7 +339,7 @@ mcp = fastmcp.FastMCP("TestServer") from unittest.mock import AsyncMock, patch from fastmcp.cli.run import run_command - from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import ( + from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import ( FileSystemSource, ) @@ -368,7 +368,7 @@ mcp = fastmcp.FastMCP("TestServer") from unittest.mock import AsyncMock, patch from fastmcp.cli.run import run_command - from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import ( + from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import ( FileSystemSource, ) diff --git a/tests/cli/test_run_config.py b/tests/cli/test_run_config.py index 07443414f..abc1f10af 100644 --- a/tests/cli/test_run_config.py +++ b/tests/cli/test_run_config.py @@ -6,13 +6,13 @@ from pathlib import Path import pytest -from fastmcp.cli.run import load_fastmcp_config -from fastmcp.utilities.fastmcp_config import ( +from fastmcp.cli.run import load_mcp_server_config +from fastmcp.utilities.mcp_server_config import ( Deployment, Environment, - FastMCPConfig, + MCPServerConfig, ) -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource @pytest.fixture @@ -43,17 +43,17 @@ def test_tool(message: str) -> str: return config_file -def test_load_fastmcp_config(sample_config, monkeypatch): +def test_load_mcp_server_config(sample_config, monkeypatch): """Test loading configuration and returning config subsets.""" # Capture environment changes original_env = dict(os.environ) try: - config = load_fastmcp_config(sample_config) + config = load_mcp_server_config(sample_config) # Check that we got the right types - assert isinstance(config, FastMCPConfig) + assert isinstance(config, MCPServerConfig) assert isinstance(config.source, FileSystemSource) assert isinstance(config.deployment, Deployment) assert isinstance(config.environment, Environment) @@ -95,7 +95,7 @@ def test_load_config_with_entrypoint_source(tmp_path): server_file = src_dir / "server.py" server_file.write_text("# Server") - config = load_fastmcp_config(config_file) + config = load_mcp_server_config(config_file) # Check source - path is not resolved yet, only during load_server assert config.source.path == "src/server.py" @@ -125,7 +125,7 @@ def test_load_config_with_cwd(tmp_path): original_cwd = os.getcwd() try: - config = load_fastmcp_config(config_file) # noqa: F841 + config = load_mcp_server_config(config_file) # noqa: F841 # Check that working directory was changed assert Path.cwd() == subdir.resolve() @@ -160,7 +160,7 @@ def test_load_config_with_relative_cwd(tmp_path): original_cwd = os.getcwd() try: - config = load_fastmcp_config(config_file) # noqa: F841 + config = load_mcp_server_config(config_file) # noqa: F841 # Should change to parent directory of config file assert Path.cwd() == subdir1.resolve() @@ -180,7 +180,7 @@ def test_load_minimal_config(tmp_path): server_file = tmp_path / "server.py" server_file.write_text("# Server") - config = load_fastmcp_config(config_file) + config = load_mcp_server_config(config_file) # Check we got source - path is not resolved yet, only during load_server assert isinstance(config.source, FileSystemSource) @@ -201,7 +201,7 @@ def test_load_config_with_server_args(tmp_path): server_file = tmp_path / "server.py" server_file.write_text("# Server") - config = load_fastmcp_config(config_file) + config = load_mcp_server_config(config_file) assert config.deployment.args == ["--debug", "--config", "custom.json"] @@ -221,7 +221,7 @@ def test_config_subset_independence(tmp_path): server_file = tmp_path / "server.py" server_file.write_text("# Server") - config = load_fastmcp_config(config_file) + config = load_mcp_server_config(config_file) # Each subset should be independently usable # Path is not resolved yet, only during load_server @@ -259,7 +259,7 @@ def test_environment_config_path_resolution(tmp_path): server_file = tmp_path / "server.py" server_file.write_text("# Server") - config = load_fastmcp_config(config_file) + config = load_mcp_server_config(config_file) # Check that UV command is built with resolved paths uv_cmd = config.environment.build_uv_run_command(["fastmcp", "run", "server.py"]) diff --git a/tests/cli/test_server_args.py b/tests/cli/test_server_args.py index 5ec7ccc6a..8f1782dbf 100644 --- a/tests/cli/test_server_args.py +++ b/tests/cli/test_server_args.py @@ -4,8 +4,8 @@ from pathlib import Path import pytest -from fastmcp.utilities.fastmcp_config import FastMCPConfig -from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource +from fastmcp.utilities.mcp_server_config import MCPServerConfig +from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource class TestServerArguments: @@ -39,7 +39,7 @@ def get_config() -> dict: # Test with arguments source = FileSystemSource(path=str(server_file)) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) from fastmcp.cli.cli import with_argv @@ -69,7 +69,7 @@ mcp = FastMCP(args.name) """) source = FileSystemSource(path=str(server_file)) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) from fastmcp.cli.cli import with_argv @@ -96,7 +96,7 @@ mcp = FastMCP(name) """) source = FileSystemSource(path=str(server_file)) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) from fastmcp.cli.cli import with_argv @@ -123,7 +123,7 @@ mcp = FastMCP(name) pytest.skip("config_server.py example not found") source = FileSystemSource(path=str(config_server)) - config = FastMCPConfig(source=source) + config = MCPServerConfig(source=source) from fastmcp.cli.cli import with_argv diff --git a/tests/utilities/test_cli.py b/tests/utilities/test_cli.py index cd8b55dcb..56d5a5255 100644 --- a/tests/utilities/test_cli.py +++ b/tests/utilities/test_cli.py @@ -1,6 +1,6 @@ """Tests for CLI utility functions.""" -from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import Environment +from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import Environment class TestEnvironmentBuildUVRunCommand: From ea47d232bc7c4c360a36ffafe9bdca25a9612a0e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 29 Aug 2025 22:29:35 -0400 Subject: [PATCH 03/12] Refactor Environment to support multiple runtime types (#1673) --- src/fastmcp/cli/claude.py | 6 +- src/fastmcp/cli/cli.py | 25 +- src/fastmcp/cli/install/claude_code.py | 6 +- src/fastmcp/cli/install/claude_desktop.py | 6 +- src/fastmcp/cli/install/cursor.py | 10 +- src/fastmcp/cli/install/mcp_json.py | 6 +- src/fastmcp/cli/run.py | 6 +- src/fastmcp/client/transports.py | 4 +- .../utilities/mcp_server_config/__init__.py | 4 +- .../v1/environments/__init__.py | 6 + .../mcp_server_config/v1/environments/base.py | 28 ++ .../mcp_server_config/v1/environments/uv.py | 303 ++++++++++++++++++ .../mcp_server_config/v1/mcp_server_config.py | 301 +---------------- .../mcp_server_config/v1/schema.json | 76 ++--- tests/cli/test_config.py | 14 +- .../cli/test_mcp_server_config_integration.py | 2 +- tests/cli/test_project_prepare.py | 21 +- tests/cli/test_run_config.py | 6 +- tests/cli/test_run_with_uv.py | 6 +- tests/utilities/test_cli.py | 51 +-- 20 files changed, 480 insertions(+), 407 deletions(-) create mode 100644 src/fastmcp/utilities/mcp_server_config/v1/environments/__init__.py create mode 100644 src/fastmcp/utilities/mcp_server_config/v1/environments/base.py create mode 100644 src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py index 5004ccebd..1a97defb6 100644 --- a/src/fastmcp/cli/claude.py +++ b/src/fastmcp/cli/claude.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment logger = get_logger(__name__) @@ -99,7 +99,7 @@ def update_claude_config( deduplicated_packages = None # Build uv run command using Environment.build_uv_run_command() - env_config = Environment( + env_config = UVEnvironment( dependencies=deduplicated_packages, editable=[str(p) for p in with_editable] if with_editable else None, ) @@ -113,7 +113,7 @@ def update_claude_config( file_spec = str(Path(file_spec).resolve()) # Build the full command - full_command = env_config.build_uv_run_command(["fastmcp", "run", file_spec]) + full_command = env_config.build_command(["fastmcp", "run", file_spec]) # Extract command and args for the config server_config: dict[str, Any] = { diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index dd7e65a01..076277932 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -26,7 +26,8 @@ from fastmcp.utilities.inspect import ( inspect_fastmcp, ) from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config import Environment, MCPServerConfig +from fastmcp.utilities.mcp_server_config import MCPServerConfig +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment logger = get_logger("cli") console = Console() @@ -245,7 +246,7 @@ async def dev( env_deps = config.environment.dependencies or [] all_deps = list(set(env_deps + server.dependencies)) if not config.environment: - config.environment = Environment(dependencies=all_deps) + config.environment = UVEnvironment(dependencies=all_deps) else: config.environment.dependencies = all_deps @@ -269,7 +270,7 @@ async def dev( inspector_cmd += f"@{inspector_version}" # Use the environment from config (already has CLI overrides applied) - uv_cmd = config.environment.build_uv_run_command( + uv_cmd = config.environment.build_command( ["fastmcp", "run", server_spec, "--no-banner"] ) @@ -460,7 +461,9 @@ async def run( ) # Check if we need to use uv run (but skip if we're already in uv or user said to skip) - needs_uv = config.environment.needs_uv() and not skip_env + # We check if the environment would modify the command + test_cmd = ["test"] + needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env if needs_uv: # Use uv run subprocess - always use run_with_uv which handles output correctly @@ -627,7 +630,9 @@ async def inspect( sys.exit(1) # Check if we need to use uv run (but skip if we're already in uv or user said to skip) - needs_uv = config.environment.needs_uv() and not skip_env + # We check if the environment would modify the command + test_cmd = ["test"] + needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env if needs_uv: # Build and run uv command @@ -643,8 +648,14 @@ async def inspect( inspect_command.extend(["--format", format.value]) if output: inspect_command.extend(["--output", str(output)]) - config.environment.run_with_uv(inspect_command) - return # run_with_uv exits the process + + # Run the command using subprocess + import subprocess + + cmd = config.environment.build_command(inspect_command) + env = os.environ | {"FASTMCP_UV_SPAWNED": "1"} + process = subprocess.run(cmd, check=True, env=env) + sys.exit(process.returncode) logger.debug( "Inspecting server", diff --git a/src/fastmcp/cli/install/claude_code.py b/src/fastmcp/cli/install/claude_code.py index 7c04c02e2..e3ecbf26c 100644 --- a/src/fastmcp/cli/install/claude_code.py +++ b/src/fastmcp/cli/install/claude_code.py @@ -10,7 +10,7 @@ import cyclopts from rich import print from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from .shared import process_common_args @@ -116,7 +116,7 @@ def install_claude_code( deduplicated_packages = None # Build uv run command using Environment.build_uv_run_command() - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=deduplicated_packages, requirements=str(with_requirements) if with_requirements else None, @@ -131,7 +131,7 @@ def install_claude_code( server_spec = str(file.resolve()) # Build the full command - full_command = env_config.build_uv_run_command(["fastmcp", "run", server_spec]) + full_command = env_config.build_command(["fastmcp", "run", server_spec]) # Build claude mcp add command cmd_parts = [claude_cmd, "mcp", "add"] diff --git a/src/fastmcp/cli/install/claude_desktop.py b/src/fastmcp/cli/install/claude_desktop.py index df93a8856..44bca8c70 100644 --- a/src/fastmcp/cli/install/claude_desktop.py +++ b/src/fastmcp/cli/install/claude_desktop.py @@ -10,7 +10,7 @@ from rich import print from fastmcp.mcp_config import StdioMCPServer, update_config_file from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from .shared import process_common_args @@ -81,7 +81,7 @@ def install_claude_desktop( if not deduplicated_packages: deduplicated_packages = None - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=deduplicated_packages, requirements=str(with_requirements) if with_requirements else None, @@ -95,7 +95,7 @@ def install_claude_desktop( server_spec = str(file.resolve()) # Build the full command - full_command = env_config.build_uv_run_command(["fastmcp", "run", server_spec]) + full_command = env_config.build_command(["fastmcp", "run", server_spec]) # Create server configuration server_config = StdioMCPServer( diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index 650ef57ff..387787367 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -11,7 +11,7 @@ from rich import print from fastmcp.mcp_config import StdioMCPServer, update_config_file from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from .shared import process_common_args @@ -115,7 +115,7 @@ def install_cursor_workspace( if not deduplicated_packages: deduplicated_packages = None - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=deduplicated_packages, requirements=str(with_requirements.resolve()) if with_requirements else None, @@ -129,7 +129,7 @@ def install_cursor_workspace( server_spec = str(file.resolve()) # Build the full command - full_command = env_config.build_uv_run_command(["fastmcp", "run", server_spec]) + full_command = env_config.build_command(["fastmcp", "run", server_spec]) # Create server configuration server_config = StdioMCPServer( @@ -193,7 +193,7 @@ def install_cursor( if not deduplicated_packages: deduplicated_packages = None - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=deduplicated_packages, requirements=str(with_requirements.resolve()) if with_requirements else None, @@ -207,7 +207,7 @@ def install_cursor( server_spec = str(file.resolve()) # Build the full command - full_command = env_config.build_uv_run_command(["fastmcp", "run", server_spec]) + full_command = env_config.build_command(["fastmcp", "run", server_spec]) # If workspace is specified, install to workspace-specific config if workspace: diff --git a/src/fastmcp/cli/install/mcp_json.py b/src/fastmcp/cli/install/mcp_json.py index 7fab1e067..aa860eebf 100644 --- a/src/fastmcp/cli/install/mcp_json.py +++ b/src/fastmcp/cli/install/mcp_json.py @@ -10,7 +10,7 @@ import pyperclip from rich import print from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from .shared import process_common_args @@ -56,7 +56,7 @@ def install_mcp_json( if not deduplicated_packages: deduplicated_packages = None - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=deduplicated_packages, requirements=str(with_requirements) if with_requirements else None, @@ -70,7 +70,7 @@ def install_mcp_json( server_spec = str(file.resolve()) # Build the full command - full_command = env_config.build_uv_run_command(["fastmcp", "run", server_spec]) + full_command = env_config.build_command(["fastmcp", "run", server_spec]) # Build MCP server configuration server_config = { diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index c01ea0085..f63737ed8 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -13,9 +13,9 @@ from mcp.server.fastmcp import FastMCP as FastMCP1x from fastmcp.server.server import FastMCP from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config import ( - Environment, MCPServerConfig, ) +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource logger = get_logger("cli.run") @@ -67,7 +67,7 @@ def run_with_uv( """ # Build uv command using Environment.build_uv_run_command() - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=with_packages if with_packages else None, requirements=str(with_requirements.resolve()) if with_requirements else None, @@ -97,7 +97,7 @@ def run_with_uv( inner_cmd.append("--no-banner") # Build the full uv command - cmd = env_config.build_uv_run_command(inner_cmd) + cmd = env_config.build_command(inner_cmd) # Set marker to prevent infinite loops when subprocess calls FastMCP again env = os.environ | {"FASTMCP_UV_SPAWNED": "1"} diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index c2b4c596f..5e031da38 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -37,7 +37,7 @@ from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url from fastmcp.server.dependencies import get_http_headers from fastmcp.server.server import FastMCP from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment logger = get_logger(__name__) @@ -597,7 +597,7 @@ class UvStdioTransport(StdioTransport): ) # Create Environment from provided parameters (internal use) - env_config = Environment( + env_config = UVEnvironment( python=python_version, dependencies=with_packages, requirements=with_requirements, diff --git a/src/fastmcp/utilities/mcp_server_config/__init__.py b/src/fastmcp/utilities/mcp_server_config/__init__.py index 363aa3b30..a51dbcf6d 100644 --- a/src/fastmcp/utilities/mcp_server_config/__init__.py +++ b/src/fastmcp/utilities/mcp_server_config/__init__.py @@ -4,9 +4,10 @@ This module provides versioned configuration support for FastMCP servers. The current version is v1, which is re-exported here for convenience. """ +from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import ( Deployment, - Environment, MCPServerConfig, generate_schema, ) @@ -17,6 +18,7 @@ __all__ = [ "BaseSource", "Deployment", "Environment", + "UVEnvironment", "MCPServerConfig", "FileSystemSource", "generate_schema", diff --git a/src/fastmcp/utilities/mcp_server_config/v1/environments/__init__.py b/src/fastmcp/utilities/mcp_server_config/v1/environments/__init__.py new file mode 100644 index 000000000..3cccf548f --- /dev/null +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/__init__.py @@ -0,0 +1,6 @@ +"""Environment configuration for MCP servers.""" + +from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment + +__all__ = ["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 new file mode 100644 index 000000000..c4a23a716 --- /dev/null +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod +from pathlib import Path + +from pydantic import BaseModel + + +class Environment(BaseModel, ABC): + """Base class for environment configuration.""" + + @abstractmethod + def build_command(self, command: list[str]) -> list[str]: + """Build the full command with environment setup. + + Args: + command: Base command to wrap with environment setup + + Returns: + Full command ready for subprocess execution + """ + pass + + async def prepare(self, output_dir: Path | None = None) -> None: + """Prepare the environment (optional, can be no-op). + + Args: + output_dir: Directory for persistent environment setup + """ + pass # Default no-op implementation diff --git a/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py new file mode 100644 index 000000000..a0500ca22 --- /dev/null +++ b/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py @@ -0,0 +1,303 @@ +import os +import shutil +import subprocess +import sys +from pathlib import Path + +from pydantic import Field + +from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment + +logger = get_logger("cli.config") + + +class UVEnvironment(Environment): + """Configuration for Python environment setup.""" + + python: str | None = Field( + default=None, + description="Python version constraint", + examples=["3.10", "3.11", "3.12"], + ) + + dependencies: list[str] | None = Field( + default=None, + description="Python packages to install with PEP 508 specifiers", + examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]], + ) + + requirements: str | None = Field( + default=None, + description="Path to requirements.txt file", + examples=["requirements.txt", "../requirements/prod.txt"], + ) + + project: str | None = Field( + default=None, + description="Path to project directory containing pyproject.toml", + examples=[".", "../my-project"], + ) + + editable: list[str] | None = Field( + default=None, + description="Directories to install in editable mode", + examples=[[".", "../my-package"], ["/path/to/package"]], + ) + + def build_command(self, command: list[str]) -> list[str]: + """Build complete uv run command with environment args and command to execute. + + Args: + command: Command to execute (e.g., ["fastmcp", "run", "server.py"]) + + Returns: + Complete command ready for subprocess.run, including "uv" prefix if needed. + If no environment configuration is set, returns the command unchanged. + """ + # If no environment setup is needed, return command as-is + if not self._needs_setup(): + return command + + args = ["uv", "run"] + + # Add project if specified + if self.project: + args.extend(["--project", str(self.project)]) + + # Add Python version if specified (only if no project, as project has its own Python) + if self.python and not self.project: + args.extend(["--python", self.python]) + + # Always add dependencies, requirements, and editable packages + # These work with --project to add additional packages on top of the project env + if self.dependencies: + for dep in self.dependencies: + args.extend(["--with", dep]) + + # Add requirements file + if self.requirements: + args.extend(["--with-requirements", str(self.requirements)]) + + # Add editable packages + if self.editable: + for editable_path in self.editable: + args.extend(["--with-editable", str(editable_path)]) + + # Add the command + args.extend(command) + + return args + + def run_with_uv(self, command: list[str]) -> None: + """Execute a command using uv run with this environment configuration. + + Args: + command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"]) + """ + import subprocess + + # Build the full uv command + cmd = self.build_command(command) + + # Set marker to prevent infinite loops when subprocess calls FastMCP again + env = os.environ | {"FASTMCP_UV_SPAWNED": "1"} + + logger.debug(f"Running command: {' '.join(cmd)}") + + try: + # Run without capturing output so it flows through naturally + process = subprocess.run(cmd, check=True, env=env) + sys.exit(process.returncode) + except subprocess.CalledProcessError as e: + logger.error(f"Command failed: {e}") + sys.exit(e.returncode) + + def _needs_setup(self) -> bool: + """Check if this environment config requires uv to set up. + + Returns: + True if any environment settings require uv run + """ + return any( + [ + self.python is not None, + self.dependencies is not None, + self.requirements is not None, + self.project is not None, + self.editable is not None, + ] + ) + + # Backward compatibility aliases + def needs_uv(self) -> bool: + """Deprecated: Use _needs_setup() internally or check if build_command modifies the command.""" + return self._needs_setup() + + def build_uv_run_command(self, command: list[str]) -> list[str]: + """Deprecated: Use build_command() instead.""" + return self.build_command(command) + + async def prepare(self, output_dir: Path | None = None) -> None: + """Prepare the Python environment using uv. + + Args: + output_dir: Directory where the persistent uv project will be created. + If None, creates a temporary directory for ephemeral use. + """ + + # Check if uv is available + if not shutil.which("uv"): + raise RuntimeError( + "uv is not installed. Please install it with: " + "curl -LsSf https://astral.sh/uv/install.sh | sh" + ) + + # Only prepare environment if there are actual settings to apply + if not self._needs_setup(): + logger.debug("No environment settings configured, skipping preparation") + return + + # Handle None case for ephemeral use + if output_dir is None: + import tempfile + + output_dir = Path(tempfile.mkdtemp(prefix="fastmcp-env-")) + logger.info(f"Creating ephemeral environment in {output_dir}") + else: + logger.info(f"Creating persistent environment in {output_dir}") + output_dir = Path(output_dir).resolve() + + # Initialize the project + logger.debug(f"Initializing uv project in {output_dir}") + try: + subprocess.run( + [ + "uv", + "init", + "--project", + str(output_dir), + "--name", + "fastmcp-env", + ], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + # If project already exists, that's fine - continue + if "already initialized" in e.stderr.lower(): + logger.debug( + f"Project already initialized at {output_dir}, continuing..." + ) + else: + logger.error(f"Failed to initialize project: {e.stderr}") + raise RuntimeError(f"Failed to initialize project: {e.stderr}") from e + + # Pin Python version if specified + if self.python: + logger.debug(f"Pinning Python version to {self.python}") + try: + subprocess.run( + [ + "uv", + "python", + "pin", + self.python, + "--project", + str(output_dir), + ], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to pin Python version: {e.stderr}") + raise RuntimeError(f"Failed to pin Python version: {e.stderr}") from e + + # Add dependencies with --no-sync to defer installation + # dependencies ALWAYS include fastmcp; this is compatible with + # specific fastmcp versions that might be in the dependencies list + dependencies = (self.dependencies or []) + ["fastmcp"] + logger.debug(f"Adding dependencies: {', '.join(dependencies)}") + try: + subprocess.run( + [ + "uv", + "add", + *dependencies, + "--no-sync", + "--project", + str(output_dir), + ], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to add dependencies: {e.stderr}") + raise RuntimeError(f"Failed to add dependencies: {e.stderr}") from e + + # Add requirements file if specified + if self.requirements: + logger.debug(f"Adding requirements from {self.requirements}") + # Resolve requirements path relative to current directory + req_path = Path(self.requirements).resolve() + try: + subprocess.run( + [ + "uv", + "add", + "-r", + str(req_path), + "--no-sync", + "--project", + str(output_dir), + ], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to add requirements: {e.stderr}") + raise RuntimeError(f"Failed to add requirements: {e.stderr}") from e + + # Add editable packages if specified + if self.editable: + editable_paths = [str(Path(e).resolve()) for e in self.editable] + logger.debug(f"Adding editable packages: {', '.join(editable_paths)}") + try: + subprocess.run( + [ + "uv", + "add", + "--editable", + *editable_paths, + "--no-sync", + "--project", + str(output_dir), + ], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to add editable packages: {e.stderr}") + raise RuntimeError( + f"Failed to add editable packages: {e.stderr}" + ) from e + + # Final sync to install everything + logger.info("Installing dependencies...") + try: + subprocess.run( + ["uv", "sync", "--project", str(output_dir)], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to sync dependencies: {e.stderr}") + raise RuntimeError(f"Failed to sync dependencies: {e.stderr}") from e + + logger.info(f"Environment prepared successfully in {output_dir}") 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 ca50470e7..690f7bf48 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 @@ -10,14 +10,13 @@ from __future__ import annotations import json import os import re -import shutil -import subprocess from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Literal, TypeAlias, 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.filesystem import FileSystemSource logger = get_logger("cli.config") @@ -27,285 +26,9 @@ 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 = FileSystemSource - - -class Environment(BaseModel): - """Configuration for Python environment setup.""" - - python: str | None = Field( - default=None, - description="Python version constraint", - examples=["3.10", "3.11", "3.12"], - ) - - dependencies: list[str] | None = Field( - default=None, - description="Python packages to install with PEP 508 specifiers", - examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]], - ) - - requirements: str | None = Field( - default=None, - description="Path to requirements.txt file", - examples=["requirements.txt", "../requirements/prod.txt"], - ) - - project: str | None = Field( - default=None, - description="Path to project directory containing pyproject.toml", - examples=[".", "../my-project"], - ) - - editable: list[str] | None = Field( - default=None, - description="Directories to install in editable mode", - examples=[[".", "../my-package"], ["/path/to/package"]], - ) - - def build_uv_run_command(self, command: list[str]) -> list[str]: - """Build complete uv run command with environment args and command to execute. - - Args: - command: Command to execute (e.g., ["fastmcp", "run", "server.py"]) - - Returns: - Complete command ready for subprocess.run, including "uv" prefix - """ - args = ["uv", "run"] - - # Add project if specified - if self.project: - args.extend(["--project", str(self.project)]) - - # Add Python version if specified (only if no project, as project has its own Python) - if self.python and not self.project: - args.extend(["--python", self.python]) - - # Always add dependencies, requirements, and editable packages - # These work with --project to add additional packages on top of the project env - if self.dependencies: - for dep in self.dependencies: - args.extend(["--with", dep]) - - # Add requirements file - if self.requirements: - args.extend(["--with-requirements", str(self.requirements)]) - - # Add editable packages - if self.editable: - for editable_path in self.editable: - args.extend(["--with-editable", str(editable_path)]) - - # Add the command - args.extend(command) - - return args - - def run_with_uv(self, command: list[str]) -> None: - """Execute a command using uv run with this environment configuration. - - Args: - command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"]) - """ - import subprocess - import sys - - # Build the full uv command - cmd = self.build_uv_run_command(command) - - # Set marker to prevent infinite loops when subprocess calls FastMCP again - env = os.environ | {"FASTMCP_UV_SPAWNED": "1"} - - logger.debug(f"Running command: {' '.join(cmd)}") - - try: - # Run without capturing output so it flows through naturally - process = subprocess.run(cmd, check=True, env=env) - sys.exit(process.returncode) - except subprocess.CalledProcessError as e: - logger.error(f"Command failed: {e}") - sys.exit(e.returncode) - - def needs_uv(self) -> bool: - """Check if this environment config requires uv to set up. - - Returns: - True if any environment settings require uv run - """ - return any( - [ - self.python is not None, - self.dependencies is not None, - self.requirements is not None, - self.project is not None, - self.editable is not None, - ] - ) - - async def prepare(self, output_dir: Path | None = None) -> None: - """Prepare the Python environment using uv. - - Args: - output_dir: Directory where the persistent uv project will be created. - If None, creates a temporary directory for ephemeral use. - """ - - # Check if uv is available - if not shutil.which("uv"): - raise RuntimeError( - "uv is not installed. Please install it with: " - "curl -LsSf https://astral.sh/uv/install.sh | sh" - ) - - # Only prepare environment if there are actual settings to apply - if not self.needs_uv(): - logger.debug("No environment settings configured, skipping preparation") - return - - # Handle None case for ephemeral use - if output_dir is None: - import tempfile - - output_dir = Path(tempfile.mkdtemp(prefix="fastmcp-env-")) - logger.info(f"Creating ephemeral environment in {output_dir}") - else: - logger.info(f"Creating persistent environment in {output_dir}") - output_dir = Path(output_dir).resolve() - - # Initialize the project - logger.debug(f"Initializing uv project in {output_dir}") - try: - subprocess.run( - [ - "uv", - "init", - "--project", - str(output_dir), - "--name", - "fastmcp-env", - ], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - # If project already exists, that's fine - continue - if "already initialized" in e.stderr.lower(): - logger.debug( - f"Project already initialized at {output_dir}, continuing..." - ) - else: - logger.error(f"Failed to initialize project: {e.stderr}") - raise RuntimeError(f"Failed to initialize project: {e.stderr}") from e - - # Pin Python version if specified - if self.python: - logger.debug(f"Pinning Python version to {self.python}") - try: - subprocess.run( - [ - "uv", - "python", - "pin", - self.python, - "--project", - str(output_dir), - ], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to pin Python version: {e.stderr}") - raise RuntimeError(f"Failed to pin Python version: {e.stderr}") from e - - # Add dependencies with --no-sync to defer installation - # dependencies ALWAYS include fastmcp; this is compatible with - # specific fastmcp versions that might be in the dependencies list - dependencies = (self.dependencies or []) + ["fastmcp"] - logger.debug(f"Adding dependencies: {', '.join(dependencies)}") - try: - subprocess.run( - [ - "uv", - "add", - *dependencies, - "--no-sync", - "--project", - str(output_dir), - ], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to add dependencies: {e.stderr}") - raise RuntimeError(f"Failed to add dependencies: {e.stderr}") from e - - # Add requirements file if specified - if self.requirements: - logger.debug(f"Adding requirements from {self.requirements}") - # Resolve requirements path relative to current directory - req_path = Path(self.requirements).resolve() - try: - subprocess.run( - [ - "uv", - "add", - "-r", - str(req_path), - "--no-sync", - "--project", - str(output_dir), - ], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to add requirements: {e.stderr}") - raise RuntimeError(f"Failed to add requirements: {e.stderr}") from e - - # Add editable packages if specified - if self.editable: - editable_paths = [str(Path(e).resolve()) for e in self.editable] - logger.debug(f"Adding editable packages: {', '.join(editable_paths)}") - try: - subprocess.run( - [ - "uv", - "add", - "--editable", - *editable_paths, - "--no-sync", - "--project", - str(output_dir), - ], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to add editable packages: {e.stderr}") - raise RuntimeError( - f"Failed to add editable packages: {e.stderr}" - ) from e - - # Final sync to install everything - logger.info("Installing dependencies...") - try: - subprocess.run( - ["uv", "sync", "--project", str(output_dir)], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to sync dependencies: {e.stderr}") - raise RuntimeError(f"Failed to sync dependencies: {e.stderr}") from e - - logger.info(f"Environment prepared successfully in {output_dir}") +SourceType: TypeAlias = FileSystemSource +# Type alias for environment union (will expand with other environments in future) +EnvironmentType: TypeAlias = UVEnvironment class Deployment(BaseModel): @@ -431,8 +154,8 @@ class MCPServerConfig(BaseModel): ) # Environment configuration - environment: Environment = Field( - default_factory=lambda: Environment(), + environment: EnvironmentType = Field( + default_factory=lambda: UVEnvironment(), description="Python environment setup configuration", ) @@ -448,7 +171,7 @@ class MCPServerConfig(BaseModel): @overload def __init__(self, *, source: dict | FileSystemSource, **data) -> None: ... @overload - def __init__(self, *, environment: dict | Environment, **data) -> None: ... + def __init__(self, *, environment: dict | UVEnvironment, **data) -> None: ... @overload def __init__(self, *, deployment: dict | Deployment, **data) -> None: ... def __init__(self, **data) -> None: ... @@ -478,17 +201,17 @@ class MCPServerConfig(BaseModel): @field_validator("environment", mode="before") @classmethod - def validate_environment(cls, v: dict | Environment) -> Environment: + def validate_environment(cls, v: dict | UVEnvironment) -> UVEnvironment: """Validate and convert environment to Environment. Accepts: - Environment instance - dict that can be converted to Environment """ - if isinstance(v, Environment): + if isinstance(v, UVEnvironment): return v elif isinstance(v, dict): - return Environment(**v) # type: ignore[arg-type] + return UVEnvironment(**v) # type: ignore[arg-type] else: raise ValueError("environment must be a dict, Environment instance") @@ -578,7 +301,7 @@ class MCPServerConfig(BaseModel): # Build environment config if any env args provided environment = None if any([python, dependencies, requirements, project, editable]): - environment = Environment( + environment = UVEnvironment( python=python, dependencies=dependencies, requirements=requirements, diff --git a/src/fastmcp/utilities/mcp_server_config/v1/schema.json b/src/fastmcp/utilities/mcp_server_config/v1/schema.json index c28a50452..bc072a67d 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/schema.json +++ b/src/fastmcp/utilities/mcp_server_config/v1/schema.json @@ -162,7 +162,42 @@ "title": "Deployment", "type": "object" }, - "Environment": { + "FileSystemSource": { + "description": "Source for local Python files.", + "properties": { + "type": { + "const": "filesystem", + "default": "filesystem", + "description": "Source type", + "title": "Type", + "type": "string" + }, + "path": { + "description": "Path to Python file containing the server", + "title": "Path", + "type": "string" + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)", + "title": "Entrypoint" + } + }, + "required": [ + "path" + ], + "title": "FileSystemSource", + "type": "object" + }, + "UVEnvironment": { "description": "Configuration for Python environment setup.", "properties": { "python": { @@ -266,42 +301,7 @@ "title": "Editable" } }, - "title": "Environment", - "type": "object" - }, - "FileSystemSource": { - "description": "Source for local Python files.", - "properties": { - "type": { - "const": "filesystem", - "default": "filesystem", - "description": "Source type", - "title": "Type", - "type": "string" - }, - "path": { - "description": "Path to Python file containing the server", - "title": "Path", - "type": "string" - }, - "entrypoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)", - "title": "Entrypoint" - } - }, - "required": [ - "path" - ], - "title": "FileSystemSource", + "title": "UVEnvironment", "type": "object" } }, @@ -339,7 +339,7 @@ ] }, "environment": { - "$ref": "#/$defs/Environment", + "$ref": "#/$defs/UVEnvironment", "description": "Python environment setup configuration" }, "deployment": { diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 6d187f934..4ea56fdf6 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -9,9 +9,9 @@ from pydantic import ValidationError from fastmcp.utilities.mcp_server_config import ( Deployment, - Environment, MCPServerConfig, ) +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource @@ -104,7 +104,7 @@ class TestEnvironment: }, ) - cmd = config.environment.build_uv_run_command(["fastmcp", "run", "server.py"]) + cmd = config.environment.build_command(["fastmcp", "run", "server.py"]) assert cmd[0] == "uv" assert cmd[1] == "run" @@ -263,7 +263,7 @@ class TestMCPServerConfig: 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, Environment) + assert isinstance(config.environment, UVEnvironment) assert isinstance(config.deployment, Deployment) # Check they have no values set assert not config.environment.needs_uv() @@ -289,7 +289,7 @@ class TestMCPServerConfig: assert isinstance(config.source, FileSystemSource) assert config.source.path == "server.py" assert config.source.entrypoint is None - assert isinstance(config.environment, Environment) + assert isinstance(config.environment, UVEnvironment) assert isinstance(config.deployment, Deployment) def test_from_file(self, tmp_path): @@ -416,7 +416,7 @@ class TestMCPServerConfig: 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, Environment) + assert isinstance(config.environment, UVEnvironment) assert isinstance(config.deployment, Deployment) # Only environment with values @@ -434,9 +434,9 @@ class TestMCPServerConfig: config = MCPServerConfig( source={"path": "server.py"}, deployment={"transport": "http"} ) - assert isinstance(config.environment, Environment) + assert isinstance(config.environment, UVEnvironment) assert all( getattr(config.environment, field, None) is None - for field in Environment.model_fields + for field in UVEnvironment.model_fields ) assert config.deployment.transport == "http" diff --git a/tests/cli/test_mcp_server_config_integration.py b/tests/cli/test_mcp_server_config_integration.py index e925f1bb4..1a25d14dd 100644 --- a/tests/cli/test_mcp_server_config_integration.py +++ b/tests/cli/test_mcp_server_config_integration.py @@ -229,7 +229,7 @@ class TestPathResolution: # Build UV command assert config.environment is not None - uv_cmd = config.environment.build_uv_run_command(["fastmcp", "run"]) + uv_cmd = config.environment.build_command(["fastmcp", "run"]) # Should include requirements file assert "--with-requirements" in uv_cmd diff --git a/tests/cli/test_project_prepare.py b/tests/cli/test_project_prepare.py index 5a2ad15b1..88e3fe5c2 100644 --- a/tests/cli/test_project_prepare.py +++ b/tests/cli/test_project_prepare.py @@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from fastmcp.utilities.mcp_server_config import Environment, MCPServerConfig +from fastmcp.utilities.mcp_server_config import MCPServerConfig +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource @@ -25,7 +26,7 @@ class TestMCPServerConfigPrepare: """Test that prepare() calls both prepare_environment and prepare_source.""" config = MCPServerConfig( source=FileSystemSource(path="server.py"), - environment=Environment(python="3.10"), + environment=UVEnvironment(python="3.10"), ) await config.prepare() @@ -45,7 +46,7 @@ class TestMCPServerConfigPrepare: """Test that prepare() with output_dir calls prepare_environment with it.""" config = MCPServerConfig( source=FileSystemSource(path="server.py"), - environment=Environment(python="3.10"), + environment=UVEnvironment(python="3.10"), ) output_path = Path("/tmp/test-env") @@ -66,7 +67,7 @@ class TestMCPServerConfigPrepare: """Test that prepare() skips source when skip_source=True.""" config = MCPServerConfig( source=FileSystemSource(path="server.py"), - environment=Environment(python="3.10"), + environment=UVEnvironment(python="3.10"), ) await config.prepare(skip_source=True) @@ -79,7 +80,7 @@ class TestMCPServerConfigPrepare: new_callable=AsyncMock, ) @patch( - "fastmcp.utilities.mcp_server_config.v1.mcp_server_config.Environment.prepare", + "fastmcp.utilities.mcp_server_config.v1.environments.uv.UVEnvironment.prepare", new_callable=AsyncMock, ) async def test_prepare_no_environment_settings(self, mock_env_prepare, mock_src): @@ -104,7 +105,7 @@ class TestEnvironmentPrepare: """Test that prepare() raises error when uv is not installed.""" mock_which.return_value = None - env = Environment(python="3.10") + env = UVEnvironment(python="3.10") with pytest.raises(RuntimeError, match="uv is not installed"): await env.prepare(tmp_path / "test-env") @@ -115,7 +116,7 @@ class TestEnvironmentPrepare: """Test that prepare() does nothing when no settings are configured.""" mock_which.return_value = "/usr/bin/uv" - env = Environment() # No settings + env = UVEnvironment() # No settings await env.prepare(tmp_path / "test-env") @@ -131,7 +132,7 @@ class TestEnvironmentPrepare: returncode=0, stdout="Environment cached", stderr="" ) - env = Environment(python="3.10") + env = UVEnvironment(python="3.10") await env.prepare(tmp_path / "test-env") @@ -150,7 +151,7 @@ class TestEnvironmentPrepare: mock_which.return_value = "/usr/bin/uv" mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") - env = Environment(dependencies=["numpy", "pandas"]) + env = UVEnvironment(dependencies=["numpy", "pandas"]) await env.prepare(tmp_path / "test-env") @@ -179,7 +180,7 @@ class TestEnvironmentPrepare: 1, ["uv"], stderr="Package not found" ) - env = Environment(python="3.10") + env = UVEnvironment(python="3.10") with pytest.raises(RuntimeError, match="Failed to initialize project"): await env.prepare(tmp_path / "test-env") diff --git a/tests/cli/test_run_config.py b/tests/cli/test_run_config.py index abc1f10af..b8114aa07 100644 --- a/tests/cli/test_run_config.py +++ b/tests/cli/test_run_config.py @@ -9,9 +9,9 @@ import pytest from fastmcp.cli.run import load_mcp_server_config from fastmcp.utilities.mcp_server_config import ( Deployment, - Environment, MCPServerConfig, ) +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource @@ -56,7 +56,7 @@ def test_load_mcp_server_config(sample_config, monkeypatch): assert isinstance(config, MCPServerConfig) assert isinstance(config.source, FileSystemSource) assert isinstance(config.deployment, Deployment) - assert isinstance(config.environment, Environment) + assert isinstance(config.environment, UVEnvironment) # Check source - path is not resolved yet, only during load_server assert config.source.path == "server.py" @@ -262,7 +262,7 @@ def test_environment_config_path_resolution(tmp_path): config = load_mcp_server_config(config_file) # Check that UV command is built with resolved paths - uv_cmd = config.environment.build_uv_run_command(["fastmcp", "run", "server.py"]) + uv_cmd = config.environment.build_command(["fastmcp", "run", "server.py"]) assert "--with-requirements" in uv_cmd assert "--project" in uv_cmd diff --git a/tests/cli/test_run_with_uv.py b/tests/cli/test_run_with_uv.py index 446b21787..3bc302269 100644 --- a/tests/cli/test_run_with_uv.py +++ b/tests/cli/test_run_with_uv.py @@ -27,9 +27,8 @@ class TestRunWithUv: cmd = mock_run.call_args[0][0] env = mock_run.call_args.kwargs.get("env", {}) + # With no environment config, the command should be returned unchanged expected = [ - "uv", - "run", "fastmcp", "run", "server.py", @@ -150,9 +149,8 @@ class TestRunWithUv: assert exc_info.value.code == 0 cmd = mock_run.call_args[0][0] + # With no environment config, no uv run prefix expected = [ - "uv", - "run", "fastmcp", "run", "server.py", diff --git a/tests/utilities/test_cli.py b/tests/utilities/test_cli.py index 56d5a5255..afe17deda 100644 --- a/tests/utilities/test_cli.py +++ b/tests/utilities/test_cli.py @@ -1,6 +1,6 @@ """Tests for CLI utility functions.""" -from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import Environment +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment class TestEnvironmentBuildUVRunCommand: @@ -8,16 +8,17 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_basic(self): """Test building basic uv command with no environment config.""" - env = Environment() - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) - expected = ["uv", "run", "fastmcp", "run", "server.py"] + env = UVEnvironment() + cmd = env.build_command(["fastmcp", "run", "server.py"]) + # With no config, the command should be returned unchanged + expected = ["fastmcp", "run", "server.py"] assert cmd == expected def test_build_uv_run_command_with_editable(self): """Test building uv command with editable package.""" editable_path = "/path/to/package" - env = Environment(editable=[editable_path]) - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + env = UVEnvironment(editable=[editable_path]) + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -31,8 +32,8 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_packages(self): """Test building uv command with additional packages.""" - env = Environment(dependencies=["pkg1", "pkg2"]) - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + env = UVEnvironment(dependencies=["pkg1", "pkg2"]) + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -48,8 +49,8 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_python_version(self): """Test building uv command with Python version.""" - env = Environment(python="3.10") - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + env = UVEnvironment(python="3.10") + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -64,8 +65,8 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_requirements(self): """Test building uv command with requirements file.""" requirements_path = "/path/to/requirements.txt" - env = Environment(requirements=requirements_path) - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + env = UVEnvironment(requirements=requirements_path) + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -80,8 +81,8 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_with_project(self): """Test building uv command with project directory.""" project_path = "/path/to/project" - env = Environment(project=project_path) - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + env = UVEnvironment(project=project_path) + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -97,13 +98,13 @@ class TestEnvironmentBuildUVRunCommand: """Test building uv command with all options.""" requirements_path = "/path/to/requirements.txt" editable_path = "/local/pkg" - env = Environment( + env = UVEnvironment( python="3.10", dependencies=["pandas", "numpy"], requirements=requirements_path, editable=[editable_path], ) - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -129,13 +130,13 @@ class TestEnvironmentBuildUVRunCommand: def test_build_uv_run_command_project_with_extras(self): """Test that project flag works with additional dependencies.""" project_path = "/path/to/project" - env = Environment( + env = UVEnvironment( project=project_path, python="3.10", # Should be ignored with project dependencies=["pandas"], # Should be added on top of project editable=["/pkg"], # Should be added on top of project ) - cmd = env.build_uv_run_command(["fastmcp", "run", "server.py"]) + cmd = env.build_command(["fastmcp", "run", "server.py"]) expected = [ "uv", "run", @@ -157,35 +158,35 @@ class TestEnvironmentNeedsUV: def test_needs_uv_with_python(self): """Test that needs_uv returns True with Python version.""" - env = Environment(python="3.10") + env = UVEnvironment(python="3.10") assert env.needs_uv() is True def test_needs_uv_with_dependencies(self): """Test that needs_uv returns True with dependencies.""" - env = Environment(dependencies=["pandas"]) + env = UVEnvironment(dependencies=["pandas"]) assert env.needs_uv() is True def test_needs_uv_with_requirements(self): """Test that needs_uv returns True with requirements.""" - env = Environment(requirements="/path/to/requirements.txt") + env = UVEnvironment(requirements="/path/to/requirements.txt") assert env.needs_uv() is True def test_needs_uv_with_project(self): """Test that needs_uv returns True with project.""" - env = Environment(project="/path/to/project") + env = UVEnvironment(project="/path/to/project") assert env.needs_uv() is True def test_needs_uv_with_editable(self): """Test that needs_uv returns True with editable.""" - env = Environment(editable=["/pkg"]) + env = UVEnvironment(editable=["/pkg"]) assert env.needs_uv() is True def test_needs_uv_empty(self): """Test that needs_uv returns False with empty config.""" - env = Environment() + env = UVEnvironment() assert env.needs_uv() is False def test_needs_uv_with_empty_lists(self): """Test that needs_uv returns False with empty lists.""" - env = Environment(dependencies=None, editable=None) + env = UVEnvironment(dependencies=None, editable=None) assert env.needs_uv() is False From c71c7ef8e3c0e0c216be6a0b887105407c85ad5f Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 22:35:08 -0400 Subject: [PATCH 04/12] chore: Update fastmcp.json schema (#1674) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/public/schemas/fastmcp.json/latest.json | 76 ++++++++++---------- docs/public/schemas/fastmcp.json/v1.json | 76 ++++++++++---------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/docs/public/schemas/fastmcp.json/latest.json b/docs/public/schemas/fastmcp.json/latest.json index c28a50452..bc072a67d 100644 --- a/docs/public/schemas/fastmcp.json/latest.json +++ b/docs/public/schemas/fastmcp.json/latest.json @@ -162,7 +162,42 @@ "title": "Deployment", "type": "object" }, - "Environment": { + "FileSystemSource": { + "description": "Source for local Python files.", + "properties": { + "type": { + "const": "filesystem", + "default": "filesystem", + "description": "Source type", + "title": "Type", + "type": "string" + }, + "path": { + "description": "Path to Python file containing the server", + "title": "Path", + "type": "string" + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)", + "title": "Entrypoint" + } + }, + "required": [ + "path" + ], + "title": "FileSystemSource", + "type": "object" + }, + "UVEnvironment": { "description": "Configuration for Python environment setup.", "properties": { "python": { @@ -266,42 +301,7 @@ "title": "Editable" } }, - "title": "Environment", - "type": "object" - }, - "FileSystemSource": { - "description": "Source for local Python files.", - "properties": { - "type": { - "const": "filesystem", - "default": "filesystem", - "description": "Source type", - "title": "Type", - "type": "string" - }, - "path": { - "description": "Path to Python file containing the server", - "title": "Path", - "type": "string" - }, - "entrypoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)", - "title": "Entrypoint" - } - }, - "required": [ - "path" - ], - "title": "FileSystemSource", + "title": "UVEnvironment", "type": "object" } }, @@ -339,7 +339,7 @@ ] }, "environment": { - "$ref": "#/$defs/Environment", + "$ref": "#/$defs/UVEnvironment", "description": "Python environment setup configuration" }, "deployment": { diff --git a/docs/public/schemas/fastmcp.json/v1.json b/docs/public/schemas/fastmcp.json/v1.json index c28a50452..bc072a67d 100644 --- a/docs/public/schemas/fastmcp.json/v1.json +++ b/docs/public/schemas/fastmcp.json/v1.json @@ -162,7 +162,42 @@ "title": "Deployment", "type": "object" }, - "Environment": { + "FileSystemSource": { + "description": "Source for local Python files.", + "properties": { + "type": { + "const": "filesystem", + "default": "filesystem", + "description": "Source type", + "title": "Type", + "type": "string" + }, + "path": { + "description": "Path to Python file containing the server", + "title": "Path", + "type": "string" + }, + "entrypoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)", + "title": "Entrypoint" + } + }, + "required": [ + "path" + ], + "title": "FileSystemSource", + "type": "object" + }, + "UVEnvironment": { "description": "Configuration for Python environment setup.", "properties": { "python": { @@ -266,42 +301,7 @@ "title": "Editable" } }, - "title": "Environment", - "type": "object" - }, - "FileSystemSource": { - "description": "Source for local Python files.", - "properties": { - "type": { - "const": "filesystem", - "default": "filesystem", - "description": "Source type", - "title": "Type", - "type": "string" - }, - "path": { - "description": "Path to Python file containing the server", - "title": "Path", - "type": "string" - }, - "entrypoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)", - "title": "Entrypoint" - } - }, - "required": [ - "path" - ], - "title": "FileSystemSource", + "title": "UVEnvironment", "type": "object" } }, @@ -339,7 +339,7 @@ ] }, "environment": { - "$ref": "#/$defs/Environment", + "$ref": "#/$defs/UVEnvironment", "description": "Python environment setup configuration" }, "deployment": { From bc84961ab97c9e2d2515d16a72cb89f7de87b2ac Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 22:35:28 -0400 Subject: [PATCH 05/12] chore: Update SDK documentation (#1675) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/docs.json | 8 ++ docs/python-sdk/fastmcp-cli-cli.mdx | 12 +-- ...server_config-v1-environments-__init__.mdx | 9 ++ ...mcp_server_config-v1-environments-base.mdx | 43 +++++++++ ...s-mcp_server_config-v1-environments-uv.mdx | 75 ++++++++++++++++ ...mcp_server_config-v1-mcp_server_config.mdx | 90 ++++--------------- 6 files changed, 156 insertions(+), 81 deletions(-) create mode 100644 docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx create mode 100644 docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx diff --git a/docs/docs.json b/docs/docs.json index 3cd031270..e2bc06e02 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -394,6 +394,14 @@ "group": "v1", "pages": [ "python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__", + { + "group": "environments", + "pages": [ + "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__", + "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base", + "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv" + ] + }, "python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config", { "group": "sources", diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx index 83677c671..4dddc51ad 100644 --- a/docs/python-sdk/fastmcp-cli-cli.mdx +++ b/docs/python-sdk/fastmcp-cli-cli.mdx @@ -10,7 +10,7 @@ FastMCP CLI tools using Cyclopts. ## Functions -### `with_argv` +### `with_argv` ```python with_argv(args: list[str] | None) @@ -27,7 +27,7 @@ Args are provided without the script name, so we preserve sys.argv[0] and replace the rest. -### `version` +### `version` ```python version() @@ -37,7 +37,7 @@ version() Display version information and platform details. -### `dev` +### `dev` ```python dev(server_spec: str | None = None) -> None @@ -50,7 +50,7 @@ Run an MCP server with the MCP Inspector for development. - `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json -### `run` +### `run` ```python run(server_spec: str | None = None, *server_args: str) -> None @@ -74,7 +74,7 @@ fastmcp run server.py -- --config config.json --debug - `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect -### `inspect` +### `inspect` ```python inspect(server_spec: str | None = None) -> None @@ -105,7 +105,7 @@ fastmcp inspect # auto-detect fastmcp.json - `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json -### `prepare` +### `prepare` ```python prepare(config_path: Annotated[str | None, cyclopts.Parameter(help='Path to fastmcp.json configuration file')] = None, output_dir: Annotated[str | None, cyclopts.Parameter(help='Directory to create the persistent environment in')] = None, skip_source: Annotated[bool, cyclopts.Parameter(help='Skip source preparation (e.g., git clone)')] = False) -> None diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__.mdx new file mode 100644 index 000000000..01f147482 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__.mdx @@ -0,0 +1,9 @@ +--- +title: __init__ +sidebarTitle: __init__ +--- + +# `fastmcp.utilities.mcp_server_config.v1.environments` + + +Environment configuration for MCP servers. diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx new file mode 100644 index 000000000..52b0de9a8 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx @@ -0,0 +1,43 @@ +--- +title: base +sidebarTitle: base +--- + +# `fastmcp.utilities.mcp_server_config.v1.environments.base` + +## Classes + +### `Environment` + + +Base class for environment configuration. + + +**Methods:** + +#### `build_command` + +```python +build_command(self, command: list[str]) -> list[str] +``` + +Build the full command with environment setup. + +**Args:** +- `command`: Base command to wrap with environment setup + +**Returns:** +- Full command ready for subprocess execution + + +#### `prepare` + +```python +prepare(self, output_dir: Path | None = None) -> None +``` + +Prepare the environment (optional, can be no-op). + +**Args:** +- `output_dir`: Directory for persistent environment setup + diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx new file mode 100644 index 000000000..84d117078 --- /dev/null +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx @@ -0,0 +1,75 @@ +--- +title: uv +sidebarTitle: uv +--- + +# `fastmcp.utilities.mcp_server_config.v1.environments.uv` + +## Classes + +### `UVEnvironment` + + +Configuration for Python environment setup. + + +**Methods:** + +#### `build_command` + +```python +build_command(self, command: list[str]) -> list[str] +``` + +Build complete uv run command with environment args and command to execute. + +**Args:** +- `command`: Command to execute (e.g., ["fastmcp", "run", "server.py"]) + +**Returns:** +- Complete command ready for subprocess.run, including "uv" prefix if needed. +- If no environment configuration is set, returns the command unchanged. + + +#### `run_with_uv` + +```python +run_with_uv(self, command: list[str]) -> None +``` + +Execute a command using uv run with this environment configuration. + +**Args:** +- `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"]) + + +#### `needs_uv` + +```python +needs_uv(self) -> bool +``` + +Deprecated: Use _needs_setup() internally or check if build_command modifies the command. + + +#### `build_uv_run_command` + +```python +build_uv_run_command(self, command: list[str]) -> list[str] +``` + +Deprecated: Use build_command() instead. + + +#### `prepare` + +```python +prepare(self, output_dir: Path | None = None) -> None +``` + +Prepare the Python environment using uv. + +**Args:** +- `output_dir`: Directory where the persistent uv project will be created. + If None, creates a temporary directory for ephemeral use. + diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx index fe7815a9b..7096185a0 100644 --- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx @@ -15,7 +15,7 @@ command-line arguments. ## Functions -### `generate_schema` +### `generate_schema` ```python generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None @@ -38,67 +38,7 @@ validation and auto-completion. ## Classes -### `Environment` - - -Configuration for Python environment setup. - - -**Methods:** - -#### `build_uv_run_command` - -```python -build_uv_run_command(self, command: list[str]) -> list[str] -``` - -Build complete uv run command with environment args and command to execute. - -**Args:** -- `command`: Command to execute (e.g., ["fastmcp", "run", "server.py"]) - -**Returns:** -- Complete command ready for subprocess.run, including "uv" prefix - - -#### `run_with_uv` - -```python -run_with_uv(self, command: list[str]) -> None -``` - -Execute a command using uv run with this environment configuration. - -**Args:** -- `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"]) - - -#### `needs_uv` - -```python -needs_uv(self) -> bool -``` - -Check if this environment config requires uv to set up. - -**Returns:** -- True if any environment settings require uv run - - -#### `prepare` - -```python -prepare(self, output_dir: Path | None = None) -> None -``` - -Prepare the Python environment using uv. - -**Args:** -- `output_dir`: Directory where the persistent uv project will be created. - If None, creates a temporary directory for ephemeral use. - - -### `Deployment` +### `Deployment` Configuration for server deployment and runtime settings. @@ -106,7 +46,7 @@ Configuration for server deployment and runtime settings. **Methods:** -#### `apply_runtime_settings` +#### `apply_runtime_settings` ```python apply_runtime_settings(self, config_path: Path | None = None) -> None @@ -122,7 +62,7 @@ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com" will substitute the value of the ENVIRONMENT variable at runtime. -### `MCPServerConfig` +### `MCPServerConfig` Configuration for a FastMCP server. @@ -133,7 +73,7 @@ a FastMCP server in a declarative format. **Methods:** -#### `validate_source` +#### `validate_source` ```python validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource @@ -149,10 +89,10 @@ No string parsing happens here - that's only at CLI boundaries. MCPServerConfig works only with properly typed objects. -#### `validate_environment` +#### `validate_environment` ```python -validate_environment(cls, v: dict | Environment) -> Environment +validate_environment(cls, v: dict | UVEnvironment) -> UVEnvironment ``` Validate and convert environment to Environment. @@ -162,7 +102,7 @@ Accepts: - dict that can be converted to Environment -#### `validate_deployment` +#### `validate_deployment` ```python validate_deployment(cls, v: dict | Deployment) -> Deployment @@ -175,7 +115,7 @@ Accepts: - dict that can be converted to Deployment -#### `from_file` +#### `from_file` ```python from_file(cls, file_path: Path) -> MCPServerConfig @@ -195,7 +135,7 @@ Load configuration from a JSON file. - `pydantic.ValidationError`: If the configuration is invalid -#### `from_cli_args` +#### `from_cli_args` ```python from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> MCPServerConfig @@ -226,7 +166,7 @@ goes through a config object. - MCPServerConfig instance -#### `find_config` +#### `find_config` ```python find_config(cls, start_path: Path | None = None) -> Path | None @@ -241,7 +181,7 @@ Find a fastmcp.json file in the specified directory. - Path to the configuration file, or None if not found -#### `prepare` +#### `prepare` ```python prepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None @@ -257,7 +197,7 @@ When output_dir is None, does ephemeral caching (for backwards compatibility). - `output_dir`: Directory to create the persistent uv project in (optional) -#### `prepare_environment` +#### `prepare_environment` ```python prepare_environment(self, output_dir: Path | None = None) -> None @@ -272,7 +212,7 @@ Prepare the Python environment. Delegates to the environment's prepare() method -#### `prepare_source` +#### `prepare_source` ```python prepare_source(self) -> None @@ -283,7 +223,7 @@ Prepare the source for loading. Delegates to the source's prepare() method. -#### `run_server` +#### `run_server` ```python run_server(self, **kwargs: Any) -> None From 183275c8c3453fc10c2840cce5dd82e288cbe863 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 30 Aug 2025 07:47:24 -0400 Subject: [PATCH 06/12] Add type field to Environment base class (#1676) --- docs/deployment/server-configuration.mdx | 50 ++++++++++++++---- .../utilities/mcp_server_config/__init__.py | 4 +- .../mcp_server_config/v1/environments/base.py | 4 +- .../mcp_server_config/v1/environments/uv.py | 3 ++ .../mcp_server_config/v1/mcp_server_config.py | 41 +++++---------- .../mcp_server_config/v1/sources/base.py | 2 +- .../v1/sources/filesystem.py | 7 +-- tests/cli/test_config.py | 2 + tests/cli/test_mcp_server_config_schema.py | 51 ++++++------------- 9 files changed, 86 insertions(+), 78 deletions(-) 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 From 8c678b552be5d11183378ba4ecd0a7aa1beeddfd Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 30 Aug 2025 08:33:13 -0400 Subject: [PATCH 07/12] Add resource_server_url parameter to OAuth proxy providers (#1682) --- docs/servers/auth/oauth-proxy.mdx | 7 +++- docs/servers/auth/remote-oauth.mdx | 6 +-- examples/auth/azure_oauth/server.py | 1 + examples/auth/github_oauth/server.py | 1 + examples/auth/google_oauth/server.py | 1 + examples/auth/workos_oauth/server.py | 1 + src/fastmcp/server/auth/oauth_proxy.py | 43 +++++---------------- src/fastmcp/server/auth/providers/azure.py | 14 ++++++- src/fastmcp/server/auth/providers/github.py | 16 ++++++-- src/fastmcp/server/auth/providers/google.py | 14 +++++-- src/fastmcp/server/auth/providers/workos.py | 14 ++++++- 11 files changed, 69 insertions(+), 49 deletions(-) diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index 5f3a1c1ad..fadf9b03c 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -159,7 +159,7 @@ The `OAuthProxy` class provides the complete proxy implementation: - Resource server URL (defaults to base_url) + Path of the FastMCP server (defaults to base_url). **Important**: This should point to your MCP endpoint path. For example, if your MCP server is accessible at `{base_url}/mcp`, specify `https://your-server.com/mcp` here for proper RFC 8707 compliance. @@ -228,7 +228,10 @@ auth = OAuthProxy( base_url="https://your-server.com", # Optional: customize callback path (defaults to "/auth/callback") - redirect_path="/auth/callback" + redirect_path="/auth/callback", + + # Optional: specify MCP endpoint path if different from base_url + # resource_server_url="https://your-server.com/mcp" ) mcp = FastMCP(name="My Server", auth=auth) diff --git a/docs/servers/auth/remote-oauth.mdx b/docs/servers/auth/remote-oauth.mdx index 5f6a485cd..6ffe876e5 100644 --- a/docs/servers/auth/remote-oauth.mdx +++ b/docs/servers/auth/remote-oauth.mdx @@ -111,7 +111,7 @@ token_verifier = JWTVerifier( auth = RemoteAuthProvider( token_verifier=token_verifier, authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")], - resource_server_url="https://api.yourcompany.com", + resource_server_url="https://api.yourcompany.com/mcp", # Point to your MCP endpoint # Optional: customize allowed client redirect URIs (defaults to localhost only) allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"] ) @@ -121,7 +121,7 @@ mcp = FastMCP(name="Company API", auth=auth) This configuration creates a server that accepts tokens issued by `auth.yourcompany.com` and provides the OAuth discovery metadata that MCP clients need. The `JWTVerifier` handles token validation using your identity provider's public keys, while the `RemoteAuthProvider` generates the required OAuth endpoints. -The `authorization_servers` list tells MCP clients which identity providers you trust. The `resource_server_url` identifies your server in OAuth metadata, enabling proper token audience validation. +The `authorization_servers` list tells MCP clients which identity providers you trust. The `resource_server_url` identifies your server in OAuth metadata, enabling proper token audience validation. **Important**: The `resource_server_url` should point to your actual MCP endpoint - for example, if your MCP server is accessible at `https://api.yourcompany.com/mcp`, use that full path rather than just the base URL. ### Custom Endpoints @@ -143,7 +143,7 @@ class CompanyAuthProvider(RemoteAuthProvider): super().__init__( token_verifier=token_verifier, authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")], - resource_server_url="https://api.yourcompany.com" + resource_server_url="https://api.yourcompany.com/mcp" # Your MCP endpoint path ) def get_routes(self) -> list[Route]: diff --git a/examples/auth/azure_oauth/server.py b/examples/auth/azure_oauth/server.py index 2d5062612..81687cfd4 100644 --- a/examples/auth/azure_oauth/server.py +++ b/examples/auth/azure_oauth/server.py @@ -23,6 +23,7 @@ auth = AzureProvider( tenant_id=os.getenv("AZURE_TENANT_ID") or "", # Required for single-tenant apps - get from Azure Portal base_url="http://localhost:8000", + resource_server_url="http://localhost:8000/mcp", # redirect_path="/auth/callback", # Default path - change if using a different callback URL ) diff --git a/examples/auth/github_oauth/server.py b/examples/auth/github_oauth/server.py index 1f88c6977..a0fdc0504 100644 --- a/examples/auth/github_oauth/server.py +++ b/examples/auth/github_oauth/server.py @@ -19,6 +19,7 @@ auth = GitHubProvider( client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "", client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "", base_url="http://localhost:8000", + resource_server_url="http://localhost:8000/mcp", # redirect_path="/auth/callback", # Default path - change if using a different callback URL ) diff --git a/examples/auth/google_oauth/server.py b/examples/auth/google_oauth/server.py index 2a5b1c7df..feb1fe1d4 100644 --- a/examples/auth/google_oauth/server.py +++ b/examples/auth/google_oauth/server.py @@ -19,6 +19,7 @@ auth = GoogleProvider( client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "", client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "", base_url="http://localhost:8000", + resource_server_url="http://localhost:8000/mcp", # redirect_path="/auth/callback", # Default path - change if using a different callback URL # Optional: specify required scopes # required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"], diff --git a/examples/auth/workos_oauth/server.py b/examples/auth/workos_oauth/server.py index 08c1db62b..8db24f13b 100644 --- a/examples/auth/workos_oauth/server.py +++ b/examples/auth/workos_oauth/server.py @@ -21,6 +21,7 @@ auth = WorkOSProvider( client_secret=os.getenv("WORKOS_CLIENT_SECRET") or "", authkit_domain=os.getenv("WORKOS_AUTHKIT_DOMAIN") or "https://your-app.authkit.app", base_url="http://localhost:8000", + resource_server_url="http://localhost:8000/mcp", # redirect_path="/auth/callback", # Default path - change if using a different callback URL ) diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py index 8f7418a83..14a6e9fd0 100644 --- a/src/fastmcp/server/auth/oauth_proxy.py +++ b/src/fastmcp/server/auth/oauth_proxy.py @@ -254,45 +254,20 @@ class OAuthProxy(OAuthProvider): upstream_client_secret: Client secret for upstream server upstream_revocation_endpoint: Optional upstream revocation endpoint token_verifier: Token verifier for validating access tokens - base_url: Public URL of this FastMCP server + base_url: Public URL of the server that exposes this FastMCP server; redirect path is + relative to this URL redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback") issuer_url: Issuer URL for OAuth metadata (defaults to base_url) service_documentation_url: Optional service documentation URL - resource_server_url: Resource server URL (defaults to base_url) + resource_server_url: Path of the FastMCP server. If None, FastMCP will + attempt to overwrite this with the correct path to the server + e.g. {base_url}/mcp allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*"). If None (default), only localhost redirect URIs are allowed. If empty list, all redirect URIs are allowed (not recommended for production). These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app. """ - # Convert string URLs to AnyHttpUrl for parent class - base_url_parsed = ( - AnyHttpUrl(base_url) if isinstance(base_url, str) else base_url - ) - issuer_url_parsed = ( - (AnyHttpUrl(issuer_url) if isinstance(issuer_url, str) else issuer_url) - if issuer_url - else None - ) - service_documentation_url_parsed = ( - ( - AnyHttpUrl(service_documentation_url) - if isinstance(service_documentation_url, str) - else service_documentation_url - ) - if service_documentation_url - else None - ) - resource_server_url_parsed = ( - ( - AnyHttpUrl(resource_server_url) - if isinstance(resource_server_url, str) - else resource_server_url - ) - if resource_server_url - else None - ) - # Always enable DCR since we implement it locally for MCP clients client_registration_options = ClientRegistrationOptions(enabled=True) @@ -302,13 +277,13 @@ class OAuthProxy(OAuthProvider): ) super().__init__( - base_url=base_url_parsed, - issuer_url=issuer_url_parsed, - service_documentation_url=service_documentation_url_parsed, + base_url=base_url, + issuer_url=issuer_url, + service_documentation_url=service_documentation_url, client_registration_options=client_registration_options, revocation_options=revocation_options, required_scopes=token_verifier.required_scopes, - resource_server_url=resource_server_url_parsed, + resource_server_url=resource_server_url, ) # Store upstream configuration diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py index 362f35f58..806dddf18 100644 --- a/src/fastmcp/server/auth/providers/azure.py +++ b/src/fastmcp/server/auth/providers/azure.py @@ -36,6 +36,8 @@ class AzureProviderSettings(BaseSettings): redirect_path: str | None = None required_scopes: list[str] | None = None timeout_seconds: int | None = None + resource_server_url: str | None = None + allowed_client_redirect_uris: list[str] | None = None @field_validator("required_scopes", mode="before") @classmethod @@ -160,7 +162,8 @@ class AzureProvider(OAuthProxy): redirect_path: str | NotSetT = NotSet, required_scopes: list[str] | None | NotSetT = NotSet, timeout_seconds: int | NotSetT = NotSet, - allowed_client_redirect_uris: list[str] | None = None, + resource_server_url: str | NotSetT = NotSet, + allowed_client_redirect_uris: list[str] | NotSetT = NotSet, ): """Initialize Azure OAuth provider. @@ -172,6 +175,8 @@ class AzureProvider(OAuthProxy): redirect_path: Redirect path configured in Azure (defaults to "/auth/callback") required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"]) timeout_seconds: HTTP request timeout for Azure API calls + resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at + a different path like {base_url}/mcp, specify it here for RFC 8707 compliance. allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. If None (default), all URIs are allowed. If empty list, no URIs are allowed. """ @@ -186,6 +191,8 @@ class AzureProvider(OAuthProxy): "redirect_path": redirect_path, "required_scopes": required_scopes, "timeout_seconds": timeout_seconds, + "resource_server_url": resource_server_url, + "allowed_client_redirect_uris": allowed_client_redirect_uris, }.items() if v is not NotSet } @@ -220,6 +227,8 @@ class AzureProvider(OAuthProxy): "openid", "profile", ] + resource_server_url_final = settings.resource_server_url or base_url_final + allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris # Extract secret string from SecretStr client_secret_str = ( @@ -250,7 +259,8 @@ class AzureProvider(OAuthProxy): base_url=base_url_final, redirect_path=redirect_path_final, issuer_url=base_url_final, - allowed_client_redirect_uris=allowed_client_redirect_uris, + allowed_client_redirect_uris=allowed_client_redirect_uris_final, + resource_server_url=resource_server_url_final, ) logger.info( diff --git a/src/fastmcp/server/auth/providers/github.py b/src/fastmcp/server/auth/providers/github.py index 20dc23616..49613897a 100644 --- a/src/fastmcp/server/auth/providers/github.py +++ b/src/fastmcp/server/auth/providers/github.py @@ -51,6 +51,8 @@ class GitHubProviderSettings(BaseSettings): redirect_path: str | None = None required_scopes: list[str] | None = None timeout_seconds: int | None = None + resource_server_url: AnyHttpUrl | str | None = None + allowed_client_redirect_uris: list[str] | None = None @field_validator("required_scopes", mode="before") @classmethod @@ -199,9 +201,10 @@ class GitHubProvider(OAuthProxy): client_secret: str | NotSetT = NotSet, base_url: AnyHttpUrl | str | NotSetT = NotSet, redirect_path: str | NotSetT = NotSet, - required_scopes: list[str] | None | NotSetT = NotSet, + required_scopes: list[str] | NotSetT = NotSet, timeout_seconds: int | NotSetT = NotSet, - allowed_client_redirect_uris: list[str] | None = None, + resource_server_url: AnyHttpUrl | str | NotSetT = NotSet, + allowed_client_redirect_uris: list[str] | NotSetT = NotSet, ): """Initialize GitHub OAuth provider. @@ -212,6 +215,8 @@ class GitHubProvider(OAuthProxy): redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback") required_scopes: Required GitHub scopes (defaults to ["user"]) timeout_seconds: HTTP request timeout for GitHub API calls + resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at + a different path like {base_url}/mcp, specify it here for RFC 8707 compliance. allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. If None (default), all URIs are allowed. If empty list, no URIs are allowed. """ @@ -225,6 +230,8 @@ class GitHubProvider(OAuthProxy): "redirect_path": redirect_path, "required_scopes": required_scopes, "timeout_seconds": timeout_seconds, + "resource_server_url": resource_server_url, + "allowed_client_redirect_uris": allowed_client_redirect_uris, }.items() if v is not NotSet } @@ -245,6 +252,8 @@ class GitHubProvider(OAuthProxy): redirect_path_final = settings.redirect_path or "/auth/callback" timeout_seconds_final = settings.timeout_seconds or 10 required_scopes_final = settings.required_scopes or ["user"] + resource_server_url_final = settings.resource_server_url or base_url_final + allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris # Create GitHub token verifier token_verifier = GitHubTokenVerifier( @@ -267,7 +276,8 @@ class GitHubProvider(OAuthProxy): base_url=base_url_final, redirect_path=redirect_path_final, issuer_url=base_url_final, # We act as the issuer for client registration - allowed_client_redirect_uris=allowed_client_redirect_uris, + allowed_client_redirect_uris=allowed_client_redirect_uris_final, + resource_server_url=resource_server_url_final, ) logger.info( diff --git a/src/fastmcp/server/auth/providers/google.py b/src/fastmcp/server/auth/providers/google.py index f025fe67b..337bf6157 100644 --- a/src/fastmcp/server/auth/providers/google.py +++ b/src/fastmcp/server/auth/providers/google.py @@ -53,6 +53,8 @@ class GoogleProviderSettings(BaseSettings): redirect_path: str | None = None required_scopes: list[str] | None = None timeout_seconds: int | None = None + resource_server_url: AnyHttpUrl | str | None = None + allowed_client_redirect_uris: list[str] | None = None @field_validator("required_scopes", mode="before") @classmethod @@ -215,9 +217,10 @@ class GoogleProvider(OAuthProxy): client_secret: str | NotSetT = NotSet, base_url: AnyHttpUrl | str | NotSetT = NotSet, redirect_path: str | NotSetT = NotSet, - required_scopes: list[str] | None | NotSetT = NotSet, + required_scopes: list[str] | NotSetT = NotSet, timeout_seconds: int | NotSetT = NotSet, - allowed_client_redirect_uris: list[str] | None = None, + resource_server_url: AnyHttpUrl | str | NotSetT = NotSet, + allowed_client_redirect_uris: list[str] | NotSetT = NotSet, ): """Initialize Google OAuth provider. @@ -244,6 +247,8 @@ class GoogleProvider(OAuthProxy): "redirect_path": redirect_path, "required_scopes": required_scopes, "timeout_seconds": timeout_seconds, + "resource_server_url": resource_server_url, + "allowed_client_redirect_uris": allowed_client_redirect_uris, }.items() if v is not NotSet } @@ -265,6 +270,8 @@ class GoogleProvider(OAuthProxy): timeout_seconds_final = settings.timeout_seconds or 10 # Google requires at least one scope - openid is the minimal OIDC scope required_scopes_final = settings.required_scopes or ["openid"] + resource_server_url_final = settings.resource_server_url or base_url_final + allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris # Create Google token verifier token_verifier = GoogleTokenVerifier( @@ -287,7 +294,8 @@ class GoogleProvider(OAuthProxy): base_url=base_url_final, redirect_path=redirect_path_final, issuer_url=base_url_final, # We act as the issuer for client registration - allowed_client_redirect_uris=allowed_client_redirect_uris, + allowed_client_redirect_uris=allowed_client_redirect_uris_final, + resource_server_url=resource_server_url_final, ) logger.info( diff --git a/src/fastmcp/server/auth/providers/workos.py b/src/fastmcp/server/auth/providers/workos.py index a0f939fb6..2c443eb64 100644 --- a/src/fastmcp/server/auth/providers/workos.py +++ b/src/fastmcp/server/auth/providers/workos.py @@ -43,6 +43,8 @@ class WorkOSProviderSettings(BaseSettings): redirect_path: str | None = None required_scopes: list[str] | None = None timeout_seconds: int | None = None + resource_server_url: AnyHttpUrl | str | None = None + allowed_client_redirect_uris: list[str] | None = None @field_validator("required_scopes", mode="before") @classmethod @@ -167,7 +169,8 @@ class WorkOSProvider(OAuthProxy): redirect_path: str | NotSetT = NotSet, required_scopes: list[str] | None | NotSetT = NotSet, timeout_seconds: int | NotSetT = NotSet, - allowed_client_redirect_uris: list[str] | None = None, + resource_server_url: AnyHttpUrl | str | NotSetT = NotSet, + allowed_client_redirect_uris: list[str] | NotSetT = NotSet, ): """Initialize WorkOS OAuth provider. @@ -179,6 +182,8 @@ class WorkOSProvider(OAuthProxy): redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback") required_scopes: Required OAuth scopes (no default) timeout_seconds: HTTP request timeout for WorkOS API calls + resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at + a different path like {base_url}/mcp, specify it here for RFC 8707 compliance. allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. If None (default), all URIs are allowed. If empty list, no URIs are allowed. """ @@ -193,6 +198,8 @@ class WorkOSProvider(OAuthProxy): "redirect_path": redirect_path, "required_scopes": required_scopes, "timeout_seconds": timeout_seconds, + "resource_server_url": resource_server_url, + "allowed_client_redirect_uris": allowed_client_redirect_uris, }.items() if v is not NotSet } @@ -221,6 +228,8 @@ class WorkOSProvider(OAuthProxy): redirect_path_final = settings.redirect_path or "/auth/callback" timeout_seconds_final = settings.timeout_seconds or 10 scopes_final = settings.required_scopes or [] + resource_server_url_final = settings.resource_server_url or base_url_final + allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris # Extract secret string from SecretStr client_secret_str = ( @@ -244,7 +253,8 @@ class WorkOSProvider(OAuthProxy): base_url=base_url_final, redirect_path=redirect_path_final, issuer_url=base_url_final, - allowed_client_redirect_uris=allowed_client_redirect_uris, + allowed_client_redirect_uris=allowed_client_redirect_uris_final, + resource_server_url=resource_server_url_final, ) logger.info( From 97e0987070f1f596a74df6b15589a9649ae3ae69 Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:36:23 -0400 Subject: [PATCH 08/12] chore: Update fastmcp.json schema (#1680) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- docs/public/schemas/fastmcp.json/latest.json | 7 ++++++- docs/public/schemas/fastmcp.json/v1.json | 7 ++++++- src/fastmcp/utilities/mcp_server_config/v1/schema.json | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/public/schemas/fastmcp.json/latest.json b/docs/public/schemas/fastmcp.json/latest.json index bc072a67d..fe2e21ce9 100644 --- a/docs/public/schemas/fastmcp.json/latest.json +++ b/docs/public/schemas/fastmcp.json/latest.json @@ -168,7 +168,6 @@ "type": { "const": "filesystem", "default": "filesystem", - "description": "Source type", "title": "Type", "type": "string" }, @@ -200,6 +199,12 @@ "UVEnvironment": { "description": "Configuration for Python environment setup.", "properties": { + "type": { + "const": "uv", + "default": "uv", + "title": "Type", + "type": "string" + }, "python": { "anyOf": [ { diff --git a/docs/public/schemas/fastmcp.json/v1.json b/docs/public/schemas/fastmcp.json/v1.json index bc072a67d..fe2e21ce9 100644 --- a/docs/public/schemas/fastmcp.json/v1.json +++ b/docs/public/schemas/fastmcp.json/v1.json @@ -168,7 +168,6 @@ "type": { "const": "filesystem", "default": "filesystem", - "description": "Source type", "title": "Type", "type": "string" }, @@ -200,6 +199,12 @@ "UVEnvironment": { "description": "Configuration for Python environment setup.", "properties": { + "type": { + "const": "uv", + "default": "uv", + "title": "Type", + "type": "string" + }, "python": { "anyOf": [ { diff --git a/src/fastmcp/utilities/mcp_server_config/v1/schema.json b/src/fastmcp/utilities/mcp_server_config/v1/schema.json index bc072a67d..fe2e21ce9 100644 --- a/src/fastmcp/utilities/mcp_server_config/v1/schema.json +++ b/src/fastmcp/utilities/mcp_server_config/v1/schema.json @@ -168,7 +168,6 @@ "type": { "const": "filesystem", "default": "filesystem", - "description": "Source type", "title": "Type", "type": "string" }, @@ -200,6 +199,12 @@ "UVEnvironment": { "description": "Configuration for Python environment setup.", "properties": { + "type": { + "const": "uv", + "default": "uv", + "title": "Type", + "type": "string" + }, "python": { "anyOf": [ { From f7d7303dee9154fcea96161abe1da646eaa9366a Mon Sep 17 00:00:00 2001 From: "marvin-context-protocol[bot]" <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:36:31 -0400 Subject: [PATCH 09/12] chore: Update SDK documentation (#1679) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- .../fastmcp-server-auth-oauth_proxy.mdx | 20 +++++----- .../fastmcp-server-auth-providers-azure.mdx | 6 +-- .../fastmcp-server-auth-providers-github.mdx | 6 +-- .../fastmcp-server-auth-providers-google.mdx | 6 +-- .../fastmcp-server-auth-providers-workos.mdx | 12 +++--- ...mcp_server_config-v1-environments-base.mdx | 4 +- ...s-mcp_server_config-v1-environments-uv.mdx | 12 +++--- ...mcp_server_config-v1-mcp_server_config.mdx | 38 +++++++++---------- ...ties-mcp_server_config-v1-sources-base.mdx | 2 +- ...cp_server_config-v1-sources-filesystem.mdx | 4 +- 10 files changed, 54 insertions(+), 56 deletions(-) diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx index 163843467..6d261e0a4 100644 --- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx +++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy.mdx @@ -182,7 +182,7 @@ Handles provider-specific requirements: **Methods:** -#### `get_client` +#### `get_client` ```python get_client(self, client_id: str) -> OAuthClientInformationFull | None @@ -199,7 +199,7 @@ handles the case where a client with cached tokens reconnects on a different port. -#### `register_client` +#### `register_client` ```python register_client(self, client_info: OAuthClientInformationFull) -> None @@ -226,7 +226,7 @@ The flow: 4. When client reconnects with a different port, ProxyDCRClient accepts it -#### `authorize` +#### `authorize` ```python authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str @@ -240,7 +240,7 @@ This implements the DCR-compliant proxy pattern: 3. Redirect to IdP with our fixed callback URL -#### `load_authorization_code` +#### `load_authorization_code` ```python load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None @@ -252,7 +252,7 @@ Look up our client code and return authorization code object with PKCE challenge for validation. -#### `exchange_authorization_code` +#### `exchange_authorization_code` ```python exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken @@ -264,7 +264,7 @@ For the DCR-compliant proxy flow, we return the IdP tokens that were obtained during the IdP callback exchange. PKCE validation is handled by the MCP framework. -#### `load_refresh_token` +#### `load_refresh_token` ```python load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None @@ -273,7 +273,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) Load refresh token from local storage. -#### `exchange_refresh_token` +#### `exchange_refresh_token` ```python exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken @@ -282,7 +282,7 @@ exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: Exchange refresh token for new access token using authlib. -#### `load_access_token` +#### `load_access_token` ```python load_access_token(self, token: str) -> AccessToken | None @@ -294,7 +294,7 @@ Delegates to the JWT verifier which handles signature validation, expiration checking, and claims validation using the upstream JWKS. -#### `revoke_token` +#### `revoke_token` ```python revoke_token(self, token: AccessToken | RefreshToken) -> None @@ -306,7 +306,7 @@ Removes tokens from local storage and attempts to revoke them with the upstream server if a revocation endpoint is configured. -#### `get_routes` +#### `get_routes` ```python get_routes(self) -> list[Route] diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx index ed66062f9..343128eba 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx @@ -20,7 +20,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows. Settings for Azure OAuth provider. -### `AzureTokenVerifier` +### `AzureTokenVerifier` Token verifier for Azure OAuth tokens. @@ -31,7 +31,7 @@ to get user information and validate the token. **Methods:** -#### `verify_token` +#### `verify_token` ```python verify_token(self, token: str) -> AccessToken | None @@ -40,7 +40,7 @@ verify_token(self, token: str) -> AccessToken | None Verify Azure OAuth token by calling Microsoft Graph API. -### `AzureProvider` +### `AzureProvider` Azure (Microsoft Entra) OAuth provider for FastMCP. diff --git a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx index e6f23ab15..91d86fa87 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx @@ -35,7 +35,7 @@ Example: Settings for GitHub OAuth provider. -### `GitHubTokenVerifier` +### `GitHubTokenVerifier` Token verifier for GitHub OAuth tokens. @@ -46,7 +46,7 @@ by calling GitHub's API to check if they're valid and get user info. **Methods:** -#### `verify_token` +#### `verify_token` ```python verify_token(self, token: str) -> AccessToken | None @@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None Verify GitHub OAuth token by calling GitHub API. -### `GitHubProvider` +### `GitHubProvider` Complete GitHub OAuth provider for FastMCP. diff --git a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx index 4f4d4688b..2eaf874cc 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx @@ -35,7 +35,7 @@ Example: Settings for Google OAuth provider. -### `GoogleTokenVerifier` +### `GoogleTokenVerifier` Token verifier for Google OAuth tokens. @@ -46,7 +46,7 @@ by calling Google's tokeninfo API to check if they're valid and get user info. **Methods:** -#### `verify_token` +#### `verify_token` ```python verify_token(self, token: str) -> AccessToken | None @@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None Verify Google OAuth token by calling Google's tokeninfo API. -### `GoogleProvider` +### `GoogleProvider` Complete Google OAuth provider for FastMCP. diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx index 31d0145d2..fdacdf859 100644 --- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx +++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx @@ -24,7 +24,7 @@ Choose based on your WorkOS setup and authentication requirements. Settings for WorkOS OAuth provider. -### `WorkOSTokenVerifier` +### `WorkOSTokenVerifier` Token verifier for WorkOS OAuth tokens. @@ -35,7 +35,7 @@ the /oauth2/userinfo endpoint to check validity and get user info. **Methods:** -#### `verify_token` +#### `verify_token` ```python verify_token(self, token: str) -> AccessToken | None @@ -44,7 +44,7 @@ verify_token(self, token: str) -> AccessToken | None Verify WorkOS OAuth token by calling userinfo endpoint. -### `WorkOSProvider` +### `WorkOSProvider` Complete WorkOS OAuth provider for FastMCP. @@ -65,9 +65,9 @@ Setup Requirements: 4. Note your Client ID and Client Secret -### `AuthKitProviderSettings` +### `AuthKitProviderSettings` -### `AuthKitProvider` +### `AuthKitProvider` AuthKit metadata provider for DCR (Dynamic Client Registration). @@ -93,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification **Methods:** -#### `get_routes` +#### `get_routes` ```python get_routes(self) -> list[Route] diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx index 52b0de9a8..3a97b5d9e 100644 --- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx @@ -15,7 +15,7 @@ Base class for environment configuration. **Methods:** -#### `build_command` +#### `build_command` ```python build_command(self, command: list[str]) -> list[str] @@ -30,7 +30,7 @@ Build the full command with environment setup. - Full command ready for subprocess execution -#### `prepare` +#### `prepare` ```python prepare(self, output_dir: Path | None = None) -> None diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx index 84d117078..de51b8098 100644 --- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx @@ -7,7 +7,7 @@ sidebarTitle: uv ## Classes -### `UVEnvironment` +### `UVEnvironment` Configuration for Python environment setup. @@ -15,7 +15,7 @@ Configuration for Python environment setup. **Methods:** -#### `build_command` +#### `build_command` ```python build_command(self, command: list[str]) -> list[str] @@ -31,7 +31,7 @@ Build complete uv run command with environment args and command to execute. - If no environment configuration is set, returns the command unchanged. -#### `run_with_uv` +#### `run_with_uv` ```python run_with_uv(self, command: list[str]) -> None @@ -43,7 +43,7 @@ Execute a command using uv run with this environment configuration. - `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"]) -#### `needs_uv` +#### `needs_uv` ```python needs_uv(self) -> bool @@ -52,7 +52,7 @@ needs_uv(self) -> bool Deprecated: Use _needs_setup() internally or check if build_command modifies the command. -#### `build_uv_run_command` +#### `build_uv_run_command` ```python build_uv_run_command(self, command: list[str]) -> list[str] @@ -61,7 +61,7 @@ build_uv_run_command(self, command: list[str]) -> list[str] Deprecated: Use build_command() instead. -#### `prepare` +#### `prepare` ```python prepare(self, output_dir: Path | None = None) -> None diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx index 7096185a0..e9d8c398d 100644 --- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx @@ -15,7 +15,7 @@ command-line arguments. ## Functions -### `generate_schema` +### `generate_schema` ```python generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None @@ -38,7 +38,7 @@ validation and auto-completion. ## Classes -### `Deployment` +### `Deployment` Configuration for server deployment and runtime settings. @@ -46,7 +46,7 @@ Configuration for server deployment and runtime settings. **Methods:** -#### `apply_runtime_settings` +#### `apply_runtime_settings` ```python apply_runtime_settings(self, config_path: Path | None = None) -> None @@ -62,7 +62,7 @@ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com" will substitute the value of the ENVIRONMENT variable at runtime. -### `MCPServerConfig` +### `MCPServerConfig` Configuration for a FastMCP server. @@ -73,10 +73,10 @@ a FastMCP server in a declarative format. **Methods:** -#### `validate_source` +#### `validate_source` ```python -validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource +validate_source(cls, v: dict | Source) -> SourceType ``` Validate and convert source to proper format. @@ -89,20 +89,18 @@ No string parsing happens here - that's only at CLI boundaries. MCPServerConfig works only with properly typed objects. -#### `validate_environment` +#### `validate_environment` ```python -validate_environment(cls, v: dict | UVEnvironment) -> UVEnvironment +validate_environment(cls, v: dict | Any) -> EnvironmentType ``` -Validate and convert environment to Environment. +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". -#### `validate_deployment` +#### `validate_deployment` ```python validate_deployment(cls, v: dict | Deployment) -> Deployment @@ -115,7 +113,7 @@ Accepts: - dict that can be converted to Deployment -#### `from_file` +#### `from_file` ```python from_file(cls, file_path: Path) -> MCPServerConfig @@ -135,7 +133,7 @@ Load configuration from a JSON file. - `pydantic.ValidationError`: If the configuration is invalid -#### `from_cli_args` +#### `from_cli_args` ```python from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> MCPServerConfig @@ -166,7 +164,7 @@ goes through a config object. - MCPServerConfig instance -#### `find_config` +#### `find_config` ```python find_config(cls, start_path: Path | None = None) -> Path | None @@ -181,7 +179,7 @@ Find a fastmcp.json file in the specified directory. - Path to the configuration file, or None if not found -#### `prepare` +#### `prepare` ```python prepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None @@ -197,7 +195,7 @@ When output_dir is None, does ephemeral caching (for backwards compatibility). - `output_dir`: Directory to create the persistent uv project in (optional) -#### `prepare_environment` +#### `prepare_environment` ```python prepare_environment(self, output_dir: Path | None = None) -> None @@ -212,7 +210,7 @@ Prepare the Python environment. Delegates to the environment's prepare() method -#### `prepare_source` +#### `prepare_source` ```python prepare_source(self) -> None @@ -223,7 +221,7 @@ Prepare the source for loading. Delegates to the source's prepare() method. -#### `run_server` +#### `run_server` ```python run_server(self, **kwargs: Any) -> None diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx index bf80ca1d6..0a2aa84ac 100644 --- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx @@ -7,7 +7,7 @@ sidebarTitle: base ## Classes -### `BaseSource` +### `Source` Abstract base class for all source types. diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx index 9ff83063b..b557613c8 100644 --- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx +++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx @@ -15,7 +15,7 @@ Source for local Python files. **Methods:** -#### `parse_path_with_object` +#### `parse_path_with_object` ```python parse_path_with_object(cls, v: str) -> str @@ -27,7 +27,7 @@ This validator runs before the model is created, allowing us to handle the "file.py:object" syntax at the model boundary. -#### `load_server` +#### `load_server` ```python load_server(self) -> Any From 1c6ba0a0119913e7999d06838d966e44175a75e7 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:40:42 -0400 Subject: [PATCH 10/12] Ignore chore PRs (#1684) --- .github/workflows/update-config-schema.yml | 2 ++ .github/workflows/update-sdk-docs.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/update-config-schema.yml b/.github/workflows/update-config-schema.yml index eb4115dd7..6d0d662e6 100644 --- a/.github/workflows/update-config-schema.yml +++ b/.github/workflows/update-config-schema.yml @@ -79,6 +79,8 @@ jobs: 🤖 Generated by Marvin branch: marvin/update-config-schema + labels: | + ignore in release notes delete-branch: true author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>" committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>" diff --git a/.github/workflows/update-sdk-docs.yml b/.github/workflows/update-sdk-docs.yml index f275d5234..83ffe7859 100644 --- a/.github/workflows/update-sdk-docs.yml +++ b/.github/workflows/update-sdk-docs.yml @@ -62,6 +62,8 @@ jobs: 🤖 Generated by Marvin branch: marvin/update-sdk-docs + labels: | + ignore in release notes delete-branch: true author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>" committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>" From 1db76fb31c87f48c6d9a606da552e69b7f3c92f1 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 31 Aug 2025 17:47:42 -0400 Subject: [PATCH 11/12] Update changelog.mdx (#1691) --- docs/changelog.mdx | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index da758efe6..0b181f8d7 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -4,6 +4,60 @@ icon: "list-check" rss: true --- + + +**[v2.12.0: Auth to the Races](https://github.com/jlowin/fastmcp/releases/tag/v2.12.0)** + +This release introduces major authentication and configuration enhancements that make FastMCP more accessible and powerful for developers working with various identity providers and deployment scenarios. + +## OAuth Proxy: Broader Provider Support + +The OAuth Proxy bridges the gap for authentication providers that don't support Dynamic Client Registration (DCR), a requirement for standard MCP OAuth flows. This feature enables seamless integration with major platforms that previously required complex workarounds. + +**Native integrations now available:** +- GitHub +- Google +- WorkOS +- Azure + +With the OAuth Proxy, you can authenticate users through these providers with minimal configuration, expanding the ecosystem of supported identity platforms and making FastMCP servers more accessible to enterprise environments. + +## Declarative JSON Configuration + +The new `fastmcp.json` configuration system establishes a single source of truth for server settings, replacing scattered configuration across multiple files and environment variables. + +**Configure everything in one place:** +- Dependencies and requirements +- Transport settings +- Server entrypoints +- Metadata and descriptions +- Environment variables + +This standardization not only simplifies deployment but also enables portable server descriptions that can be shared and reused across projects. The typed source system provides validation and autocompletion, reducing configuration errors. + +## Sampling API Fallback + +Not all MCP clients support advanced features like the Sampling API for LLM completions. The new fallback mechanism solves this adoption challenge by allowing servers to generate sampling completions server-side when clients lack support. + +This approach: +- Maintains compatibility with all clients +- Encourages feature adoption without breaking existing integrations +- Provides a smooth upgrade path as client capabilities evolve + +## Breaking Changes +- The `inspect` command now provides structured output with format options for better integration with tooling + +## Additional Enhancements +- Improved CLI configuration parsing with better error messages +- Support for multiple `--with-editable` flags for development workflows +- Comma-separated OAuth scope support for fine-grained permissions +- Configurable logging middleware for better debugging +- Support for importing custom route endpoints + +**Full Changelog**: [v2.11.3...v2.12.0](https://github.com/jlowin/fastmcp/compare/v2.11.3...v2.12.0) + + + **[v2.11.3: API-tite for Change](https://github.com/jlowin/fastmcp/releases/tag/v2.11.3)** From be17a18420a64c5eefc40ac13930f701c9b24334 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:03:55 -0400 Subject: [PATCH 12/12] Fix documentation: use StreamableHttpTransport for headers in testing (#1702) Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Co-authored-by: Jeremiah Lowin --- docs/deployment/testing.mdx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/deployment/testing.mdx b/docs/deployment/testing.mdx index f5db67b58..a7bb63a57 100644 --- a/docs/deployment/testing.mdx +++ b/docs/deployment/testing.mdx @@ -133,11 +133,15 @@ async def test_deployed_server(): The FastMCP Client handles authentication transparently, making it easy to test secured servers: ```python +from fastmcp.client.transports import StreamableHttpTransport + async def test_authenticated_server(): # Bearer token authentication async with Client( - "https://api.example.com/mcp", - headers={"Authorization": "Bearer test-token"} + StreamableHttpTransport( + "https://api.example.com/mcp", + headers={"Authorization": "Bearer test-token"} + ) ) as client: await client.ping() tools = await client.list_tools()