fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__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.0rc1.dist-info}/METADATA +3 -2
- fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from fastmcp.utilities.logging import get_logger
|
|
11
|
+
from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
|
|
12
|
+
|
|
13
|
+
logger = get_logger("cli.config")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UVEnvironment(Environment):
|
|
17
|
+
"""Configuration for Python environment setup."""
|
|
18
|
+
|
|
19
|
+
type: Literal["uv"] = "uv"
|
|
20
|
+
|
|
21
|
+
python: str | None = Field(
|
|
22
|
+
default=None,
|
|
23
|
+
description="Python version constraint",
|
|
24
|
+
examples=["3.10", "3.11", "3.12"],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
dependencies: list[str] | None = Field(
|
|
28
|
+
default=None,
|
|
29
|
+
description="Python packages to install with PEP 508 specifiers",
|
|
30
|
+
examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
requirements: str | None = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
description="Path to requirements.txt file",
|
|
36
|
+
examples=["requirements.txt", "../requirements/prod.txt"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
project: str | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="Path to project directory containing pyproject.toml",
|
|
42
|
+
examples=[".", "../my-project"],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
editable: list[str] | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description="Directories to install in editable mode",
|
|
48
|
+
examples=[[".", "../my-package"], ["/path/to/package"]],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def build_command(self, command: list[str]) -> list[str]:
|
|
52
|
+
"""Build complete uv run command with environment args and command to execute.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
command: Command to execute (e.g., ["fastmcp", "run", "server.py"])
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Complete command ready for subprocess.run, including "uv" prefix if needed.
|
|
59
|
+
If no environment configuration is set, returns the command unchanged.
|
|
60
|
+
"""
|
|
61
|
+
# If no environment setup is needed, return command as-is
|
|
62
|
+
if not self._needs_setup():
|
|
63
|
+
return command
|
|
64
|
+
|
|
65
|
+
args = ["uv", "run"]
|
|
66
|
+
|
|
67
|
+
# Add project if specified
|
|
68
|
+
if self.project:
|
|
69
|
+
args.extend(["--project", str(self.project)])
|
|
70
|
+
|
|
71
|
+
# Add Python version if specified (only if no project, as project has its own Python)
|
|
72
|
+
if self.python and not self.project:
|
|
73
|
+
args.extend(["--python", self.python])
|
|
74
|
+
|
|
75
|
+
# Always add dependencies, requirements, and editable packages
|
|
76
|
+
# These work with --project to add additional packages on top of the project env
|
|
77
|
+
if self.dependencies:
|
|
78
|
+
for dep in self.dependencies:
|
|
79
|
+
args.extend(["--with", dep])
|
|
80
|
+
|
|
81
|
+
# Add requirements file
|
|
82
|
+
if self.requirements:
|
|
83
|
+
args.extend(["--with-requirements", str(self.requirements)])
|
|
84
|
+
|
|
85
|
+
# Add editable packages
|
|
86
|
+
if self.editable:
|
|
87
|
+
for editable_path in self.editable:
|
|
88
|
+
args.extend(["--with-editable", str(editable_path)])
|
|
89
|
+
|
|
90
|
+
# Add the command
|
|
91
|
+
args.extend(command)
|
|
92
|
+
|
|
93
|
+
return args
|
|
94
|
+
|
|
95
|
+
def run_with_uv(self, command: list[str]) -> None:
|
|
96
|
+
"""Execute a command using uv run with this environment configuration.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
|
|
100
|
+
"""
|
|
101
|
+
import subprocess
|
|
102
|
+
|
|
103
|
+
# Build the full uv command
|
|
104
|
+
cmd = self.build_command(command)
|
|
105
|
+
|
|
106
|
+
# Set marker to prevent infinite loops when subprocess calls FastMCP again
|
|
107
|
+
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
|
108
|
+
|
|
109
|
+
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Run without capturing output so it flows through naturally
|
|
113
|
+
process = subprocess.run(cmd, check=True, env=env)
|
|
114
|
+
sys.exit(process.returncode)
|
|
115
|
+
except subprocess.CalledProcessError as e:
|
|
116
|
+
logger.error(f"Command failed: {e}")
|
|
117
|
+
sys.exit(e.returncode)
|
|
118
|
+
|
|
119
|
+
def _needs_setup(self) -> bool:
|
|
120
|
+
"""Check if this environment config requires uv to set up.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if any environment settings require uv run
|
|
124
|
+
"""
|
|
125
|
+
return any(
|
|
126
|
+
[
|
|
127
|
+
self.python is not None,
|
|
128
|
+
self.dependencies is not None,
|
|
129
|
+
self.requirements is not None,
|
|
130
|
+
self.project is not None,
|
|
131
|
+
self.editable is not None,
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Backward compatibility aliases
|
|
136
|
+
def needs_uv(self) -> bool:
|
|
137
|
+
"""Deprecated: Use _needs_setup() internally or check if build_command modifies the command."""
|
|
138
|
+
return self._needs_setup()
|
|
139
|
+
|
|
140
|
+
def build_uv_run_command(self, command: list[str]) -> list[str]:
|
|
141
|
+
"""Deprecated: Use build_command() instead."""
|
|
142
|
+
return self.build_command(command)
|
|
143
|
+
|
|
144
|
+
async def prepare(self, output_dir: Path | None = None) -> None:
|
|
145
|
+
"""Prepare the Python environment using uv.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
output_dir: Directory where the persistent uv project will be created.
|
|
149
|
+
If None, creates a temporary directory for ephemeral use.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
# Check if uv is available
|
|
153
|
+
if not shutil.which("uv"):
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
"uv is not installed. Please install it with: "
|
|
156
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Only prepare environment if there are actual settings to apply
|
|
160
|
+
if not self._needs_setup():
|
|
161
|
+
logger.debug("No environment settings configured, skipping preparation")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Handle None case for ephemeral use
|
|
165
|
+
if output_dir is None:
|
|
166
|
+
import tempfile
|
|
167
|
+
|
|
168
|
+
output_dir = Path(tempfile.mkdtemp(prefix="fastmcp-env-"))
|
|
169
|
+
logger.info(f"Creating ephemeral environment in {output_dir}")
|
|
170
|
+
else:
|
|
171
|
+
logger.info(f"Creating persistent environment in {output_dir}")
|
|
172
|
+
output_dir = Path(output_dir).resolve()
|
|
173
|
+
|
|
174
|
+
# Initialize the project
|
|
175
|
+
logger.debug(f"Initializing uv project in {output_dir}")
|
|
176
|
+
try:
|
|
177
|
+
subprocess.run(
|
|
178
|
+
[
|
|
179
|
+
"uv",
|
|
180
|
+
"init",
|
|
181
|
+
"--project",
|
|
182
|
+
str(output_dir),
|
|
183
|
+
"--name",
|
|
184
|
+
"fastmcp-env",
|
|
185
|
+
],
|
|
186
|
+
check=True,
|
|
187
|
+
capture_output=True,
|
|
188
|
+
text=True,
|
|
189
|
+
)
|
|
190
|
+
except subprocess.CalledProcessError as e:
|
|
191
|
+
# If project already exists, that's fine - continue
|
|
192
|
+
if "already initialized" in e.stderr.lower():
|
|
193
|
+
logger.debug(
|
|
194
|
+
f"Project already initialized at {output_dir}, continuing..."
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
logger.error(f"Failed to initialize project: {e.stderr}")
|
|
198
|
+
raise RuntimeError(f"Failed to initialize project: {e.stderr}") from e
|
|
199
|
+
|
|
200
|
+
# Pin Python version if specified
|
|
201
|
+
if self.python:
|
|
202
|
+
logger.debug(f"Pinning Python version to {self.python}")
|
|
203
|
+
try:
|
|
204
|
+
subprocess.run(
|
|
205
|
+
[
|
|
206
|
+
"uv",
|
|
207
|
+
"python",
|
|
208
|
+
"pin",
|
|
209
|
+
self.python,
|
|
210
|
+
"--project",
|
|
211
|
+
str(output_dir),
|
|
212
|
+
],
|
|
213
|
+
check=True,
|
|
214
|
+
capture_output=True,
|
|
215
|
+
text=True,
|
|
216
|
+
)
|
|
217
|
+
except subprocess.CalledProcessError as e:
|
|
218
|
+
logger.error(f"Failed to pin Python version: {e.stderr}")
|
|
219
|
+
raise RuntimeError(f"Failed to pin Python version: {e.stderr}") from e
|
|
220
|
+
|
|
221
|
+
# Add dependencies with --no-sync to defer installation
|
|
222
|
+
# dependencies ALWAYS include fastmcp; this is compatible with
|
|
223
|
+
# specific fastmcp versions that might be in the dependencies list
|
|
224
|
+
dependencies = (self.dependencies or []) + ["fastmcp"]
|
|
225
|
+
logger.debug(f"Adding dependencies: {', '.join(dependencies)}")
|
|
226
|
+
try:
|
|
227
|
+
subprocess.run(
|
|
228
|
+
[
|
|
229
|
+
"uv",
|
|
230
|
+
"add",
|
|
231
|
+
*dependencies,
|
|
232
|
+
"--no-sync",
|
|
233
|
+
"--project",
|
|
234
|
+
str(output_dir),
|
|
235
|
+
],
|
|
236
|
+
check=True,
|
|
237
|
+
capture_output=True,
|
|
238
|
+
text=True,
|
|
239
|
+
)
|
|
240
|
+
except subprocess.CalledProcessError as e:
|
|
241
|
+
logger.error(f"Failed to add dependencies: {e.stderr}")
|
|
242
|
+
raise RuntimeError(f"Failed to add dependencies: {e.stderr}") from e
|
|
243
|
+
|
|
244
|
+
# Add requirements file if specified
|
|
245
|
+
if self.requirements:
|
|
246
|
+
logger.debug(f"Adding requirements from {self.requirements}")
|
|
247
|
+
# Resolve requirements path relative to current directory
|
|
248
|
+
req_path = Path(self.requirements).resolve()
|
|
249
|
+
try:
|
|
250
|
+
subprocess.run(
|
|
251
|
+
[
|
|
252
|
+
"uv",
|
|
253
|
+
"add",
|
|
254
|
+
"-r",
|
|
255
|
+
str(req_path),
|
|
256
|
+
"--no-sync",
|
|
257
|
+
"--project",
|
|
258
|
+
str(output_dir),
|
|
259
|
+
],
|
|
260
|
+
check=True,
|
|
261
|
+
capture_output=True,
|
|
262
|
+
text=True,
|
|
263
|
+
)
|
|
264
|
+
except subprocess.CalledProcessError as e:
|
|
265
|
+
logger.error(f"Failed to add requirements: {e.stderr}")
|
|
266
|
+
raise RuntimeError(f"Failed to add requirements: {e.stderr}") from e
|
|
267
|
+
|
|
268
|
+
# Add editable packages if specified
|
|
269
|
+
if self.editable:
|
|
270
|
+
editable_paths = [str(Path(e).resolve()) for e in self.editable]
|
|
271
|
+
logger.debug(f"Adding editable packages: {', '.join(editable_paths)}")
|
|
272
|
+
try:
|
|
273
|
+
subprocess.run(
|
|
274
|
+
[
|
|
275
|
+
"uv",
|
|
276
|
+
"add",
|
|
277
|
+
"--editable",
|
|
278
|
+
*editable_paths,
|
|
279
|
+
"--no-sync",
|
|
280
|
+
"--project",
|
|
281
|
+
str(output_dir),
|
|
282
|
+
],
|
|
283
|
+
check=True,
|
|
284
|
+
capture_output=True,
|
|
285
|
+
text=True,
|
|
286
|
+
)
|
|
287
|
+
except subprocess.CalledProcessError as e:
|
|
288
|
+
logger.error(f"Failed to add editable packages: {e.stderr}")
|
|
289
|
+
raise RuntimeError(
|
|
290
|
+
f"Failed to add editable packages: {e.stderr}"
|
|
291
|
+
) from e
|
|
292
|
+
|
|
293
|
+
# Final sync to install everything
|
|
294
|
+
logger.info("Installing dependencies...")
|
|
295
|
+
try:
|
|
296
|
+
subprocess.run(
|
|
297
|
+
["uv", "sync", "--project", str(output_dir)],
|
|
298
|
+
check=True,
|
|
299
|
+
capture_output=True,
|
|
300
|
+
text=True,
|
|
301
|
+
)
|
|
302
|
+
except subprocess.CalledProcessError as e:
|
|
303
|
+
logger.error(f"Failed to sync dependencies: {e.stderr}")
|
|
304
|
+
raise RuntimeError(f"Failed to sync dependencies: {e.stderr}") from e
|
|
305
|
+
|
|
306
|
+
logger.info(f"Environment prepared successfully in {output_dir}")
|