fastmcp 2.10.1__py3-none-any.whl → 2.10.3__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/cli/cli.py CHANGED
@@ -1,6 +1,5 @@
1
- """FastMCP CLI tools."""
1
+ """FastMCP CLI tools using Cyclopts."""
2
2
 
3
- import asyncio
4
3
  import importlib.metadata
5
4
  import importlib.util
6
5
  import os
@@ -8,18 +7,17 @@ import platform
8
7
  import subprocess
9
8
  import sys
10
9
  from pathlib import Path
11
- from typing import Annotated
10
+ from typing import Annotated, Literal
12
11
 
13
- import dotenv
14
- import typer
12
+ import cyclopts
13
+ import pyperclip
15
14
  from pydantic import TypeAdapter
16
15
  from rich.console import Console
17
16
  from rich.table import Table
18
- from typer import Context, Exit
19
17
 
20
18
  import fastmcp
21
- from fastmcp.cli import claude
22
19
  from fastmcp.cli import run as run_module
20
+ from fastmcp.cli.install import install_app
23
21
  from fastmcp.server.server import FastMCP
24
22
  from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
25
23
  from fastmcp.utilities.logging import get_logger
@@ -27,11 +25,10 @@ from fastmcp.utilities.logging import get_logger
27
25
  logger = get_logger("cli")
28
26
  console = Console()
29
27
 
30
- app = typer.Typer(
28
+ app = cyclopts.App(
31
29
  name="fastmcp",
32
- help="FastMCP CLI",
33
- add_completion=False,
34
- no_args_is_help=True, # Show help if no args provided
30
+ help="FastMCP 2.0 - The fast, Pythonic way to build MCP servers and clients.",
31
+ version=fastmcp.__version__,
35
32
  )
36
33
 
37
34
 
@@ -64,6 +61,7 @@ def _build_uv_command(
64
61
  server_spec: str,
65
62
  with_editable: Path | None = None,
66
63
  with_packages: list[str] | None = None,
64
+ no_banner: bool = False,
67
65
  ) -> list[str]:
68
66
  """Build the uv run command that runs a MCP server through mcp run."""
69
67
  cmd = ["uv"]
@@ -80,14 +78,26 @@ def _build_uv_command(
80
78
 
81
79
  # Add mcp run command
82
80
  cmd.extend(["fastmcp", "run", server_spec])
83
- return cmd
84
81
 
82
+ if no_banner:
83
+ cmd.append("--no-banner")
84
+
85
+ return cmd
85
86
 
86
- @app.command()
87
- def version(ctx: Context):
88
- if ctx.resilient_parsing:
89
- return
90
87
 
88
+ @app.command
89
+ def version(
90
+ *,
91
+ copy: Annotated[
92
+ bool,
93
+ cyclopts.Parameter(
94
+ "--copy",
95
+ help="Copy version information to clipboard",
96
+ negative=False,
97
+ ),
98
+ ] = False,
99
+ ):
100
+ """Display version information and platform details."""
91
101
  info = {
92
102
  "FastMCP version": fastmcp.__version__,
93
103
  "MCP version": importlib.metadata.version("mcp"),
@@ -101,58 +111,64 @@ def version(ctx: Context):
101
111
  g.add_column(style="cyan", justify="right")
102
112
  for k, v in info.items():
103
113
  g.add_row(k + ":", str(v).replace("\n", " "))
104
- console.print(g)
105
114
 
106
- raise Exit()
115
+ if copy:
116
+ # Use Rich's plain text rendering for copying
117
+ plain_console = Console(file=None, force_terminal=False, legacy_windows=False)
118
+ with plain_console.capture() as capture:
119
+ plain_console.print(g)
120
+ pyperclip.copy(capture.get())
121
+ console.print("[green]✓[/green] Version information copied to clipboard")
122
+ else:
123
+ console.print(g)
107
124
 
108
125
 
109
- @app.command()
126
+ @app.command
110
127
  def dev(
111
- server_spec: str = typer.Argument(
112
- ...,
113
- help="Python file to run, optionally with :object suffix",
114
- ),
128
+ server_spec: str,
129
+ *,
115
130
  with_editable: Annotated[
116
131
  Path | None,
117
- typer.Option(
118
- "--with-editable",
119
- "-e",
132
+ cyclopts.Parameter(
133
+ name=["--with-editable", "-e"],
120
134
  help="Directory containing pyproject.toml to install in editable mode",
121
- exists=True,
122
- file_okay=False,
123
- resolve_path=True,
124
135
  ),
125
136
  ] = None,
126
137
  with_packages: Annotated[
127
138
  list[str],
128
- typer.Option(
139
+ cyclopts.Parameter(
129
140
  "--with",
130
141
  help="Additional packages to install",
142
+ negative=False,
131
143
  ),
132
144
  ] = [],
133
145
  inspector_version: Annotated[
134
146
  str | None,
135
- typer.Option(
147
+ cyclopts.Parameter(
136
148
  "--inspector-version",
137
149
  help="Version of the MCP Inspector to use",
138
150
  ),
139
151
  ] = None,
140
152
  ui_port: Annotated[
141
153
  int | None,
142
- typer.Option(
154
+ cyclopts.Parameter(
143
155
  "--ui-port",
144
156
  help="Port for the MCP Inspector UI",
145
157
  ),
146
158
  ] = None,
147
159
  server_port: Annotated[
148
160
  int | None,
149
- typer.Option(
161
+ cyclopts.Parameter(
150
162
  "--server-port",
151
163
  help="Port for the MCP Inspector Proxy server",
152
164
  ),
153
165
  ] = None,
154
166
  ) -> None:
155
- """Run a MCP server with the MCP Inspector."""
167
+ """Run an MCP server with the MCP Inspector for development.
168
+
169
+ Args:
170
+ server_spec: Python file to run, optionally with :object suffix
171
+ """
156
172
  file, server_object = run_module.parse_file_path(server_spec)
157
173
 
158
174
  logger.debug(
@@ -192,7 +208,9 @@ def dev(
192
208
  if inspector_version:
193
209
  inspector_cmd += f"@{inspector_version}"
194
210
 
195
- uv_cmd = _build_uv_command(server_spec, with_editable, with_packages)
211
+ uv_cmd = _build_uv_command(
212
+ server_spec, with_editable, with_packages, no_banner=True
213
+ )
196
214
 
197
215
  # Run the MCP Inspector command with shell=True on Windows
198
216
  shell = sys.platform == "win32"
@@ -223,59 +241,69 @@ def dev(
223
241
  sys.exit(1)
224
242
 
225
243
 
226
- @app.command(context_settings={"allow_extra_args": True})
244
+ @app.command
227
245
  def run(
228
- ctx: typer.Context,
229
- server_spec: str = typer.Argument(
230
- ...,
231
- help="Python file, object specification (file:obj), or URL",
232
- ),
246
+ server_spec: str,
247
+ *,
233
248
  transport: Annotated[
234
- str | None,
235
- typer.Option(
236
- "--transport",
237
- "-t",
238
- help="Transport protocol to use (stdio, http, or sse)",
249
+ Literal["stdio", "http", "sse"] | None,
250
+ cyclopts.Parameter(
251
+ name=["--transport", "-t"],
252
+ help="Transport protocol to use",
239
253
  ),
240
254
  ] = None,
241
255
  host: Annotated[
242
256
  str | None,
243
- typer.Option(
257
+ cyclopts.Parameter(
244
258
  "--host",
245
259
  help="Host to bind to when using http transport (default: 127.0.0.1)",
246
260
  ),
247
261
  ] = None,
248
262
  port: Annotated[
249
263
  int | None,
250
- typer.Option(
251
- "--port",
252
- "-p",
264
+ cyclopts.Parameter(
265
+ name=["--port", "-p"],
253
266
  help="Port to bind to when using http transport (default: 8000)",
254
267
  ),
255
268
  ] = None,
256
- log_level: Annotated[
269
+ path: Annotated[
257
270
  str | None,
258
- typer.Option(
259
- "--log-level",
260
- "-l",
261
- help="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
271
+ cyclopts.Parameter(
272
+ "--path",
273
+ help="The route path for the server (default: /mcp/ for http transport, /sse/ for sse transport)",
262
274
  ),
263
275
  ] = None,
276
+ log_level: Annotated[
277
+ Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None,
278
+ cyclopts.Parameter(
279
+ name=["--log-level", "-l"],
280
+ help="Log level",
281
+ ),
282
+ ] = None,
283
+ no_banner: Annotated[
284
+ bool,
285
+ cyclopts.Parameter(
286
+ "--no-banner",
287
+ help="Don't show the server banner",
288
+ negative=False,
289
+ ),
290
+ ] = False,
264
291
  ) -> None:
265
- """Run a MCP server or connect to a remote one.
292
+ """Run an MCP server or connect to a remote one.
266
293
 
267
294
  The server can be specified in three ways:
268
- 1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app.\n
269
- 2. Import approach: server.py:app - imports and runs the specified server object.\n
270
- 3. URL approach: http://server-url - connects to a remote server and creates a proxy.\n\n
271
-
272
- Note: This command runs the server directly. You are responsible for ensuring
273
- all dependencies are available.
295
+ 1. Module approach: server.py - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
296
+ 2. Import approach: server.py:app - imports and runs the specified server object
297
+ 3. URL approach: http://server-url - connects to a remote server and creates a proxy
274
298
 
275
299
  Server arguments can be passed after -- :
276
300
  fastmcp run server.py -- --config config.json --debug
301
+
302
+ Args:
303
+ server_spec: Python file, object specification (file:obj), or URL
277
304
  """
278
- server_args = ctx.args # extra args after --
305
+ # TODO: Handle server_args from extra context
306
+ server_args = [] # Will need to handle this with Cyclopts context
279
307
 
280
308
  logger.debug(
281
309
  "Running server or client",
@@ -284,6 +312,7 @@ def run(
284
312
  "transport": transport,
285
313
  "host": host,
286
314
  "port": port,
315
+ "path": path,
287
316
  "log_level": log_level,
288
317
  "server_args": server_args,
289
318
  },
@@ -295,8 +324,10 @@ def run(
295
324
  transport=transport,
296
325
  host=host,
297
326
  port=port,
327
+ path=path,
298
328
  log_level=log_level,
299
329
  server_args=server_args,
330
+ show_banner=not no_banner,
300
331
  )
301
332
  except Exception as e:
302
333
  logger.error(
@@ -309,166 +340,33 @@ def run(
309
340
  sys.exit(1)
310
341
 
311
342
 
312
- @app.command()
313
- def install(
314
- server_spec: str = typer.Argument(
315
- ...,
316
- help="Python file to run, optionally with :object suffix",
317
- ),
318
- server_name: Annotated[
319
- str | None,
320
- typer.Option(
321
- "--name",
322
- "-n",
323
- help="Custom name for the server (defaults to server's name attribute or"
324
- " file name)",
325
- ),
326
- ] = None,
327
- with_editable: Annotated[
328
- Path | None,
329
- typer.Option(
330
- "--with-editable",
331
- "-e",
332
- help="Directory containing pyproject.toml to install in editable mode",
333
- exists=True,
334
- file_okay=False,
335
- resolve_path=True,
336
- ),
337
- ] = None,
338
- with_packages: Annotated[
339
- list[str],
340
- typer.Option(
341
- "--with",
342
- help="Additional packages to install",
343
- ),
344
- ] = [],
345
- env_vars: Annotated[
346
- list[str],
347
- typer.Option(
348
- "--env-var",
349
- "-v",
350
- help="Environment variables in KEY=VALUE format",
351
- ),
352
- ] = [],
353
- env_file: Annotated[
354
- Path | None,
355
- typer.Option(
356
- "--env-file",
357
- "-f",
358
- help="Load environment variables from a .env file",
359
- exists=True,
360
- file_okay=True,
361
- dir_okay=False,
362
- resolve_path=True,
363
- ),
364
- ] = None,
365
- ) -> None:
366
- """Install a MCP server in the Claude desktop app.
367
-
368
- Environment variables are preserved once added and only updated if new values
369
- are explicitly provided.
370
- """
371
- file, server_object = run_module.parse_file_path(server_spec)
372
-
373
- logger.debug(
374
- "Installing server",
375
- extra={
376
- "file": str(file),
377
- "server_name": server_name,
378
- "server_object": server_object,
379
- "with_editable": str(with_editable) if with_editable else None,
380
- "with_packages": with_packages,
381
- },
382
- )
383
-
384
- if not claude.get_claude_config_path():
385
- logger.error("Claude app not found")
386
- sys.exit(1)
387
-
388
- # Try to import server to get its name, but fall back to file name if dependencies
389
- # missing
390
- name = server_name
391
- server = None
392
- if not name:
393
- try:
394
- server = run_module.import_server(file, server_object)
395
- name = server.name
396
- except (ImportError, ModuleNotFoundError) as e:
397
- logger.debug(
398
- "Could not import server (likely missing dependencies), using file"
399
- " name",
400
- extra={"error": str(e)},
401
- )
402
- name = file.stem
403
-
404
- # Get server dependencies if available
405
- server_dependencies = getattr(server, "dependencies", []) if server else []
406
- if server_dependencies:
407
- with_packages = list(set(with_packages + server_dependencies))
408
-
409
- # Process environment variables if provided
410
- env_dict: dict[str, str] | None = None
411
- if env_file or env_vars:
412
- env_dict = {}
413
- # Load from .env file if specified
414
- if env_file:
415
- try:
416
- env_dict |= {
417
- k: v
418
- for k, v in dotenv.dotenv_values(env_file).items()
419
- if v is not None
420
- }
421
- except Exception as e:
422
- logger.error(f"Failed to load .env file: {e}")
423
- sys.exit(1)
424
-
425
- # Add command line environment variables
426
- for env_var in env_vars:
427
- key, value = _parse_env_var(env_var)
428
- env_dict[key] = value
429
-
430
- if claude.update_claude_config(
431
- server_spec,
432
- name,
433
- with_editable=with_editable,
434
- with_packages=with_packages,
435
- env_vars=env_dict,
436
- ):
437
- logger.info(f"Successfully installed {name} in Claude app")
438
- else:
439
- logger.error(f"Failed to install {name} in Claude app")
440
- sys.exit(1)
441
-
442
-
443
- @app.command()
444
- def inspect(
445
- server_spec: str = typer.Argument(
446
- ...,
447
- help="Python file to inspect, optionally with :object suffix",
448
- ),
343
+ @app.command
344
+ async def inspect(
345
+ server_spec: str,
346
+ *,
449
347
  output: Annotated[
450
348
  Path,
451
- typer.Option(
452
- "--output",
453
- "-o",
349
+ cyclopts.Parameter(
350
+ name=["--output", "-o"],
454
351
  help="Output file path for the JSON report (default: server-info.json)",
455
352
  ),
456
353
  ] = Path("server-info.json"),
457
354
  ) -> None:
458
- """Inspect a FastMCP server and generate a JSON report.
355
+ """Inspect an MCP server and generate a JSON report.
459
356
 
460
- This command analyzes a FastMCP server (v1.x or v2.x) and generates
461
- a comprehensive JSON report containing information about the server's
462
- name, instructions, version, tools, prompts, resources, templates,
463
- and capabilities.
357
+ This command analyzes an MCP server and generates a comprehensive JSON report
358
+ containing information about the server's name, instructions, version, tools,
359
+ prompts, resources, templates, and capabilities.
464
360
 
465
361
  Examples:
466
362
  fastmcp inspect server.py
467
363
  fastmcp inspect server.py -o report.json
468
364
  fastmcp inspect server.py:mcp -o analysis.json
469
365
  fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
470
- """
471
366
 
367
+ Args:
368
+ server_spec: Python file to inspect, optionally with :object suffix
369
+ """
472
370
  # Parse the server specification
473
371
  file, server_object = run_module.parse_file_path(server_spec)
474
372
 
@@ -485,22 +383,8 @@ def inspect(
485
383
  # Import the server
486
384
  server = run_module.import_server(file, server_object)
487
385
 
488
- # Get server information
489
- async def get_info():
490
- return await inspect_fastmcp(server)
491
-
492
- try:
493
- # Try to use existing event loop if available
494
- asyncio.get_running_loop()
495
- # If there's already a loop running, we need to run in a thread
496
- import concurrent.futures
497
-
498
- with concurrent.futures.ThreadPoolExecutor() as executor:
499
- future = executor.submit(asyncio.run, get_info())
500
- info = future.result()
501
- except RuntimeError:
502
- # No running loop, safe to use asyncio.run
503
- info = asyncio.run(get_info())
386
+ # Get server information - using native async support
387
+ info = await inspect_fastmcp(server)
504
388
 
505
389
  info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2)
506
390
 
@@ -533,3 +417,11 @@ def inspect(
533
417
  )
534
418
  console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}")
535
419
  sys.exit(1)
420
+
421
+
422
+ # Add install subcommands using proper Cyclopts pattern
423
+ app.command(install_app)
424
+
425
+
426
+ if __name__ == "__main__":
427
+ app()
@@ -0,0 +1,20 @@
1
+ """Install subcommands for FastMCP CLI using Cyclopts."""
2
+
3
+ import cyclopts
4
+
5
+ from .claude_code import claude_code_command
6
+ from .claude_desktop import claude_desktop_command
7
+ from .cursor import cursor_command
8
+ from .mcp_config import mcp_config_command
9
+
10
+ # Create a cyclopts app for install subcommands
11
+ install_app = cyclopts.App(
12
+ name="install",
13
+ help="Install MCP servers in various clients and formats.",
14
+ )
15
+
16
+ # Register each command from its respective module
17
+ install_app.command(claude_code_command, name="claude-code")
18
+ install_app.command(claude_desktop_command, name="claude-desktop")
19
+ install_app.command(cursor_command, name="cursor")
20
+ install_app.command(mcp_config_command, name="mcp-json")