fastmcp 2.10.2__py3-none-any.whl → 2.10.4__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 +102 -225
- fastmcp/cli/install/__init__.py +20 -0
- fastmcp/cli/install/claude_code.py +186 -0
- fastmcp/cli/install/claude_desktop.py +186 -0
- fastmcp/cli/install/cursor.py +196 -0
- fastmcp/cli/install/mcp_config.py +165 -0
- fastmcp/cli/install/shared.py +85 -0
- fastmcp/cli/run.py +13 -4
- fastmcp/client/client.py +230 -124
- fastmcp/client/transports.py +1 -1
- fastmcp/mcp_config.py +282 -0
- fastmcp/prompts/prompt.py +2 -4
- fastmcp/resources/resource.py +2 -2
- fastmcp/resources/template.py +1 -1
- fastmcp/server/openapi.py +40 -9
- fastmcp/server/proxy.py +101 -48
- fastmcp/server/server.py +32 -3
- fastmcp/tools/tool.py +3 -2
- fastmcp/tools/tool_transform.py +5 -6
- fastmcp/utilities/json_schema.py +14 -3
- fastmcp/utilities/openapi.py +92 -0
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/METADATA +4 -3
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/RECORD +26 -20
- fastmcp/utilities/mcp_config.py +0 -103
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.2.dist-info → fastmcp-2.10.4.dist-info}/licenses/LICENSE +0 -0
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
|
|
14
|
-
import
|
|
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 =
|
|
28
|
+
app = cyclopts.App(
|
|
31
29
|
name="fastmcp",
|
|
32
|
-
help="FastMCP
|
|
33
|
-
|
|
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(
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
244
|
+
@app.command
|
|
234
245
|
def run(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
...,
|
|
238
|
-
help="Python file, object specification (file:obj), or URL",
|
|
239
|
-
),
|
|
246
|
+
server_spec: str,
|
|
247
|
+
*,
|
|
240
248
|
transport: Annotated[
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"--transport",
|
|
244
|
-
"
|
|
245
|
-
help="Transport protocol to use (stdio, http, or sse)",
|
|
249
|
+
run_module.TransportType | 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
+
path: Annotated[
|
|
264
270
|
str | None,
|
|
265
|
-
|
|
266
|
-
"--
|
|
267
|
-
"
|
|
268
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
283
|
-
2. Import approach: server.py:app - imports and runs the specified server object
|
|
284
|
-
3. URL approach: http://server-url - connects to a remote server and creates a proxy
|
|
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
|
-
|
|
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
|
|
329
|
-
server_spec: str
|
|
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
|
-
|
|
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
|
|
355
|
+
"""Inspect an MCP server and generate a JSON report.
|
|
474
356
|
|
|
475
|
-
This command analyzes
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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)
|