fastmcp 2.11.3__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/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +139 -64
- 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 +0 -2
- fastmcp/experimental/server/openapi/server.py +0 -2
- fastmcp/experimental/utilities/openapi/parser.py +5 -1
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +2 -0
- fastmcp/resources/resource_manager.py +4 -0
- fastmcp/server/auth/__init__.py +2 -0
- fastmcp/server/auth/auth.py +2 -1
- 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 +24 -12
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/context.py +91 -41
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +3 -3
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +76 -15
- fastmcp/settings.py +16 -1
- fastmcp/tools/tool.py +22 -9
- fastmcp/tools/tool_manager.py +2 -0
- fastmcp/tools/tool_transform.py +39 -10
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +2 -1
- 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/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.3.dist-info/RECORD +0 -108
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/cli.py
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import importlib.util
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import platform
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
10
|
+
from contextlib import contextmanager
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Annotated, Literal
|
|
11
13
|
|
|
12
14
|
import cyclopts
|
|
13
15
|
import pyperclip
|
|
14
|
-
from pydantic import TypeAdapter
|
|
15
16
|
from rich.console import Console
|
|
16
17
|
from rich.table import Table
|
|
17
18
|
|
|
@@ -19,8 +20,14 @@ import fastmcp
|
|
|
19
20
|
from fastmcp.cli import run as run_module
|
|
20
21
|
from fastmcp.cli.install import install_app
|
|
21
22
|
from fastmcp.server.server import FastMCP
|
|
22
|
-
from fastmcp.utilities.inspect import
|
|
23
|
+
from fastmcp.utilities.inspect import (
|
|
24
|
+
InspectFormat,
|
|
25
|
+
format_info,
|
|
26
|
+
inspect_fastmcp,
|
|
27
|
+
)
|
|
23
28
|
from fastmcp.utilities.logging import get_logger
|
|
29
|
+
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
|
30
|
+
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
24
31
|
|
|
25
32
|
logger = get_logger("cli")
|
|
26
33
|
console = Console()
|
|
@@ -57,46 +64,27 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
|
|
|
57
64
|
return key.strip(), value.strip()
|
|
58
65
|
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
with_packages: list[str] | None = None,
|
|
64
|
-
no_banner: bool = False,
|
|
65
|
-
python_version: str | None = None,
|
|
66
|
-
with_requirements: Path | None = None,
|
|
67
|
-
project: Path | None = None,
|
|
68
|
-
) -> list[str]:
|
|
69
|
-
"""Build the uv run command that runs a MCP server through mcp run."""
|
|
70
|
-
cmd = ["uv", "run"]
|
|
71
|
-
|
|
72
|
-
# Add Python version if specified
|
|
73
|
-
if python_version:
|
|
74
|
-
cmd.extend(["--python", python_version])
|
|
75
|
-
|
|
76
|
-
# Add project if specified
|
|
77
|
-
if project:
|
|
78
|
-
cmd.extend(["--project", str(project)])
|
|
79
|
-
|
|
80
|
-
cmd.extend(["--with", "fastmcp"])
|
|
81
|
-
|
|
82
|
-
if with_editable:
|
|
83
|
-
cmd.extend(["--with-editable", str(with_editable)])
|
|
67
|
+
@contextmanager
|
|
68
|
+
def with_argv(args: list[str] | None):
|
|
69
|
+
"""Temporarily replace sys.argv if args provided.
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
cmd.extend(["--with", pkg])
|
|
71
|
+
This context manager is used at the CLI boundary to inject
|
|
72
|
+
server arguments when needed, without mutating sys.argv deep
|
|
73
|
+
in the source loading logic.
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
75
|
+
Args are provided without the script name, so we preserve sys.argv[0]
|
|
76
|
+
and replace the rest.
|
|
77
|
+
"""
|
|
78
|
+
if args is not None:
|
|
79
|
+
original = sys.argv[:]
|
|
80
|
+
try:
|
|
81
|
+
# Preserve the script name (sys.argv[0]) and replace the rest
|
|
82
|
+
sys.argv = [sys.argv[0]] + args
|
|
83
|
+
yield
|
|
84
|
+
finally:
|
|
85
|
+
sys.argv = original
|
|
86
|
+
else:
|
|
87
|
+
yield
|
|
100
88
|
|
|
101
89
|
|
|
102
90
|
@app.command
|
|
@@ -107,7 +95,7 @@ def version(
|
|
|
107
95
|
cyclopts.Parameter(
|
|
108
96
|
"--copy",
|
|
109
97
|
help="Copy version information to clipboard",
|
|
110
|
-
negative=
|
|
98
|
+
negative="",
|
|
111
99
|
),
|
|
112
100
|
] = False,
|
|
113
101
|
):
|
|
@@ -139,23 +127,24 @@ def version(
|
|
|
139
127
|
|
|
140
128
|
@app.command
|
|
141
129
|
async def dev(
|
|
142
|
-
server_spec: str,
|
|
130
|
+
server_spec: str | None = None,
|
|
143
131
|
*,
|
|
144
132
|
with_editable: Annotated[
|
|
145
|
-
Path | None,
|
|
133
|
+
list[Path] | None,
|
|
146
134
|
cyclopts.Parameter(
|
|
147
|
-
|
|
148
|
-
help="Directory containing pyproject.toml to install in editable mode",
|
|
135
|
+
"--with-editable",
|
|
136
|
+
help="Directory containing pyproject.toml to install in editable mode (can be used multiple times)",
|
|
137
|
+
negative="",
|
|
149
138
|
),
|
|
150
139
|
] = None,
|
|
151
140
|
with_packages: Annotated[
|
|
152
|
-
list[str],
|
|
141
|
+
list[str] | None,
|
|
153
142
|
cyclopts.Parameter(
|
|
154
143
|
"--with",
|
|
155
|
-
help="Additional packages to install",
|
|
156
|
-
negative=
|
|
144
|
+
help="Additional packages to install (can be used multiple times)",
|
|
145
|
+
negative="",
|
|
157
146
|
),
|
|
158
|
-
] =
|
|
147
|
+
] = None,
|
|
159
148
|
inspector_version: Annotated[
|
|
160
149
|
str | None,
|
|
161
150
|
cyclopts.Parameter(
|
|
@@ -202,27 +191,64 @@ async def dev(
|
|
|
202
191
|
"""Run an MCP server with the MCP Inspector for development.
|
|
203
192
|
|
|
204
193
|
Args:
|
|
205
|
-
server_spec: Python file to run, optionally with :object suffix
|
|
194
|
+
server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
|
|
206
195
|
"""
|
|
207
|
-
|
|
196
|
+
from fastmcp.utilities.cli import load_and_merge_config
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Load config and apply CLI overrides
|
|
200
|
+
config, server_spec = load_and_merge_config(
|
|
201
|
+
server_spec,
|
|
202
|
+
python=python,
|
|
203
|
+
with_packages=with_packages or [],
|
|
204
|
+
with_requirements=with_requirements,
|
|
205
|
+
project=project,
|
|
206
|
+
editable=[str(p) for p in with_editable] if with_editable else None,
|
|
207
|
+
port=server_port, # Use deployment config for server port
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Get server port from config if not specified via CLI
|
|
211
|
+
if not server_port:
|
|
212
|
+
server_port = config.deployment.port
|
|
213
|
+
|
|
214
|
+
except FileNotFoundError:
|
|
215
|
+
sys.exit(1)
|
|
208
216
|
|
|
209
217
|
logger.debug(
|
|
210
218
|
"Starting dev server",
|
|
211
219
|
extra={
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"with_packages": with_packages,
|
|
220
|
+
"server_spec": server_spec,
|
|
221
|
+
"with_editable": config.environment.editable,
|
|
222
|
+
"with_packages": config.environment.dependencies,
|
|
216
223
|
"ui_port": ui_port,
|
|
217
224
|
"server_port": server_port,
|
|
218
225
|
},
|
|
219
226
|
)
|
|
220
227
|
|
|
221
228
|
try:
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
# Load server to check for deprecated dependencies
|
|
230
|
+
if not config:
|
|
231
|
+
logger.error("No configuration available")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
assert config is not None # For type checker
|
|
234
|
+
server: FastMCP = await config.source.load_server()
|
|
235
|
+
if server.dependencies:
|
|
236
|
+
import warnings
|
|
237
|
+
|
|
238
|
+
warnings.warn(
|
|
239
|
+
f"Server '{server.name}' uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). "
|
|
240
|
+
"Please migrate to fastmcp.json configuration file. "
|
|
241
|
+
"See https://gofastmcp.com/docs/deployment/server-configuration for details.",
|
|
242
|
+
DeprecationWarning,
|
|
243
|
+
stacklevel=2,
|
|
244
|
+
)
|
|
245
|
+
# Merge server dependencies with environment dependencies
|
|
246
|
+
env_deps = config.environment.dependencies or []
|
|
247
|
+
all_deps = list(set(env_deps + server.dependencies))
|
|
248
|
+
if not config.environment:
|
|
249
|
+
config.environment = UVEnvironment(dependencies=all_deps)
|
|
250
|
+
else:
|
|
251
|
+
config.environment.dependencies = all_deps
|
|
226
252
|
|
|
227
253
|
env_vars = {}
|
|
228
254
|
if ui_port:
|
|
@@ -243,30 +269,28 @@ async def dev(
|
|
|
243
269
|
if inspector_version:
|
|
244
270
|
inspector_cmd += f"@{inspector_version}"
|
|
245
271
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
with_packages,
|
|
250
|
-
no_banner=True,
|
|
251
|
-
python_version=python,
|
|
252
|
-
with_requirements=with_requirements,
|
|
253
|
-
project=project,
|
|
272
|
+
# Use the environment from config (already has CLI overrides applied)
|
|
273
|
+
uv_cmd = config.environment.build_command(
|
|
274
|
+
["fastmcp", "run", server_spec, "--no-banner"]
|
|
254
275
|
)
|
|
255
276
|
|
|
277
|
+
# Set marker to prevent infinite loops when subprocess calls FastMCP
|
|
278
|
+
env = dict(os.environ.items()) | env_vars | {"FASTMCP_UV_SPAWNED": "1"}
|
|
279
|
+
|
|
256
280
|
# Run the MCP Inspector command with shell=True on Windows
|
|
257
281
|
shell = sys.platform == "win32"
|
|
258
282
|
process = subprocess.run(
|
|
259
283
|
[npx_cmd, inspector_cmd] + uv_cmd,
|
|
260
284
|
check=True,
|
|
261
285
|
shell=shell,
|
|
262
|
-
env=
|
|
286
|
+
env=env,
|
|
263
287
|
)
|
|
264
288
|
sys.exit(process.returncode)
|
|
265
289
|
except subprocess.CalledProcessError as e:
|
|
266
290
|
logger.error(
|
|
267
291
|
"Dev server failed",
|
|
268
292
|
extra={
|
|
269
|
-
"file": str(
|
|
293
|
+
"file": str(server_spec),
|
|
270
294
|
"error": str(e),
|
|
271
295
|
"returncode": e.returncode,
|
|
272
296
|
},
|
|
@@ -277,14 +301,14 @@ async def dev(
|
|
|
277
301
|
"npx not found. Please ensure Node.js and npm are properly installed "
|
|
278
302
|
"and added to your system PATH. You may need to restart your terminal "
|
|
279
303
|
"after installation.",
|
|
280
|
-
extra={"file": str(
|
|
304
|
+
extra={"file": str(server_spec)},
|
|
281
305
|
)
|
|
282
306
|
sys.exit(1)
|
|
283
307
|
|
|
284
308
|
|
|
285
309
|
@app.command
|
|
286
310
|
async def run(
|
|
287
|
-
server_spec: str,
|
|
311
|
+
server_spec: str | None = None,
|
|
288
312
|
*server_args: str,
|
|
289
313
|
transport: Annotated[
|
|
290
314
|
run_module.TransportType | None,
|
|
@@ -326,7 +350,7 @@ async def run(
|
|
|
326
350
|
cyclopts.Parameter(
|
|
327
351
|
"--no-banner",
|
|
328
352
|
help="Don't show the server banner",
|
|
329
|
-
negative=
|
|
353
|
+
negative="",
|
|
330
354
|
),
|
|
331
355
|
] = False,
|
|
332
356
|
python: Annotated[
|
|
@@ -337,13 +361,13 @@ async def run(
|
|
|
337
361
|
),
|
|
338
362
|
] = None,
|
|
339
363
|
with_packages: Annotated[
|
|
340
|
-
list[str],
|
|
364
|
+
list[str] | None,
|
|
341
365
|
cyclopts.Parameter(
|
|
342
366
|
"--with",
|
|
343
367
|
help="Additional packages to install (can be used multiple times)",
|
|
344
|
-
negative=
|
|
368
|
+
negative="",
|
|
345
369
|
),
|
|
346
|
-
] =
|
|
370
|
+
] = None,
|
|
347
371
|
project: Annotated[
|
|
348
372
|
Path | None,
|
|
349
373
|
cyclopts.Parameter(
|
|
@@ -358,49 +382,109 @@ async def run(
|
|
|
358
382
|
help="Requirements file to install dependencies from",
|
|
359
383
|
),
|
|
360
384
|
] = None,
|
|
385
|
+
skip_source: Annotated[
|
|
386
|
+
bool,
|
|
387
|
+
cyclopts.Parameter(
|
|
388
|
+
"--skip-source",
|
|
389
|
+
help="Skip source preparation step (use when source is already prepared)",
|
|
390
|
+
negative="",
|
|
391
|
+
),
|
|
392
|
+
] = False,
|
|
393
|
+
skip_env: Annotated[
|
|
394
|
+
bool,
|
|
395
|
+
cyclopts.Parameter(
|
|
396
|
+
"--skip-env",
|
|
397
|
+
help="Skip environment configuration (for internal use when already in a uv environment)",
|
|
398
|
+
negative="",
|
|
399
|
+
),
|
|
400
|
+
] = False,
|
|
361
401
|
) -> None:
|
|
362
402
|
"""Run an MCP server or connect to a remote one.
|
|
363
403
|
|
|
364
|
-
The server can be specified in
|
|
404
|
+
The server can be specified in several ways:
|
|
365
405
|
1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
|
|
366
406
|
2. Import approach: "server.py:app" - imports and runs the specified server object
|
|
367
407
|
3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
|
|
368
408
|
4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
|
|
409
|
+
5. FastMCP config: "fastmcp.json" - runs server using FastMCP configuration
|
|
410
|
+
6. No argument: looks for fastmcp.json in current directory
|
|
369
411
|
|
|
370
412
|
Server arguments can be passed after -- :
|
|
371
413
|
fastmcp run server.py -- --config config.json --debug
|
|
372
414
|
|
|
373
415
|
Args:
|
|
374
|
-
server_spec: Python file, object specification (file:obj),
|
|
416
|
+
server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
|
|
375
417
|
"""
|
|
418
|
+
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
|
419
|
+
|
|
420
|
+
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
|
421
|
+
if skip_env or is_already_in_uv_subprocess():
|
|
422
|
+
skip_env = True
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
# Load config and apply CLI overrides
|
|
426
|
+
config, server_spec = load_and_merge_config(
|
|
427
|
+
server_spec,
|
|
428
|
+
python=python,
|
|
429
|
+
with_packages=with_packages or [],
|
|
430
|
+
with_requirements=with_requirements,
|
|
431
|
+
project=project,
|
|
432
|
+
transport=transport,
|
|
433
|
+
host=host,
|
|
434
|
+
port=port,
|
|
435
|
+
path=path,
|
|
436
|
+
log_level=log_level,
|
|
437
|
+
server_args=list(server_args) if server_args else None,
|
|
438
|
+
)
|
|
439
|
+
except FileNotFoundError:
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
|
|
442
|
+
# Get effective values (CLI overrides take precedence)
|
|
443
|
+
final_transport = transport or config.deployment.transport
|
|
444
|
+
final_host = host or config.deployment.host
|
|
445
|
+
final_port = port or config.deployment.port
|
|
446
|
+
final_path = path or config.deployment.path
|
|
447
|
+
final_log_level = log_level or config.deployment.log_level
|
|
448
|
+
final_server_args = server_args or config.deployment.args
|
|
449
|
+
|
|
376
450
|
logger.debug(
|
|
377
451
|
"Running server or client",
|
|
378
452
|
extra={
|
|
379
453
|
"server_spec": server_spec,
|
|
380
|
-
"transport":
|
|
381
|
-
"host":
|
|
382
|
-
"port":
|
|
383
|
-
"path":
|
|
384
|
-
"log_level":
|
|
385
|
-
"server_args": list(
|
|
454
|
+
"transport": final_transport,
|
|
455
|
+
"host": final_host,
|
|
456
|
+
"port": final_port,
|
|
457
|
+
"path": final_path,
|
|
458
|
+
"log_level": final_log_level,
|
|
459
|
+
"server_args": list(final_server_args) if final_server_args else [],
|
|
386
460
|
},
|
|
387
461
|
)
|
|
388
462
|
|
|
389
|
-
#
|
|
390
|
-
if
|
|
463
|
+
# Check if we need to use uv run (but skip if we're already in uv or user said to skip)
|
|
464
|
+
# We check if the environment would modify the command
|
|
465
|
+
test_cmd = ["test"]
|
|
466
|
+
needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env
|
|
467
|
+
|
|
468
|
+
if needs_uv:
|
|
469
|
+
# Use uv run subprocess - always use run_with_uv which handles output correctly
|
|
391
470
|
try:
|
|
392
471
|
run_module.run_with_uv(
|
|
393
472
|
server_spec=server_spec,
|
|
394
|
-
python_version=python,
|
|
395
|
-
with_packages=
|
|
396
|
-
with_requirements=
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
473
|
+
python_version=config.environment.python,
|
|
474
|
+
with_packages=config.environment.dependencies,
|
|
475
|
+
with_requirements=Path(config.environment.requirements)
|
|
476
|
+
if config.environment.requirements
|
|
477
|
+
else None,
|
|
478
|
+
project=Path(config.environment.project)
|
|
479
|
+
if config.environment.project
|
|
480
|
+
else None,
|
|
481
|
+
transport=final_transport,
|
|
482
|
+
host=final_host,
|
|
483
|
+
port=final_port,
|
|
484
|
+
path=final_path,
|
|
485
|
+
log_level=final_log_level,
|
|
403
486
|
show_banner=not no_banner,
|
|
487
|
+
editable=config.environment.editable,
|
|
404
488
|
)
|
|
405
489
|
except Exception as e:
|
|
406
490
|
logger.error(
|
|
@@ -416,13 +500,14 @@ async def run(
|
|
|
416
500
|
try:
|
|
417
501
|
await run_module.run_command(
|
|
418
502
|
server_spec=server_spec,
|
|
419
|
-
transport=
|
|
420
|
-
host=
|
|
421
|
-
port=
|
|
422
|
-
path=
|
|
423
|
-
log_level=
|
|
424
|
-
server_args=list(
|
|
503
|
+
transport=final_transport,
|
|
504
|
+
host=final_host,
|
|
505
|
+
port=final_port,
|
|
506
|
+
path=final_path,
|
|
507
|
+
log_level=final_log_level,
|
|
508
|
+
server_args=list(final_server_args) if final_server_args else [],
|
|
425
509
|
show_banner=not no_banner,
|
|
510
|
+
skip_source=skip_source,
|
|
426
511
|
)
|
|
427
512
|
except Exception as e:
|
|
428
513
|
logger.error(
|
|
@@ -437,70 +522,228 @@ async def run(
|
|
|
437
522
|
|
|
438
523
|
@app.command
|
|
439
524
|
async def inspect(
|
|
440
|
-
server_spec: str,
|
|
525
|
+
server_spec: str | None = None,
|
|
441
526
|
*,
|
|
527
|
+
format: Annotated[
|
|
528
|
+
InspectFormat | None,
|
|
529
|
+
cyclopts.Parameter(
|
|
530
|
+
name=["--format", "-f"],
|
|
531
|
+
help="Output format: fastmcp (FastMCP-specific) or mcp (MCP protocol). Required when using -o.",
|
|
532
|
+
),
|
|
533
|
+
] = None,
|
|
442
534
|
output: Annotated[
|
|
443
|
-
Path,
|
|
535
|
+
Path | None,
|
|
444
536
|
cyclopts.Parameter(
|
|
445
537
|
name=["--output", "-o"],
|
|
446
|
-
help="Output file path for the JSON report
|
|
538
|
+
help="Output file path for the JSON report. If not specified, outputs to stdout when format is provided.",
|
|
539
|
+
),
|
|
540
|
+
] = None,
|
|
541
|
+
python: Annotated[
|
|
542
|
+
str | None,
|
|
543
|
+
cyclopts.Parameter(
|
|
544
|
+
"--python",
|
|
545
|
+
help="Python version to use (e.g., 3.10, 3.11)",
|
|
546
|
+
),
|
|
547
|
+
] = None,
|
|
548
|
+
with_packages: Annotated[
|
|
549
|
+
list[str] | None,
|
|
550
|
+
cyclopts.Parameter(
|
|
551
|
+
"--with",
|
|
552
|
+
help="Additional packages to install (can be used multiple times)",
|
|
553
|
+
negative="",
|
|
554
|
+
),
|
|
555
|
+
] = None,
|
|
556
|
+
project: Annotated[
|
|
557
|
+
Path | None,
|
|
558
|
+
cyclopts.Parameter(
|
|
559
|
+
"--project",
|
|
560
|
+
help="Run the command within the given project directory",
|
|
561
|
+
),
|
|
562
|
+
] = None,
|
|
563
|
+
with_requirements: Annotated[
|
|
564
|
+
Path | None,
|
|
565
|
+
cyclopts.Parameter(
|
|
566
|
+
"--with-requirements",
|
|
567
|
+
help="Requirements file to install dependencies from",
|
|
568
|
+
),
|
|
569
|
+
] = None,
|
|
570
|
+
skip_env: Annotated[
|
|
571
|
+
bool,
|
|
572
|
+
cyclopts.Parameter(
|
|
573
|
+
"--skip-env",
|
|
574
|
+
help="Skip environment configuration (for internal use when already in a uv environment)",
|
|
575
|
+
negative="",
|
|
447
576
|
),
|
|
448
|
-
] =
|
|
577
|
+
] = False,
|
|
449
578
|
) -> None:
|
|
450
|
-
"""Inspect an MCP server and generate a JSON report.
|
|
579
|
+
"""Inspect an MCP server and display information or generate a JSON report.
|
|
451
580
|
|
|
452
|
-
This command analyzes an MCP server
|
|
453
|
-
|
|
454
|
-
prompts, resources, templates, and capabilities.
|
|
581
|
+
This command analyzes an MCP server. Without flags, it displays a text summary.
|
|
582
|
+
Use --format to output complete JSON data.
|
|
455
583
|
|
|
456
584
|
Examples:
|
|
585
|
+
# Show text summary
|
|
457
586
|
fastmcp inspect server.py
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
fastmcp inspect
|
|
587
|
+
|
|
588
|
+
# Output FastMCP format JSON to stdout
|
|
589
|
+
fastmcp inspect server.py --format fastmcp
|
|
590
|
+
|
|
591
|
+
# Save MCP protocol format to file (format required with -o)
|
|
592
|
+
fastmcp inspect server.py --format mcp -o manifest.json
|
|
593
|
+
|
|
594
|
+
# Inspect from fastmcp.json configuration
|
|
595
|
+
fastmcp inspect fastmcp.json
|
|
596
|
+
fastmcp inspect # auto-detect fastmcp.json
|
|
461
597
|
|
|
462
598
|
Args:
|
|
463
|
-
server_spec: Python file to inspect, optionally with :object suffix
|
|
599
|
+
server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
|
|
464
600
|
"""
|
|
465
|
-
|
|
466
|
-
|
|
601
|
+
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
|
602
|
+
|
|
603
|
+
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
|
604
|
+
if skip_env or is_already_in_uv_subprocess():
|
|
605
|
+
skip_env = True
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
# Load config and apply CLI overrides
|
|
609
|
+
config, server_spec = load_and_merge_config(
|
|
610
|
+
server_spec,
|
|
611
|
+
python=python,
|
|
612
|
+
with_packages=with_packages or [],
|
|
613
|
+
with_requirements=with_requirements,
|
|
614
|
+
project=project,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Check if it's an MCPConfig (which inspect doesn't support)
|
|
618
|
+
if server_spec.endswith(".json") and config is None:
|
|
619
|
+
# This might be an MCPConfig, check the file
|
|
620
|
+
try:
|
|
621
|
+
with open(Path(server_spec)) as f:
|
|
622
|
+
data = json.load(f)
|
|
623
|
+
if "mcpServers" in data:
|
|
624
|
+
logger.error("MCPConfig files are not supported by inspect command")
|
|
625
|
+
sys.exit(1)
|
|
626
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
627
|
+
pass
|
|
628
|
+
|
|
629
|
+
except FileNotFoundError:
|
|
630
|
+
sys.exit(1)
|
|
631
|
+
|
|
632
|
+
# Check if we need to use uv run (but skip if we're already in uv or user said to skip)
|
|
633
|
+
# We check if the environment would modify the command
|
|
634
|
+
test_cmd = ["test"]
|
|
635
|
+
needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env
|
|
636
|
+
|
|
637
|
+
if needs_uv:
|
|
638
|
+
# Build and run uv command
|
|
639
|
+
# The environment is already configured in the config object
|
|
640
|
+
inspect_command = [
|
|
641
|
+
"fastmcp",
|
|
642
|
+
"inspect",
|
|
643
|
+
server_spec,
|
|
644
|
+
]
|
|
645
|
+
|
|
646
|
+
# Add format and output flags if specified
|
|
647
|
+
if format:
|
|
648
|
+
inspect_command.extend(["--format", format.value])
|
|
649
|
+
if output:
|
|
650
|
+
inspect_command.extend(["--output", str(output)])
|
|
651
|
+
|
|
652
|
+
# Run the command using subprocess
|
|
653
|
+
import subprocess
|
|
654
|
+
|
|
655
|
+
cmd = config.environment.build_command(inspect_command)
|
|
656
|
+
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
|
657
|
+
process = subprocess.run(cmd, check=True, env=env)
|
|
658
|
+
sys.exit(process.returncode)
|
|
467
659
|
|
|
468
660
|
logger.debug(
|
|
469
661
|
"Inspecting server",
|
|
470
662
|
extra={
|
|
471
|
-
"
|
|
472
|
-
"
|
|
473
|
-
"output": str(output),
|
|
663
|
+
"server_spec": server_spec,
|
|
664
|
+
"format": format,
|
|
665
|
+
"output": str(output) if output else None,
|
|
474
666
|
},
|
|
475
667
|
)
|
|
476
668
|
|
|
477
669
|
try:
|
|
478
|
-
#
|
|
479
|
-
|
|
670
|
+
# Load the server using the config
|
|
671
|
+
if not config:
|
|
672
|
+
logger.error("No configuration available")
|
|
673
|
+
sys.exit(1)
|
|
674
|
+
assert config is not None # For type checker
|
|
675
|
+
server = await config.source.load_server()
|
|
480
676
|
|
|
481
|
-
# Get server information
|
|
677
|
+
# Get basic server information
|
|
482
678
|
info = await inspect_fastmcp(server)
|
|
483
679
|
|
|
484
|
-
|
|
680
|
+
# Check for invalid combination
|
|
681
|
+
if output and not format:
|
|
682
|
+
console.print(
|
|
683
|
+
"[bold red]Error:[/bold red] --format is required when using -o/--output"
|
|
684
|
+
)
|
|
685
|
+
console.print(
|
|
686
|
+
"[dim]Use --format fastmcp or --format mcp to specify the output format[/dim]"
|
|
687
|
+
)
|
|
688
|
+
sys.exit(1)
|
|
689
|
+
|
|
690
|
+
# If no format specified, show text summary
|
|
691
|
+
if format is None:
|
|
692
|
+
# Display text summary
|
|
693
|
+
console.print()
|
|
694
|
+
|
|
695
|
+
# Server section
|
|
696
|
+
console.print("[bold]Server[/bold]")
|
|
697
|
+
console.print(f" Name: {info.name}")
|
|
698
|
+
if info.version:
|
|
699
|
+
console.print(f" Version: {info.version}")
|
|
700
|
+
console.print(f" Generation: {info.server_generation}")
|
|
701
|
+
if info.instructions:
|
|
702
|
+
console.print(f" Instructions: {info.instructions}")
|
|
703
|
+
console.print()
|
|
704
|
+
|
|
705
|
+
# Components section
|
|
706
|
+
console.print("[bold]Components[/bold]")
|
|
707
|
+
console.print(f" Tools: {len(info.tools)}")
|
|
708
|
+
console.print(f" Prompts: {len(info.prompts)}")
|
|
709
|
+
console.print(f" Resources: {len(info.resources)}")
|
|
710
|
+
console.print(f" Templates: {len(info.templates)}")
|
|
711
|
+
console.print()
|
|
712
|
+
|
|
713
|
+
# Environment section
|
|
714
|
+
console.print("[bold]Environment[/bold]")
|
|
715
|
+
console.print(f" FastMCP: {info.fastmcp_version}")
|
|
716
|
+
console.print(f" MCP: {info.mcp_version}")
|
|
717
|
+
console.print()
|
|
718
|
+
|
|
719
|
+
console.print(
|
|
720
|
+
"[dim]Use --format \\[fastmcp|mcp] for complete JSON output[/dim]"
|
|
721
|
+
)
|
|
722
|
+
return
|
|
485
723
|
|
|
486
|
-
#
|
|
487
|
-
|
|
724
|
+
# Generate formatted JSON output
|
|
725
|
+
formatted_json = await format_info(server, format, info)
|
|
488
726
|
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
727
|
+
# Output to file or stdout
|
|
728
|
+
if output:
|
|
729
|
+
# Ensure output directory exists
|
|
730
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
492
731
|
|
|
493
|
-
|
|
732
|
+
# Write JSON report
|
|
733
|
+
with output.open("wb") as f:
|
|
734
|
+
f.write(formatted_json)
|
|
494
735
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
736
|
+
logger.info(f"Server inspection complete. Report saved to {output}")
|
|
737
|
+
|
|
738
|
+
# Print confirmation to console
|
|
739
|
+
console.print(
|
|
740
|
+
f"[bold green]✓[/bold green] Server inspection saved to: [cyan]{output}[/cyan]"
|
|
741
|
+
)
|
|
742
|
+
console.print(f" Server: [bold]{info.name}[/bold]")
|
|
743
|
+
console.print(f" Format: {format.value}")
|
|
744
|
+
else:
|
|
745
|
+
# Output JSON to stdout
|
|
746
|
+
console.print(formatted_json.decode("utf-8"))
|
|
504
747
|
|
|
505
748
|
except Exception as e:
|
|
506
749
|
logger.error(
|
|
@@ -514,6 +757,99 @@ async def inspect(
|
|
|
514
757
|
sys.exit(1)
|
|
515
758
|
|
|
516
759
|
|
|
760
|
+
# Create project subcommand group
|
|
761
|
+
project_app = cyclopts.App(name="project", help="Manage FastMCP projects")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@project_app.command
|
|
765
|
+
async def prepare(
|
|
766
|
+
config_path: Annotated[
|
|
767
|
+
str | None,
|
|
768
|
+
cyclopts.Parameter(help="Path to fastmcp.json configuration file"),
|
|
769
|
+
] = None,
|
|
770
|
+
output_dir: Annotated[
|
|
771
|
+
str | None,
|
|
772
|
+
cyclopts.Parameter(help="Directory to create the persistent environment in"),
|
|
773
|
+
] = None,
|
|
774
|
+
skip_source: Annotated[
|
|
775
|
+
bool,
|
|
776
|
+
cyclopts.Parameter(help="Skip source preparation (e.g., git clone)"),
|
|
777
|
+
] = False,
|
|
778
|
+
) -> None:
|
|
779
|
+
"""Prepare a FastMCP project by creating a persistent uv environment.
|
|
780
|
+
|
|
781
|
+
This command creates a persistent uv project with all dependencies installed:
|
|
782
|
+
- Creates a pyproject.toml with dependencies from the config
|
|
783
|
+
- Installs all Python packages into a .venv
|
|
784
|
+
- Prepares the source (git clone, download, etc.) unless --skip-source
|
|
785
|
+
|
|
786
|
+
After running this command, you can use:
|
|
787
|
+
fastmcp run <config> --project <output-dir>
|
|
788
|
+
|
|
789
|
+
This is useful for:
|
|
790
|
+
- CI/CD pipelines with separate build and run stages
|
|
791
|
+
- Docker images where you prepare during build
|
|
792
|
+
- Production deployments where you want fast startup times
|
|
793
|
+
|
|
794
|
+
Example:
|
|
795
|
+
fastmcp project prepare myserver.json --output-dir ./prepared-env
|
|
796
|
+
fastmcp run myserver.json --project ./prepared-env
|
|
797
|
+
"""
|
|
798
|
+
from pathlib import Path
|
|
799
|
+
|
|
800
|
+
# Require output-dir
|
|
801
|
+
if output_dir is None:
|
|
802
|
+
logger.error(
|
|
803
|
+
"The --output-dir parameter is required.\n"
|
|
804
|
+
"Please specify where to create the persistent environment."
|
|
805
|
+
)
|
|
806
|
+
sys.exit(1)
|
|
807
|
+
|
|
808
|
+
# Auto-detect fastmcp.json if not provided
|
|
809
|
+
if config_path is None:
|
|
810
|
+
found_config = MCPServerConfig.find_config()
|
|
811
|
+
if found_config:
|
|
812
|
+
config_path = str(found_config)
|
|
813
|
+
logger.info(f"Using configuration from {config_path}")
|
|
814
|
+
else:
|
|
815
|
+
logger.error(
|
|
816
|
+
"No configuration file specified and no fastmcp.json found.\n"
|
|
817
|
+
"Please specify a configuration file or create a fastmcp.json."
|
|
818
|
+
)
|
|
819
|
+
sys.exit(1)
|
|
820
|
+
|
|
821
|
+
config_file = Path(config_path)
|
|
822
|
+
if not config_file.exists():
|
|
823
|
+
logger.error(f"Configuration file not found: {config_path}")
|
|
824
|
+
sys.exit(1)
|
|
825
|
+
|
|
826
|
+
output_path = Path(output_dir)
|
|
827
|
+
|
|
828
|
+
try:
|
|
829
|
+
# Load the configuration
|
|
830
|
+
config = MCPServerConfig.from_file(config_file)
|
|
831
|
+
|
|
832
|
+
# Prepare environment and source
|
|
833
|
+
await config.prepare(
|
|
834
|
+
skip_source=skip_source,
|
|
835
|
+
output_dir=output_path,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
console.print(
|
|
839
|
+
f"[bold green]✓[/bold green] Project prepared successfully in {output_path}!\n"
|
|
840
|
+
f"You can now run the server with:\n"
|
|
841
|
+
f" [cyan]fastmcp run {config_path} --project {output_dir}[/cyan]"
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
except Exception as e:
|
|
845
|
+
logger.error(f"Failed to prepare project: {e}")
|
|
846
|
+
console.print(f"[bold red]✗[/bold red] Failed to prepare project: {e}")
|
|
847
|
+
sys.exit(1)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# Add project subcommand group
|
|
851
|
+
app.command(project_app)
|
|
852
|
+
|
|
517
853
|
# Add install subcommands using proper Cyclopts pattern
|
|
518
854
|
app.command(install_app)
|
|
519
855
|
|