fastmcp 2.10.2__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
 
@@ -88,11 +85,19 @@ def _build_uv_command(
88
85
  return cmd
89
86
 
90
87
 
91
- @app.command()
92
- def version(ctx: Context):
93
- if ctx.resilient_parsing:
94
- return
95
-
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."""
96
101
  info = {
97
102
  "FastMCP version": fastmcp.__version__,
98
103
  "MCP version": importlib.metadata.version("mcp"),
@@ -106,58 +111,64 @@ def version(ctx: Context):
106
111
  g.add_column(style="cyan", justify="right")
107
112
  for k, v in info.items():
108
113
  g.add_row(k + ":", str(v).replace("\n", " "))
109
- console.print(g)
110
114
 
111
- 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)
112
124
 
113
125
 
114
- @app.command()
126
+ @app.command
115
127
  def dev(
116
- server_spec: str = typer.Argument(
117
- ...,
118
- help="Python file to run, optionally with :object suffix",
119
- ),
128
+ server_spec: str,
129
+ *,
120
130
  with_editable: Annotated[
121
131
  Path | None,
122
- typer.Option(
123
- "--with-editable",
124
- "-e",
132
+ cyclopts.Parameter(
133
+ name=["--with-editable", "-e"],
125
134
  help="Directory containing pyproject.toml to install in editable mode",
126
- exists=True,
127
- file_okay=False,
128
- resolve_path=True,
129
135
  ),
130
136
  ] = None,
131
137
  with_packages: Annotated[
132
138
  list[str],
133
- typer.Option(
139
+ cyclopts.Parameter(
134
140
  "--with",
135
141
  help="Additional packages to install",
142
+ negative=False,
136
143
  ),
137
144
  ] = [],
138
145
  inspector_version: Annotated[
139
146
  str | None,
140
- typer.Option(
147
+ cyclopts.Parameter(
141
148
  "--inspector-version",
142
149
  help="Version of the MCP Inspector to use",
143
150
  ),
144
151
  ] = None,
145
152
  ui_port: Annotated[
146
153
  int | None,
147
- typer.Option(
154
+ cyclopts.Parameter(
148
155
  "--ui-port",
149
156
  help="Port for the MCP Inspector UI",
150
157
  ),
151
158
  ] = None,
152
159
  server_port: Annotated[
153
160
  int | None,
154
- typer.Option(
161
+ cyclopts.Parameter(
155
162
  "--server-port",
156
163
  help="Port for the MCP Inspector Proxy server",
157
164
  ),
158
165
  ] = None,
159
166
  ) -> None:
160
- """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
+ """
161
172
  file, server_object = run_module.parse_file_path(server_spec)
162
173
 
163
174
  logger.debug(
@@ -230,66 +241,69 @@ def dev(
230
241
  sys.exit(1)
231
242
 
232
243
 
233
- @app.command(context_settings={"allow_extra_args": True})
244
+ @app.command
234
245
  def run(
235
- ctx: typer.Context,
236
- server_spec: str = typer.Argument(
237
- ...,
238
- help="Python file, object specification (file:obj), or URL",
239
- ),
246
+ server_spec: str,
247
+ *,
240
248
  transport: Annotated[
241
- str | None,
242
- typer.Option(
243
- "--transport",
244
- "-t",
245
- 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",
246
253
  ),
247
254
  ] = None,
248
255
  host: Annotated[
249
256
  str | None,
250
- typer.Option(
257
+ cyclopts.Parameter(
251
258
  "--host",
252
259
  help="Host to bind to when using http transport (default: 127.0.0.1)",
253
260
  ),
254
261
  ] = None,
255
262
  port: Annotated[
256
263
  int | None,
257
- typer.Option(
258
- "--port",
259
- "-p",
264
+ cyclopts.Parameter(
265
+ name=["--port", "-p"],
260
266
  help="Port to bind to when using http transport (default: 8000)",
261
267
  ),
262
268
  ] = None,
263
- log_level: Annotated[
269
+ path: Annotated[
264
270
  str | None,
265
- typer.Option(
266
- "--log-level",
267
- "-l",
268
- 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)",
274
+ ),
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",
269
281
  ),
270
282
  ] = None,
271
283
  no_banner: Annotated[
272
284
  bool,
273
- typer.Option(
285
+ cyclopts.Parameter(
274
286
  "--no-banner",
275
287
  help="Don't show the server banner",
288
+ negative=False,
276
289
  ),
277
290
  ] = False,
278
291
  ) -> None:
279
- """Run a MCP server or connect to a remote one.
292
+ """Run an MCP server or connect to a remote one.
280
293
 
281
294
  The server can be specified in three ways:
282
- 1. Module approach: server.py - runs the module directly, looking for an object named mcp/server/app.\n
283
- 2. Import approach: server.py:app - imports and runs the specified server object.\n
284
- 3. URL approach: http://server-url - connects to a remote server and creates a proxy.\n\n
285
-
286
- Note: This command runs the server directly. You are responsible for ensuring
287
- 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
288
298
 
289
299
  Server arguments can be passed after -- :
290
300
  fastmcp run server.py -- --config config.json --debug
301
+
302
+ Args:
303
+ server_spec: Python file, object specification (file:obj), or URL
291
304
  """
292
- 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
293
307
 
294
308
  logger.debug(
295
309
  "Running server or client",
@@ -298,6 +312,7 @@ def run(
298
312
  "transport": transport,
299
313
  "host": host,
300
314
  "port": port,
315
+ "path": path,
301
316
  "log_level": log_level,
302
317
  "server_args": server_args,
303
318
  },
@@ -309,6 +324,7 @@ def run(
309
324
  transport=transport,
310
325
  host=host,
311
326
  port=port,
327
+ path=path,
312
328
  log_level=log_level,
313
329
  server_args=server_args,
314
330
  show_banner=not no_banner,
@@ -324,166 +340,33 @@ def run(
324
340
  sys.exit(1)
325
341
 
326
342
 
327
- @app.command()
328
- def install(
329
- server_spec: str = typer.Argument(
330
- ...,
331
- help="Python file to run, optionally with :object suffix",
332
- ),
333
- server_name: Annotated[
334
- str | None,
335
- typer.Option(
336
- "--name",
337
- "-n",
338
- help="Custom name for the server (defaults to server's name attribute or"
339
- " file name)",
340
- ),
341
- ] = None,
342
- with_editable: Annotated[
343
- Path | None,
344
- typer.Option(
345
- "--with-editable",
346
- "-e",
347
- help="Directory containing pyproject.toml to install in editable mode",
348
- exists=True,
349
- file_okay=False,
350
- resolve_path=True,
351
- ),
352
- ] = None,
353
- with_packages: Annotated[
354
- list[str],
355
- typer.Option(
356
- "--with",
357
- help="Additional packages to install",
358
- ),
359
- ] = [],
360
- env_vars: Annotated[
361
- list[str],
362
- typer.Option(
363
- "--env-var",
364
- "-v",
365
- help="Environment variables in KEY=VALUE format",
366
- ),
367
- ] = [],
368
- env_file: Annotated[
369
- Path | None,
370
- typer.Option(
371
- "--env-file",
372
- "-f",
373
- help="Load environment variables from a .env file",
374
- exists=True,
375
- file_okay=True,
376
- dir_okay=False,
377
- resolve_path=True,
378
- ),
379
- ] = None,
380
- ) -> None:
381
- """Install a MCP server in the Claude desktop app.
382
-
383
- Environment variables are preserved once added and only updated if new values
384
- are explicitly provided.
385
- """
386
- file, server_object = run_module.parse_file_path(server_spec)
387
-
388
- logger.debug(
389
- "Installing server",
390
- extra={
391
- "file": str(file),
392
- "server_name": server_name,
393
- "server_object": server_object,
394
- "with_editable": str(with_editable) if with_editable else None,
395
- "with_packages": with_packages,
396
- },
397
- )
398
-
399
- if not claude.get_claude_config_path():
400
- logger.error("Claude app not found")
401
- sys.exit(1)
402
-
403
- # Try to import server to get its name, but fall back to file name if dependencies
404
- # missing
405
- name = server_name
406
- server = None
407
- if not name:
408
- try:
409
- server = run_module.import_server(file, server_object)
410
- name = server.name
411
- except (ImportError, ModuleNotFoundError) as e:
412
- logger.debug(
413
- "Could not import server (likely missing dependencies), using file"
414
- " name",
415
- extra={"error": str(e)},
416
- )
417
- name = file.stem
418
-
419
- # Get server dependencies if available
420
- server_dependencies = getattr(server, "dependencies", []) if server else []
421
- if server_dependencies:
422
- with_packages = list(set(with_packages + server_dependencies))
423
-
424
- # Process environment variables if provided
425
- env_dict: dict[str, str] | None = None
426
- if env_file or env_vars:
427
- env_dict = {}
428
- # Load from .env file if specified
429
- if env_file:
430
- try:
431
- env_dict |= {
432
- k: v
433
- for k, v in dotenv.dotenv_values(env_file).items()
434
- if v is not None
435
- }
436
- except Exception as e:
437
- logger.error(f"Failed to load .env file: {e}")
438
- sys.exit(1)
439
-
440
- # Add command line environment variables
441
- for env_var in env_vars:
442
- key, value = _parse_env_var(env_var)
443
- env_dict[key] = value
444
-
445
- if claude.update_claude_config(
446
- server_spec,
447
- name,
448
- with_editable=with_editable,
449
- with_packages=with_packages,
450
- env_vars=env_dict,
451
- ):
452
- logger.info(f"Successfully installed {name} in Claude app")
453
- else:
454
- logger.error(f"Failed to install {name} in Claude app")
455
- sys.exit(1)
456
-
457
-
458
- @app.command()
459
- def inspect(
460
- server_spec: str = typer.Argument(
461
- ...,
462
- help="Python file to inspect, optionally with :object suffix",
463
- ),
343
+ @app.command
344
+ async def inspect(
345
+ server_spec: str,
346
+ *,
464
347
  output: Annotated[
465
348
  Path,
466
- typer.Option(
467
- "--output",
468
- "-o",
349
+ cyclopts.Parameter(
350
+ name=["--output", "-o"],
469
351
  help="Output file path for the JSON report (default: server-info.json)",
470
352
  ),
471
353
  ] = Path("server-info.json"),
472
354
  ) -> None:
473
- """Inspect a FastMCP server and generate a JSON report.
355
+ """Inspect an MCP server and generate a JSON report.
474
356
 
475
- This command analyzes a FastMCP server (v1.x or v2.x) and generates
476
- a comprehensive JSON report containing information about the server's
477
- name, instructions, version, tools, prompts, resources, templates,
478
- 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.
479
360
 
480
361
  Examples:
481
362
  fastmcp inspect server.py
482
363
  fastmcp inspect server.py -o report.json
483
364
  fastmcp inspect server.py:mcp -o analysis.json
484
365
  fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
485
- """
486
366
 
367
+ Args:
368
+ server_spec: Python file to inspect, optionally with :object suffix
369
+ """
487
370
  # Parse the server specification
488
371
  file, server_object = run_module.parse_file_path(server_spec)
489
372
 
@@ -500,22 +383,8 @@ def inspect(
500
383
  # Import the server
501
384
  server = run_module.import_server(file, server_object)
502
385
 
503
- # Get server information
504
- async def get_info():
505
- return await inspect_fastmcp(server)
506
-
507
- try:
508
- # Try to use existing event loop if available
509
- asyncio.get_running_loop()
510
- # If there's already a loop running, we need to run in a thread
511
- import concurrent.futures
512
-
513
- with concurrent.futures.ThreadPoolExecutor() as executor:
514
- future = executor.submit(asyncio.run, get_info())
515
- info = future.result()
516
- except RuntimeError:
517
- # No running loop, safe to use asyncio.run
518
- info = asyncio.run(get_info())
386
+ # Get server information - using native async support
387
+ info = await inspect_fastmcp(server)
519
388
 
520
389
  info_json = TypeAdapter(FastMCPInfo).dump_json(info, indent=2)
521
390
 
@@ -548,3 +417,11 @@ def inspect(
548
417
  )
549
418
  console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}")
550
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")
@@ -0,0 +1,186 @@
1
+ """Claude Code integration for FastMCP install using Cyclopts."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import cyclopts
9
+ from rich import print
10
+
11
+ from fastmcp.utilities.logging import get_logger
12
+
13
+ from .shared import process_common_args
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ def find_claude_command() -> str | None:
19
+ """Find the Claude Code CLI command."""
20
+ # Check the default installation location
21
+ default_path = Path.home() / ".claude" / "local" / "claude"
22
+ if default_path.exists():
23
+ try:
24
+ result = subprocess.run(
25
+ [str(default_path), "--version"],
26
+ check=True,
27
+ capture_output=True,
28
+ text=True,
29
+ )
30
+ if "Claude Code" in result.stdout:
31
+ return str(default_path)
32
+ except (subprocess.CalledProcessError, FileNotFoundError):
33
+ pass
34
+
35
+ return None
36
+
37
+
38
+ def check_claude_code_available() -> bool:
39
+ """Check if Claude Code CLI is available."""
40
+ return find_claude_command() is not None
41
+
42
+
43
+ def install_claude_code(
44
+ file: Path,
45
+ server_object: str | None,
46
+ name: str,
47
+ *,
48
+ with_editable: Path | None = None,
49
+ with_packages: list[str] | None = None,
50
+ env_vars: dict[str, str] | None = None,
51
+ ) -> bool:
52
+ """Install FastMCP server in Claude Code.
53
+
54
+ Args:
55
+ file: Path to the server file
56
+ server_object: Optional server object name (for :object suffix)
57
+ name: Name for the server in Claude Code
58
+ with_editable: Optional directory to install in editable mode
59
+ with_packages: Optional list of additional packages to install
60
+ env_vars: Optional dictionary of environment variables
61
+
62
+ Returns:
63
+ True if installation was successful, False otherwise
64
+ """
65
+ # Check if Claude Code CLI is available
66
+ claude_cmd = find_claude_command()
67
+ if not claude_cmd:
68
+ print(
69
+ "[red]Claude Code CLI not found.[/red]\n"
70
+ "[blue]Please ensure Claude Code is installed. Try running 'claude --version' to verify.[/blue]"
71
+ )
72
+ return False
73
+
74
+ # Build uv run command
75
+ args = ["run"]
76
+
77
+ # Collect all packages in a set to deduplicate
78
+ packages = {"fastmcp"}
79
+ if with_packages:
80
+ packages.update(pkg for pkg in with_packages if pkg)
81
+
82
+ # Add all packages with --with
83
+ for pkg in sorted(packages):
84
+ args.extend(["--with", pkg])
85
+
86
+ if with_editable:
87
+ args.extend(["--with-editable", str(with_editable)])
88
+
89
+ # Build server spec from parsed components
90
+ if server_object:
91
+ server_spec = f"{file.resolve()}:{server_object}"
92
+ else:
93
+ server_spec = str(file.resolve())
94
+
95
+ # Add fastmcp run command
96
+ args.extend(["fastmcp", "run", server_spec])
97
+
98
+ # Build claude mcp add command
99
+ cmd_parts = [claude_cmd, "mcp", "add"]
100
+
101
+ # Add environment variables if specified (before the name and command)
102
+ if env_vars:
103
+ for key, value in env_vars.items():
104
+ cmd_parts.extend(["-e", f"{key}={value}"])
105
+
106
+ # Add server name and command
107
+ cmd_parts.extend([name, "--"])
108
+ cmd_parts.extend(["uv"] + args)
109
+
110
+ try:
111
+ # Run the claude mcp add command
112
+ subprocess.run(cmd_parts, check=True, capture_output=True, text=True)
113
+ return True
114
+ except subprocess.CalledProcessError as e:
115
+ print(
116
+ f"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e.stderr.strip() if e.stderr else str(e)}[/red]"
117
+ )
118
+ return False
119
+ except Exception as e:
120
+ print(f"[red]Failed to install '[bold]{name}[/bold]' in Claude Code: {e}[/red]")
121
+ return False
122
+
123
+
124
+ def claude_code_command(
125
+ server_spec: str,
126
+ *,
127
+ server_name: Annotated[
128
+ str | None,
129
+ cyclopts.Parameter(
130
+ name=["--server-name", "-n"],
131
+ help="Custom name for the server in Claude Code",
132
+ ),
133
+ ] = None,
134
+ with_editable: Annotated[
135
+ Path | None,
136
+ cyclopts.Parameter(
137
+ name=["--with-editable", "-e"],
138
+ help="Directory with pyproject.toml to install in editable mode",
139
+ ),
140
+ ] = None,
141
+ with_packages: Annotated[
142
+ list[str],
143
+ cyclopts.Parameter(
144
+ "--with",
145
+ help="Additional packages to install",
146
+ negative=False,
147
+ ),
148
+ ] = [],
149
+ env_vars: Annotated[
150
+ list[str],
151
+ cyclopts.Parameter(
152
+ "--env",
153
+ help="Environment variables in KEY=VALUE format",
154
+ negative=False,
155
+ ),
156
+ ] = [],
157
+ env_file: Annotated[
158
+ Path | None,
159
+ cyclopts.Parameter(
160
+ "--env-file",
161
+ help="Load environment variables from .env file",
162
+ ),
163
+ ] = None,
164
+ ) -> None:
165
+ """Install an MCP server in Claude Code.
166
+
167
+ Args:
168
+ server_spec: Python file to install, optionally with :object suffix
169
+ """
170
+ file, server_object, name, packages, env_dict = process_common_args(
171
+ server_spec, server_name, with_packages, env_vars, env_file
172
+ )
173
+
174
+ success = install_claude_code(
175
+ file=file,
176
+ server_object=server_object,
177
+ name=name,
178
+ with_editable=with_editable,
179
+ with_packages=packages,
180
+ env_vars=env_dict,
181
+ )
182
+
183
+ if success:
184
+ print(f"[green]Successfully installed '{name}' in Claude Code[/green]")
185
+ else:
186
+ sys.exit(1)