fastmcp 2.11.2__py3-none-any.whl → 2.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/logging.py +25 -1
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +143 -67
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +1 -3
- fastmcp/experimental/server/openapi/server.py +10 -25
- fastmcp/experimental/utilities/openapi/__init__.py +2 -2
- fastmcp/experimental/utilities/openapi/formatters.py +34 -0
- fastmcp/experimental/utilities/openapi/models.py +5 -2
- fastmcp/experimental/utilities/openapi/parser.py +252 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +4 -2
- fastmcp/resources/resource_manager.py +16 -6
- fastmcp/server/auth/__init__.py +11 -1
- fastmcp/server/auth/auth.py +19 -2
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +27 -16
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/context.py +91 -41
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +44 -37
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +85 -20
- fastmcp/settings.py +18 -3
- fastmcp/tools/tool.py +23 -10
- fastmcp/tools/tool_manager.py +5 -1
- fastmcp/tools/tool_transform.py +75 -32
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +21 -5
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/openapi.py +4 -4
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/METADATA +3 -2
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/install/mcp_json.py
CHANGED
|
@@ -10,6 +10,7 @@ import pyperclip
|
|
|
10
10
|
from rich import print
|
|
11
11
|
|
|
12
12
|
from fastmcp.utilities.logging import get_logger
|
|
13
|
+
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
13
14
|
|
|
14
15
|
from .shared import process_common_args
|
|
15
16
|
|
|
@@ -21,7 +22,7 @@ def install_mcp_json(
|
|
|
21
22
|
server_object: str | None,
|
|
22
23
|
name: str,
|
|
23
24
|
*,
|
|
24
|
-
with_editable: Path | None = None,
|
|
25
|
+
with_editable: list[Path] | None = None,
|
|
25
26
|
with_packages: list[str] | None = None,
|
|
26
27
|
env_vars: dict[str, str] | None = None,
|
|
27
28
|
copy: bool = False,
|
|
@@ -35,7 +36,7 @@ def install_mcp_json(
|
|
|
35
36
|
file: Path to the server file
|
|
36
37
|
server_object: Optional server object name (for :object suffix)
|
|
37
38
|
name: Name for the server in MCP config
|
|
38
|
-
with_editable: Optional
|
|
39
|
+
with_editable: Optional list of directories to install in editable mode
|
|
39
40
|
with_packages: Optional list of additional packages to install
|
|
40
41
|
env_vars: Optional dictionary of environment variables
|
|
41
42
|
copy: If True, copy to clipboard instead of printing to stdout
|
|
@@ -47,45 +48,34 @@ def install_mcp_json(
|
|
|
47
48
|
True if generation was successful, False otherwise
|
|
48
49
|
"""
|
|
49
50
|
try:
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Add Python version if specified
|
|
54
|
-
if python_version:
|
|
55
|
-
args.extend(["--python", python_version])
|
|
56
|
-
|
|
57
|
-
# Add project if specified
|
|
58
|
-
if project:
|
|
59
|
-
args.extend(["--project", str(project)])
|
|
60
|
-
|
|
61
|
-
# Collect all packages in a set to deduplicate
|
|
62
|
-
packages = {"fastmcp"}
|
|
51
|
+
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
|
|
52
|
+
deduplicated_packages = None
|
|
63
53
|
if with_packages:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
deduplicated = list(dict.fromkeys(with_packages))
|
|
55
|
+
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
|
|
56
|
+
if not deduplicated_packages:
|
|
57
|
+
deduplicated_packages = None
|
|
58
|
+
|
|
59
|
+
env_config = UVEnvironment(
|
|
60
|
+
python=python_version,
|
|
61
|
+
dependencies=deduplicated_packages,
|
|
62
|
+
requirements=str(with_requirements) if with_requirements else None,
|
|
63
|
+
project=str(project) if project else None,
|
|
64
|
+
editable=[str(p) for p in with_editable] if with_editable else None,
|
|
65
|
+
)
|
|
76
66
|
# Build server spec from parsed components
|
|
77
67
|
if server_object:
|
|
78
68
|
server_spec = f"{file.resolve()}:{server_object}"
|
|
79
69
|
else:
|
|
80
70
|
server_spec = str(file.resolve())
|
|
81
71
|
|
|
82
|
-
#
|
|
83
|
-
|
|
72
|
+
# Build the full command
|
|
73
|
+
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
|
84
74
|
|
|
85
75
|
# Build MCP server configuration
|
|
86
76
|
server_config = {
|
|
87
|
-
"command":
|
|
88
|
-
"args":
|
|
77
|
+
"command": full_command[0],
|
|
78
|
+
"args": full_command[1:],
|
|
89
79
|
}
|
|
90
80
|
|
|
91
81
|
# Add environment variables if provided
|
|
@@ -124,28 +114,29 @@ async def mcp_json_command(
|
|
|
124
114
|
),
|
|
125
115
|
] = None,
|
|
126
116
|
with_editable: Annotated[
|
|
127
|
-
Path | None,
|
|
117
|
+
list[Path] | None,
|
|
128
118
|
cyclopts.Parameter(
|
|
129
|
-
|
|
130
|
-
help="Directory with pyproject.toml to install in editable mode",
|
|
119
|
+
"--with-editable",
|
|
120
|
+
help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
|
|
121
|
+
negative="",
|
|
131
122
|
),
|
|
132
123
|
] = None,
|
|
133
124
|
with_packages: Annotated[
|
|
134
|
-
list[str],
|
|
125
|
+
list[str] | None,
|
|
135
126
|
cyclopts.Parameter(
|
|
136
127
|
"--with",
|
|
137
|
-
help="Additional packages to install",
|
|
138
|
-
negative=
|
|
128
|
+
help="Additional packages to install (can be used multiple times)",
|
|
129
|
+
negative="",
|
|
139
130
|
),
|
|
140
|
-
] =
|
|
131
|
+
] = None,
|
|
141
132
|
env_vars: Annotated[
|
|
142
|
-
list[str],
|
|
133
|
+
list[str] | None,
|
|
143
134
|
cyclopts.Parameter(
|
|
144
135
|
"--env",
|
|
145
|
-
help="Environment variables in KEY=VALUE format",
|
|
146
|
-
negative=
|
|
136
|
+
help="Environment variables in KEY=VALUE format (can be used multiple times)",
|
|
137
|
+
negative="",
|
|
147
138
|
),
|
|
148
|
-
] =
|
|
139
|
+
] = None,
|
|
149
140
|
env_file: Annotated[
|
|
150
141
|
Path | None,
|
|
151
142
|
cyclopts.Parameter(
|
|
@@ -158,7 +149,7 @@ async def mcp_json_command(
|
|
|
158
149
|
cyclopts.Parameter(
|
|
159
150
|
"--copy",
|
|
160
151
|
help="Copy configuration to clipboard instead of printing to stdout",
|
|
161
|
-
negative=
|
|
152
|
+
negative="",
|
|
162
153
|
),
|
|
163
154
|
] = False,
|
|
164
155
|
python: Annotated[
|
|
@@ -188,6 +179,10 @@ async def mcp_json_command(
|
|
|
188
179
|
Args:
|
|
189
180
|
server_spec: Python file to install, optionally with :object suffix
|
|
190
181
|
"""
|
|
182
|
+
# Convert None to empty lists for list parameters
|
|
183
|
+
with_editable = with_editable or []
|
|
184
|
+
with_packages = with_packages or []
|
|
185
|
+
env_vars = env_vars or []
|
|
191
186
|
file, server_object, name, packages, env_dict = await process_common_args(
|
|
192
187
|
server_spec, server_name, with_packages, env_vars, env_file
|
|
193
188
|
)
|
fastmcp/cli/install/shared.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""Shared utilities for install commands."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from dotenv import dotenv_values
|
|
8
|
+
from pydantic import ValidationError
|
|
7
9
|
from rich import print
|
|
8
10
|
|
|
9
|
-
from fastmcp.cli.run import import_server, parse_file_path
|
|
10
11
|
from fastmcp.utilities.logging import get_logger
|
|
12
|
+
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
|
13
|
+
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
|
11
14
|
|
|
12
15
|
logger = get_logger(__name__)
|
|
13
16
|
|
|
@@ -26,13 +29,57 @@ def parse_env_var(env_var: str) -> tuple[str, str]:
|
|
|
26
29
|
async def process_common_args(
|
|
27
30
|
server_spec: str,
|
|
28
31
|
server_name: str | None,
|
|
29
|
-
with_packages: list[str],
|
|
30
|
-
env_vars: list[str],
|
|
32
|
+
with_packages: list[str] | None,
|
|
33
|
+
env_vars: list[str] | None,
|
|
31
34
|
env_file: Path | None,
|
|
32
35
|
) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]:
|
|
33
|
-
"""Process common arguments shared by all install commands.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
"""Process common arguments shared by all install commands.
|
|
37
|
+
|
|
38
|
+
Handles both fastmcp.json config files and traditional file.py:object syntax.
|
|
39
|
+
"""
|
|
40
|
+
# Convert None to empty lists for list parameters
|
|
41
|
+
with_packages = with_packages or []
|
|
42
|
+
env_vars = env_vars or []
|
|
43
|
+
# Create MCPServerConfig from server_spec
|
|
44
|
+
config = None
|
|
45
|
+
if server_spec.endswith(".json"):
|
|
46
|
+
config_path = Path(server_spec).resolve()
|
|
47
|
+
if not config_path.exists():
|
|
48
|
+
print(f"[red]Configuration file not found: {config_path}[/red]")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
with open(config_path) as f:
|
|
53
|
+
data = json.load(f)
|
|
54
|
+
|
|
55
|
+
# Check if it's an MCPConfig (has mcpServers key)
|
|
56
|
+
if "mcpServers" in data:
|
|
57
|
+
# MCPConfig files aren't supported for install
|
|
58
|
+
print("[red]MCPConfig files are not supported for installation[/red]")
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
else:
|
|
61
|
+
# It's a MCPServerConfig
|
|
62
|
+
config = MCPServerConfig.from_file(config_path)
|
|
63
|
+
|
|
64
|
+
# Merge packages from config if not overridden
|
|
65
|
+
if config.environment.dependencies:
|
|
66
|
+
# Merge with CLI packages (CLI takes precedence)
|
|
67
|
+
config_packages = list(config.environment.dependencies)
|
|
68
|
+
with_packages = list(set(with_packages + config_packages))
|
|
69
|
+
except (json.JSONDecodeError, ValidationError) as e:
|
|
70
|
+
print(f"[red]Invalid configuration file: {e}[/red]")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
else:
|
|
73
|
+
# Create config from file path
|
|
74
|
+
source = FileSystemSource(path=server_spec)
|
|
75
|
+
config = MCPServerConfig(source=source)
|
|
76
|
+
|
|
77
|
+
# Extract file and server_object from the source
|
|
78
|
+
# The FileSystemSource handles parsing path:object syntax
|
|
79
|
+
file = Path(config.source.path).resolve()
|
|
80
|
+
server_object = (
|
|
81
|
+
config.source.entrypoint if hasattr(config.source, "entrypoint") else None
|
|
82
|
+
)
|
|
36
83
|
|
|
37
84
|
logger.debug(
|
|
38
85
|
"Installing server",
|
|
@@ -49,7 +96,7 @@ async def process_common_args(
|
|
|
49
96
|
server = None
|
|
50
97
|
if not name:
|
|
51
98
|
try:
|
|
52
|
-
server = await
|
|
99
|
+
server = await config.source.load_server()
|
|
53
100
|
name = server.name
|
|
54
101
|
except (ImportError, ModuleNotFoundError) as e:
|
|
55
102
|
logger.debug(
|
|
@@ -59,8 +106,18 @@ async def process_common_args(
|
|
|
59
106
|
name = file.stem
|
|
60
107
|
|
|
61
108
|
# Get server dependencies if available
|
|
109
|
+
# TODO: Remove dependencies handling (deprecated in v2.11.4)
|
|
62
110
|
server_dependencies = getattr(server, "dependencies", []) if server else []
|
|
63
111
|
if server_dependencies:
|
|
112
|
+
import warnings
|
|
113
|
+
|
|
114
|
+
warnings.warn(
|
|
115
|
+
"Server uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
|
|
116
|
+
"Please migrate to fastmcp.json configuration file. "
|
|
117
|
+
"See https://gofastmcp.com/docs/deployment/server-configuration for details.",
|
|
118
|
+
DeprecationWarning,
|
|
119
|
+
stacklevel=2,
|
|
120
|
+
)
|
|
64
121
|
with_packages = list(set(with_packages + server_dependencies))
|
|
65
122
|
|
|
66
123
|
# Process environment variables if provided
|
fastmcp/cli/run.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"""FastMCP run command implementation with enhanced type hints."""
|
|
2
2
|
|
|
3
|
-
import importlib.util
|
|
4
|
-
import inspect
|
|
5
3
|
import json
|
|
4
|
+
import os
|
|
6
5
|
import re
|
|
7
6
|
import subprocess
|
|
8
7
|
import sys
|
|
9
|
-
from functools import partial
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from typing import Any, Literal
|
|
12
10
|
|
|
@@ -14,6 +12,11 @@ from mcp.server.fastmcp import FastMCP as FastMCP1x
|
|
|
14
12
|
|
|
15
13
|
from fastmcp.server.server import FastMCP
|
|
16
14
|
from fastmcp.utilities.logging import get_logger
|
|
15
|
+
from fastmcp.utilities.mcp_server_config import (
|
|
16
|
+
MCPServerConfig,
|
|
17
|
+
)
|
|
18
|
+
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
19
|
+
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
|
17
20
|
|
|
18
21
|
logger = get_logger("cli.run")
|
|
19
22
|
|
|
@@ -28,154 +31,6 @@ def is_url(path: str) -> bool:
|
|
|
28
31
|
return bool(url_pattern.match(path))
|
|
29
32
|
|
|
30
33
|
|
|
31
|
-
def parse_file_path(server_spec: str) -> tuple[Path, str | None]:
|
|
32
|
-
"""Parse a file path that may include a server object specification.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
server_spec: Path to file, optionally with :object suffix
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
Tuple of (file_path, server_object)
|
|
39
|
-
"""
|
|
40
|
-
# First check if we have a Windows path (e.g., C:\...)
|
|
41
|
-
has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
|
|
42
|
-
|
|
43
|
-
# Split on the last colon, but only if it's not part of the Windows drive letter
|
|
44
|
-
# and there's actually another colon in the string after the drive letter
|
|
45
|
-
if ":" in (server_spec[2:] if has_windows_drive else server_spec):
|
|
46
|
-
file_str, server_object = server_spec.rsplit(":", 1)
|
|
47
|
-
else:
|
|
48
|
-
file_str, server_object = server_spec, None
|
|
49
|
-
|
|
50
|
-
# Resolve the file path
|
|
51
|
-
file_path = Path(file_str).expanduser().resolve()
|
|
52
|
-
if not file_path.exists():
|
|
53
|
-
logger.error(f"File not found: {file_path}")
|
|
54
|
-
sys.exit(1)
|
|
55
|
-
if not file_path.is_file():
|
|
56
|
-
logger.error(f"Not a file: {file_path}")
|
|
57
|
-
sys.exit(1)
|
|
58
|
-
|
|
59
|
-
return file_path, server_object
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async def import_server(file: Path, server_or_factory: str | None = None) -> Any:
|
|
63
|
-
"""Import a MCP server from a file.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
file: Path to the file
|
|
67
|
-
server_or_factory: Optional object name in format "module:object" or just "object"
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
The server object (or result of calling a factory function)
|
|
71
|
-
"""
|
|
72
|
-
# Add parent directory to Python path so imports can be resolved
|
|
73
|
-
file_dir = str(file.parent)
|
|
74
|
-
if file_dir not in sys.path:
|
|
75
|
-
sys.path.insert(0, file_dir)
|
|
76
|
-
|
|
77
|
-
# Import the module
|
|
78
|
-
spec = importlib.util.spec_from_file_location("server_module", file)
|
|
79
|
-
if not spec or not spec.loader:
|
|
80
|
-
logger.error("Could not load module", extra={"file": str(file)})
|
|
81
|
-
sys.exit(1)
|
|
82
|
-
|
|
83
|
-
assert spec is not None
|
|
84
|
-
assert spec.loader is not None
|
|
85
|
-
|
|
86
|
-
module = importlib.util.module_from_spec(spec)
|
|
87
|
-
spec.loader.exec_module(module)
|
|
88
|
-
|
|
89
|
-
# If no object specified, try common server names
|
|
90
|
-
if not server_or_factory:
|
|
91
|
-
# Look for common server instance names
|
|
92
|
-
for name in ["mcp", "server", "app"]:
|
|
93
|
-
if hasattr(module, name):
|
|
94
|
-
obj = getattr(module, name)
|
|
95
|
-
return await _resolve_server_or_factory(obj, file, name)
|
|
96
|
-
|
|
97
|
-
logger.error(
|
|
98
|
-
f"No server object found in {file}. Please either:\n"
|
|
99
|
-
"1. Use a standard variable name (mcp, server, or app)\n"
|
|
100
|
-
"2. Specify the object name with file:object syntax",
|
|
101
|
-
extra={"file": str(file)},
|
|
102
|
-
)
|
|
103
|
-
sys.exit(1)
|
|
104
|
-
|
|
105
|
-
assert server_or_factory is not None
|
|
106
|
-
|
|
107
|
-
# Handle module:object syntax
|
|
108
|
-
if ":" in server_or_factory:
|
|
109
|
-
module_name, object_name = server_or_factory.split(":", 1)
|
|
110
|
-
try:
|
|
111
|
-
server_module = importlib.import_module(module_name)
|
|
112
|
-
obj = getattr(server_module, object_name, None)
|
|
113
|
-
except ImportError:
|
|
114
|
-
logger.error(
|
|
115
|
-
f"Could not import module '{module_name}'",
|
|
116
|
-
extra={"file": str(file)},
|
|
117
|
-
)
|
|
118
|
-
sys.exit(1)
|
|
119
|
-
else:
|
|
120
|
-
# Just object name
|
|
121
|
-
obj = getattr(module, server_or_factory, None)
|
|
122
|
-
|
|
123
|
-
if obj is None:
|
|
124
|
-
logger.error(
|
|
125
|
-
f"Server object '{server_or_factory}' not found",
|
|
126
|
-
extra={"file": str(file)},
|
|
127
|
-
)
|
|
128
|
-
sys.exit(1)
|
|
129
|
-
|
|
130
|
-
return await _resolve_server_or_factory(obj, file, server_or_factory)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
async def _resolve_server_or_factory(obj: Any, file: Path, name: str) -> Any:
|
|
134
|
-
"""Resolve a server object or factory function to a server instance.
|
|
135
|
-
|
|
136
|
-
Args:
|
|
137
|
-
obj: The object that might be a server or factory function
|
|
138
|
-
file: Path to the file for error messages
|
|
139
|
-
name: Name of the object for error messages
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
A server instance
|
|
143
|
-
"""
|
|
144
|
-
# Check if it's a function or coroutine function
|
|
145
|
-
if inspect.isfunction(obj) or inspect.iscoroutinefunction(obj):
|
|
146
|
-
logger.debug(f"Found factory function '{name}' in {file}")
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
if inspect.iscoroutinefunction(obj):
|
|
150
|
-
# Async factory function
|
|
151
|
-
server = await obj()
|
|
152
|
-
else:
|
|
153
|
-
# Sync factory function
|
|
154
|
-
server = obj()
|
|
155
|
-
|
|
156
|
-
# Validate the result is a FastMCP server
|
|
157
|
-
if not isinstance(server, FastMCP | FastMCP1x):
|
|
158
|
-
logger.error(
|
|
159
|
-
f"Factory function '{name}' must return a FastMCP server instance, "
|
|
160
|
-
f"got {type(server).__name__}",
|
|
161
|
-
extra={"file": str(file)},
|
|
162
|
-
)
|
|
163
|
-
sys.exit(1)
|
|
164
|
-
|
|
165
|
-
logger.debug(f"Factory function '{name}' created server: {server.name}")
|
|
166
|
-
return server
|
|
167
|
-
|
|
168
|
-
except Exception as e:
|
|
169
|
-
logger.error(
|
|
170
|
-
f"Failed to call factory function '{name}': {e}",
|
|
171
|
-
extra={"file": str(file)},
|
|
172
|
-
)
|
|
173
|
-
sys.exit(1)
|
|
174
|
-
|
|
175
|
-
# Not a function, return as-is (should be a server instance)
|
|
176
|
-
return obj
|
|
177
|
-
|
|
178
|
-
|
|
179
34
|
def run_with_uv(
|
|
180
35
|
server_spec: str,
|
|
181
36
|
python_version: str | None = None,
|
|
@@ -188,11 +43,16 @@ def run_with_uv(
|
|
|
188
43
|
path: str | None = None,
|
|
189
44
|
log_level: LogLevelType | None = None,
|
|
190
45
|
show_banner: bool = True,
|
|
46
|
+
editable: str | list[str] | None = None,
|
|
191
47
|
) -> None:
|
|
192
48
|
"""Run a MCP server using uv run subprocess.
|
|
193
49
|
|
|
50
|
+
This function is called when we need to set up a Python environment with specific
|
|
51
|
+
dependencies before running the server. The config parsing and merging should already
|
|
52
|
+
be done by the caller.
|
|
53
|
+
|
|
194
54
|
Args:
|
|
195
|
-
server_spec: Python file, object specification (file:obj), or URL
|
|
55
|
+
server_spec: Python file, object specification (file:obj), config file, or URL
|
|
196
56
|
python_version: Python version to use (e.g. "3.10")
|
|
197
57
|
with_packages: Additional packages to install
|
|
198
58
|
with_requirements: Requirements file to use
|
|
@@ -203,51 +63,49 @@ def run_with_uv(
|
|
|
203
63
|
path: Path to bind to when using http transport
|
|
204
64
|
log_level: Log level
|
|
205
65
|
show_banner: Whether to show the server banner
|
|
66
|
+
editable: Editable package paths
|
|
206
67
|
"""
|
|
207
|
-
cmd = ["uv", "run"]
|
|
208
|
-
|
|
209
|
-
# Add Python version if specified
|
|
210
|
-
if python_version:
|
|
211
|
-
cmd.extend(["--python", python_version])
|
|
212
|
-
|
|
213
|
-
# Add project if specified
|
|
214
|
-
if project:
|
|
215
|
-
cmd.extend(["--project", str(project)])
|
|
216
|
-
|
|
217
|
-
# Add fastmcp package
|
|
218
|
-
cmd.extend(["--with", "fastmcp"])
|
|
219
|
-
|
|
220
|
-
# Add additional packages
|
|
221
|
-
if with_packages:
|
|
222
|
-
for pkg in with_packages:
|
|
223
|
-
if pkg:
|
|
224
|
-
cmd.extend(["--with", pkg])
|
|
225
|
-
|
|
226
|
-
# Add requirements file
|
|
227
|
-
if with_requirements:
|
|
228
|
-
cmd.extend(["--with-requirements", str(with_requirements)])
|
|
229
68
|
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
69
|
+
# Build uv command using Environment.build_uv_run_command()
|
|
70
|
+
env_config = UVEnvironment(
|
|
71
|
+
python=python_version,
|
|
72
|
+
dependencies=with_packages if with_packages else None,
|
|
73
|
+
requirements=str(with_requirements.resolve()) if with_requirements else None,
|
|
74
|
+
project=str(project.resolve()) if project else None,
|
|
75
|
+
editable=editable
|
|
76
|
+
if isinstance(editable, list)
|
|
77
|
+
else ([editable] if editable else None),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Build the inner fastmcp command (environment variable prevents infinite recursion)
|
|
81
|
+
inner_cmd = ["fastmcp", "run", server_spec]
|
|
82
|
+
|
|
83
|
+
# Add transport options to the inner command
|
|
234
84
|
if transport:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
85
|
+
inner_cmd.extend(["--transport", transport])
|
|
86
|
+
# Only add HTTP-specific options for non-stdio transports
|
|
87
|
+
if transport != "stdio":
|
|
88
|
+
if host:
|
|
89
|
+
inner_cmd.extend(["--host", host])
|
|
90
|
+
if port:
|
|
91
|
+
inner_cmd.extend(["--port", str(port)])
|
|
92
|
+
if path:
|
|
93
|
+
inner_cmd.extend(["--path", path])
|
|
242
94
|
if log_level:
|
|
243
|
-
|
|
95
|
+
inner_cmd.extend(["--log-level", log_level])
|
|
244
96
|
if not show_banner:
|
|
245
|
-
|
|
97
|
+
inner_cmd.append("--no-banner")
|
|
98
|
+
|
|
99
|
+
# Build the full uv command
|
|
100
|
+
cmd = env_config.build_command(inner_cmd)
|
|
101
|
+
|
|
102
|
+
# Set marker to prevent infinite loops when subprocess calls FastMCP again
|
|
103
|
+
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
|
246
104
|
|
|
247
105
|
# Run the command
|
|
248
106
|
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
249
107
|
try:
|
|
250
|
-
process = subprocess.run(cmd, check=True)
|
|
108
|
+
process = subprocess.run(cmd, check=True, env=env)
|
|
251
109
|
sys.exit(process.returncode)
|
|
252
110
|
except subprocess.CalledProcessError as e:
|
|
253
111
|
logger.error(f"Failed to run server: {e}")
|
|
@@ -285,30 +143,21 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
|
|
|
285
143
|
return server
|
|
286
144
|
|
|
287
145
|
|
|
288
|
-
|
|
289
|
-
file
|
|
290
|
-
server_or_factory: str | None = None,
|
|
291
|
-
server_args: list[str] | None = None,
|
|
292
|
-
) -> Any:
|
|
293
|
-
"""Import a server with optional command line arguments.
|
|
146
|
+
def load_mcp_server_config(config_path: Path) -> MCPServerConfig:
|
|
147
|
+
"""Load a FastMCP configuration from a fastmcp.json file.
|
|
294
148
|
|
|
295
149
|
Args:
|
|
296
|
-
|
|
297
|
-
server_or_factory: Optional server object or factory function name
|
|
298
|
-
server_args: Optional command line arguments to inject
|
|
150
|
+
config_path: Path to fastmcp.json file
|
|
299
151
|
|
|
300
152
|
Returns:
|
|
301
|
-
|
|
153
|
+
MCPServerConfig object
|
|
302
154
|
"""
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
sys.argv = original_argv
|
|
310
|
-
else:
|
|
311
|
-
return await import_server(file, server_or_factory)
|
|
155
|
+
config = MCPServerConfig.from_file(config_path)
|
|
156
|
+
|
|
157
|
+
# Apply runtime settings from deployment config
|
|
158
|
+
config.deployment.apply_runtime_settings(config_path)
|
|
159
|
+
|
|
160
|
+
return config
|
|
312
161
|
|
|
313
162
|
|
|
314
163
|
async def run_command(
|
|
@@ -321,11 +170,12 @@ async def run_command(
|
|
|
321
170
|
server_args: list[str] | None = None,
|
|
322
171
|
show_banner: bool = True,
|
|
323
172
|
use_direct_import: bool = False,
|
|
173
|
+
skip_source: bool = False,
|
|
324
174
|
) -> None:
|
|
325
175
|
"""Run a MCP server or connect to a remote one.
|
|
326
176
|
|
|
327
177
|
Args:
|
|
328
|
-
server_spec: Python file, object specification (file:obj),
|
|
178
|
+
server_spec: Python file, object specification (file:obj), config file, or URL
|
|
329
179
|
transport: Transport protocol to use
|
|
330
180
|
host: Host to bind to when using http transport
|
|
331
181
|
port: Port to bind to when using http transport
|
|
@@ -334,18 +184,73 @@ async def run_command(
|
|
|
334
184
|
server_args: Additional arguments to pass to the server
|
|
335
185
|
show_banner: Whether to show the server banner
|
|
336
186
|
use_direct_import: Whether to use direct import instead of subprocess
|
|
187
|
+
skip_source: Whether to skip source preparation step
|
|
337
188
|
"""
|
|
189
|
+
# Special case: URLs
|
|
338
190
|
if is_url(server_spec):
|
|
339
191
|
# Handle URL case
|
|
340
192
|
server = create_client_server(server_spec)
|
|
341
193
|
logger.debug(f"Created client proxy server for {server_spec}")
|
|
194
|
+
# Special case: MCPConfig files (legacy)
|
|
342
195
|
elif server_spec.endswith(".json"):
|
|
343
|
-
|
|
196
|
+
# Load JSON and check which type of config it is
|
|
197
|
+
config_path = Path(server_spec)
|
|
198
|
+
with open(config_path) as f:
|
|
199
|
+
data = json.load(f)
|
|
200
|
+
|
|
201
|
+
# Check if it's an MCPConfig first (has canonical mcpServers key)
|
|
202
|
+
if "mcpServers" in data:
|
|
203
|
+
# It's an MCP config
|
|
204
|
+
server = create_mcp_config_server(config_path)
|
|
205
|
+
else:
|
|
206
|
+
# It's a FastMCP config - load it properly
|
|
207
|
+
config = load_mcp_server_config(config_path)
|
|
208
|
+
|
|
209
|
+
# Merge deployment config with CLI arguments (CLI takes precedence)
|
|
210
|
+
transport = transport or config.deployment.transport
|
|
211
|
+
host = host or config.deployment.host
|
|
212
|
+
port = port or config.deployment.port
|
|
213
|
+
path = path or config.deployment.path
|
|
214
|
+
log_level = log_level or config.deployment.log_level
|
|
215
|
+
server_args = (
|
|
216
|
+
server_args if server_args is not None else config.deployment.args
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Prepare source only (environment is handled by uv run)
|
|
220
|
+
await config.prepare_source() if not skip_source else None
|
|
221
|
+
|
|
222
|
+
# Load the server using the source
|
|
223
|
+
from contextlib import nullcontext
|
|
224
|
+
|
|
225
|
+
from fastmcp.cli.cli import with_argv
|
|
226
|
+
|
|
227
|
+
# Use sys.argv context manager if deployment args specified
|
|
228
|
+
argv_context = with_argv(server_args) if server_args else nullcontext()
|
|
229
|
+
|
|
230
|
+
with argv_context:
|
|
231
|
+
server = await config.source.load_server()
|
|
232
|
+
|
|
233
|
+
logger.debug(f'Found server "{server.name}" from config {config_path}')
|
|
344
234
|
else:
|
|
345
|
-
#
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
235
|
+
# Regular file case - create a MCPServerConfig with FileSystemSource
|
|
236
|
+
source = FileSystemSource(path=server_spec)
|
|
237
|
+
config = MCPServerConfig(source=source)
|
|
238
|
+
|
|
239
|
+
# Prepare source only (environment is handled by uv run)
|
|
240
|
+
await config.prepare_source() if not skip_source else None
|
|
241
|
+
|
|
242
|
+
# Load the server
|
|
243
|
+
from contextlib import nullcontext
|
|
244
|
+
|
|
245
|
+
from fastmcp.cli.cli import with_argv
|
|
246
|
+
|
|
247
|
+
# Use sys.argv context manager if server_args specified
|
|
248
|
+
argv_context = with_argv(server_args) if server_args else nullcontext()
|
|
249
|
+
|
|
250
|
+
with argv_context:
|
|
251
|
+
server = await config.source.load_server()
|
|
252
|
+
|
|
253
|
+
logger.debug(f'Found server "{server.name}" in {source.path}')
|
|
349
254
|
|
|
350
255
|
# Run the server
|
|
351
256
|
|
|
@@ -363,8 +268,8 @@ async def run_command(
|
|
|
363
268
|
kwargs["port"] = port
|
|
364
269
|
if path:
|
|
365
270
|
kwargs["path"] = path
|
|
366
|
-
|
|
367
|
-
|
|
271
|
+
# Note: log_level is not currently supported by run_async
|
|
272
|
+
# TODO: Add log_level support to server.run_async
|
|
368
273
|
|
|
369
274
|
if not show_banner:
|
|
370
275
|
kwargs["show_banner"] = False
|
|
@@ -382,6 +287,8 @@ def run_v1_server(
|
|
|
382
287
|
port: int | None = None,
|
|
383
288
|
transport: TransportType | None = None,
|
|
384
289
|
) -> None:
|
|
290
|
+
from functools import partial
|
|
291
|
+
|
|
385
292
|
if host:
|
|
386
293
|
server.settings.host = host
|
|
387
294
|
if port:
|