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.
Files changed (69) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/oauth_callback.py +91 -91
  13. fastmcp/client/sampling.py +12 -4
  14. fastmcp/client/transports.py +139 -64
  15. fastmcp/experimental/sampling/__init__.py +0 -0
  16. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  17. fastmcp/experimental/sampling/handlers/base.py +21 -0
  18. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  19. fastmcp/experimental/server/openapi/routing.py +0 -2
  20. fastmcp/experimental/server/openapi/server.py +0 -2
  21. fastmcp/experimental/utilities/openapi/parser.py +5 -1
  22. fastmcp/mcp_config.py +40 -20
  23. fastmcp/prompts/prompt_manager.py +2 -0
  24. fastmcp/resources/resource_manager.py +4 -0
  25. fastmcp/server/auth/__init__.py +2 -0
  26. fastmcp/server/auth/auth.py +2 -1
  27. fastmcp/server/auth/oauth_proxy.py +1047 -0
  28. fastmcp/server/auth/providers/azure.py +270 -0
  29. fastmcp/server/auth/providers/github.py +287 -0
  30. fastmcp/server/auth/providers/google.py +305 -0
  31. fastmcp/server/auth/providers/jwt.py +24 -12
  32. fastmcp/server/auth/providers/workos.py +256 -2
  33. fastmcp/server/auth/redirect_validation.py +65 -0
  34. fastmcp/server/context.py +91 -41
  35. fastmcp/server/elicitation.py +60 -1
  36. fastmcp/server/http.py +3 -3
  37. fastmcp/server/middleware/logging.py +66 -28
  38. fastmcp/server/proxy.py +2 -0
  39. fastmcp/server/sampling/handler.py +19 -0
  40. fastmcp/server/server.py +76 -15
  41. fastmcp/settings.py +16 -1
  42. fastmcp/tools/tool.py +22 -9
  43. fastmcp/tools/tool_manager.py +2 -0
  44. fastmcp/tools/tool_transform.py +39 -10
  45. fastmcp/utilities/auth.py +34 -0
  46. fastmcp/utilities/cli.py +148 -15
  47. fastmcp/utilities/components.py +2 -1
  48. fastmcp/utilities/inspect.py +166 -37
  49. fastmcp/utilities/json_schema_type.py +4 -2
  50. fastmcp/utilities/logging.py +4 -1
  51. fastmcp/utilities/mcp_config.py +47 -18
  52. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  53. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  54. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  55. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  59. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  60. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  61. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  62. fastmcp/utilities/tests.py +7 -2
  63. fastmcp/utilities/types.py +15 -2
  64. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
  65. fastmcp-2.12.0.dist-info/RECORD +129 -0
  66. fastmcp-2.11.3.dist-info/RECORD +0 -108
  67. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  68. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  69. {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 FastMCPInfo, inspect_fastmcp
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
- def _build_uv_command(
61
- server_spec: str,
62
- with_editable: Path | None = None,
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
- if with_packages:
86
- for pkg in with_packages:
87
- if pkg:
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
- if with_requirements:
91
- cmd.extend(["--with-requirements", str(with_requirements)])
92
-
93
- # Add mcp run command
94
- cmd.extend(["fastmcp", "run", server_spec])
95
-
96
- if no_banner:
97
- cmd.append("--no-banner")
98
-
99
- return cmd
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=False,
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
- name=["--with-editable", "-e"],
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=False,
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
- file, server_object = run_module.parse_file_path(server_spec)
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
- "file": str(file),
213
- "server_object": server_object,
214
- "with_editable": str(with_editable) if with_editable else None,
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
- # Import server to get dependencies
223
- server: FastMCP = await run_module.import_server(file, server_object)
224
- if server.dependencies is not None:
225
- with_packages = list(set(with_packages + server.dependencies))
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
- uv_cmd = _build_uv_command(
247
- server_spec,
248
- with_editable,
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=dict(os.environ.items()) | env_vars,
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(file),
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(file)},
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=False,
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=False,
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 four ways:
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), MCPConfig file, or URL
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": transport,
381
- "host": host,
382
- "port": port,
383
- "path": path,
384
- "log_level": log_level,
385
- "server_args": list(server_args),
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
- # If any uv-specific options are provided, use uv run
390
- if python or with_packages or with_requirements or project:
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=with_packages,
396
- with_requirements=with_requirements,
397
- project=project,
398
- transport=transport,
399
- host=host,
400
- port=port,
401
- path=path,
402
- log_level=log_level,
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=transport,
420
- host=host,
421
- port=port,
422
- path=path,
423
- log_level=log_level,
424
- server_args=list(server_args),
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 (default: server-info.json)",
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
- ] = Path("server-info.json"),
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 and generates a comprehensive JSON report
453
- containing information about the server's name, instructions, version, tools,
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
- fastmcp inspect server.py -o report.json
459
- fastmcp inspect server.py:mcp -o analysis.json
460
- fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
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
- # Parse the server specification
466
- file, server_object = run_module.parse_file_path(server_spec)
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
- "file": str(file),
472
- "server_object": server_object,
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
- # Import the server
479
- server = await run_module.import_server(file, server_object)
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 - using native async support
677
+ # Get basic server information
482
678
  info = await inspect_fastmcp(server)
483
679
 
484
- info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2)
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
- # Ensure output directory exists
487
- output.parent.mkdir(parents=True, exist_ok=True)
724
+ # Generate formatted JSON output
725
+ formatted_json = await format_info(server, format, info)
488
726
 
489
- # Write JSON report (always pretty-printed)
490
- with output.open("w", encoding="utf-8") as f:
491
- f.write(info_json.decode("utf-8"))
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
- logger.info(f"Server inspection complete. Report saved to {output}")
732
+ # Write JSON report
733
+ with output.open("wb") as f:
734
+ f.write(formatted_json)
494
735
 
495
- # Print summary to console
496
- console.print(
497
- f"[bold green]✓[/bold green] Inspected server: [bold]{info.name}[/bold]"
498
- )
499
- console.print(f" Tools: {len(info.tools)}")
500
- console.print(f" Prompts: {len(info.prompts)}")
501
- console.print(f" Resources: {len(info.resources)}")
502
- console.print(f" Templates: {len(info.templates)}")
503
- console.print(f" Report saved to: [cyan]{output}[/cyan]")
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