fastmcp 2.12.1__py3-none-any.whl → 2.13.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/__init__.py
CHANGED
fastmcp/cli/cli.py
CHANGED
|
@@ -20,6 +20,7 @@ import fastmcp
|
|
|
20
20
|
from fastmcp.cli import run as run_module
|
|
21
21
|
from fastmcp.cli.install import install_app
|
|
22
22
|
from fastmcp.server.server import FastMCP
|
|
23
|
+
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
|
23
24
|
from fastmcp.utilities.inspect import (
|
|
24
25
|
InspectFormat,
|
|
25
26
|
format_info,
|
|
@@ -45,9 +46,7 @@ def _get_npx_command():
|
|
|
45
46
|
# Try both npx.cmd and npx.exe on Windows
|
|
46
47
|
for cmd in ["npx.cmd", "npx.exe", "npx"]:
|
|
47
48
|
try:
|
|
48
|
-
subprocess.run(
|
|
49
|
-
[cmd, "--version"], check=True, capture_output=True, shell=True
|
|
50
|
-
)
|
|
49
|
+
subprocess.run([cmd, "--version"], check=True, capture_output=True)
|
|
51
50
|
return cmd
|
|
52
51
|
except subprocess.CalledProcessError:
|
|
53
52
|
continue
|
|
@@ -79,7 +78,7 @@ def with_argv(args: list[str] | None):
|
|
|
79
78
|
original = sys.argv[:]
|
|
80
79
|
try:
|
|
81
80
|
# Preserve the script name (sys.argv[0]) and replace the rest
|
|
82
|
-
sys.argv = [sys.argv[0]
|
|
81
|
+
sys.argv = [sys.argv[0], *args]
|
|
83
82
|
yield
|
|
84
83
|
finally:
|
|
85
84
|
sys.argv = original
|
|
@@ -193,7 +192,6 @@ async def dev(
|
|
|
193
192
|
Args:
|
|
194
193
|
server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
|
|
195
194
|
"""
|
|
196
|
-
from fastmcp.utilities.cli import load_and_merge_config
|
|
197
195
|
|
|
198
196
|
try:
|
|
199
197
|
# Load config and apply CLI overrides
|
|
@@ -277,12 +275,10 @@ async def dev(
|
|
|
277
275
|
# Set marker to prevent infinite loops when subprocess calls FastMCP
|
|
278
276
|
env = dict(os.environ.items()) | env_vars | {"FASTMCP_UV_SPAWNED": "1"}
|
|
279
277
|
|
|
280
|
-
# Run the MCP Inspector command
|
|
281
|
-
shell = sys.platform == "win32"
|
|
278
|
+
# Run the MCP Inspector command
|
|
282
279
|
process = subprocess.run(
|
|
283
|
-
[npx_cmd, inspector_cmd
|
|
280
|
+
[npx_cmd, inspector_cmd, *uv_cmd],
|
|
284
281
|
check=True,
|
|
285
|
-
shell=shell,
|
|
286
282
|
env=env,
|
|
287
283
|
)
|
|
288
284
|
sys.exit(process.returncode)
|
|
@@ -415,7 +411,6 @@ async def run(
|
|
|
415
411
|
Args:
|
|
416
412
|
server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
|
|
417
413
|
"""
|
|
418
|
-
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
|
419
414
|
|
|
420
415
|
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
|
421
416
|
if skip_env or is_already_in_uv_subprocess():
|
|
@@ -446,6 +441,9 @@ async def run(
|
|
|
446
441
|
final_path = path or config.deployment.path
|
|
447
442
|
final_log_level = log_level or config.deployment.log_level
|
|
448
443
|
final_server_args = server_args or config.deployment.args
|
|
444
|
+
# Use CLI override if provided, otherwise use settings
|
|
445
|
+
# no_banner CLI flag overrides the show_cli_banner setting
|
|
446
|
+
final_no_banner = no_banner if no_banner else not fastmcp.settings.show_cli_banner
|
|
449
447
|
|
|
450
448
|
logger.debug(
|
|
451
449
|
"Running server or client",
|
|
@@ -466,35 +464,53 @@ async def run(
|
|
|
466
464
|
needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env
|
|
467
465
|
|
|
468
466
|
if needs_uv:
|
|
469
|
-
#
|
|
467
|
+
# Build the inner fastmcp command
|
|
468
|
+
inner_cmd = ["fastmcp", "run", server_spec]
|
|
469
|
+
|
|
470
|
+
# Add transport options to the inner command
|
|
471
|
+
if final_transport:
|
|
472
|
+
inner_cmd.extend(["--transport", final_transport])
|
|
473
|
+
# Only add HTTP-specific options for non-stdio transports
|
|
474
|
+
if final_transport != "stdio":
|
|
475
|
+
if final_host:
|
|
476
|
+
inner_cmd.extend(["--host", final_host])
|
|
477
|
+
if final_port:
|
|
478
|
+
inner_cmd.extend(["--port", str(final_port)])
|
|
479
|
+
if final_path:
|
|
480
|
+
inner_cmd.extend(["--path", final_path])
|
|
481
|
+
if final_log_level:
|
|
482
|
+
inner_cmd.extend(["--log-level", final_log_level])
|
|
483
|
+
if final_no_banner:
|
|
484
|
+
inner_cmd.append("--no-banner")
|
|
485
|
+
# Add skip-env flag to prevent infinite recursion
|
|
486
|
+
inner_cmd.append("--skip-env")
|
|
487
|
+
|
|
488
|
+
# Add server args if any
|
|
489
|
+
if final_server_args:
|
|
490
|
+
inner_cmd.append("--")
|
|
491
|
+
inner_cmd.extend(final_server_args)
|
|
492
|
+
|
|
493
|
+
# Build the full uv command using the config's environment
|
|
494
|
+
cmd = config.environment.build_command(inner_cmd)
|
|
495
|
+
|
|
496
|
+
# Set marker to prevent infinite loops when subprocess calls FastMCP again
|
|
497
|
+
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
|
498
|
+
|
|
499
|
+
# Run the command
|
|
500
|
+
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
470
501
|
try:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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,
|
|
486
|
-
show_banner=not no_banner,
|
|
487
|
-
editable=config.environment.editable,
|
|
488
|
-
)
|
|
489
|
-
except Exception as e:
|
|
490
|
-
logger.error(
|
|
502
|
+
process = subprocess.run(cmd, check=True, env=env)
|
|
503
|
+
sys.exit(process.returncode)
|
|
504
|
+
except subprocess.CalledProcessError as e:
|
|
505
|
+
logger.exception(
|
|
491
506
|
f"Failed to run: {e}",
|
|
492
507
|
extra={
|
|
493
508
|
"server_spec": server_spec,
|
|
494
509
|
"error": str(e),
|
|
510
|
+
"returncode": e.returncode,
|
|
495
511
|
},
|
|
496
512
|
)
|
|
497
|
-
sys.exit(
|
|
513
|
+
sys.exit(e.returncode)
|
|
498
514
|
else:
|
|
499
515
|
# Use direct import for backwards compatibility
|
|
500
516
|
try:
|
|
@@ -506,11 +522,11 @@ async def run(
|
|
|
506
522
|
path=final_path,
|
|
507
523
|
log_level=final_log_level,
|
|
508
524
|
server_args=list(final_server_args) if final_server_args else [],
|
|
509
|
-
show_banner=not
|
|
525
|
+
show_banner=not final_no_banner,
|
|
510
526
|
skip_source=skip_source,
|
|
511
527
|
)
|
|
512
528
|
except Exception as e:
|
|
513
|
-
logger.
|
|
529
|
+
logger.exception(
|
|
514
530
|
f"Failed to run: {e}",
|
|
515
531
|
extra={
|
|
516
532
|
"server_spec": server_spec,
|
|
@@ -598,7 +614,6 @@ async def inspect(
|
|
|
598
614
|
Args:
|
|
599
615
|
server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
|
|
600
616
|
"""
|
|
601
|
-
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
|
602
617
|
|
|
603
618
|
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
|
604
619
|
if skip_env or is_already_in_uv_subprocess():
|
|
@@ -641,6 +656,7 @@ async def inspect(
|
|
|
641
656
|
"fastmcp",
|
|
642
657
|
"inspect",
|
|
643
658
|
server_spec,
|
|
659
|
+
"--skip-env", # Prevent infinite recursion
|
|
644
660
|
]
|
|
645
661
|
|
|
646
662
|
# Add format and output flags if specified
|
|
@@ -697,6 +713,10 @@ async def inspect(
|
|
|
697
713
|
console.print(f" Name: {info.name}")
|
|
698
714
|
if info.version:
|
|
699
715
|
console.print(f" Version: {info.version}")
|
|
716
|
+
if info.website_url:
|
|
717
|
+
console.print(f" Website: {info.website_url}")
|
|
718
|
+
if info.icons:
|
|
719
|
+
console.print(f" Icons: {len(info.icons)}")
|
|
700
720
|
console.print(f" Generation: {info.server_generation}")
|
|
701
721
|
if info.instructions:
|
|
702
722
|
console.print(f" Instructions: {info.instructions}")
|
|
@@ -746,7 +766,7 @@ async def inspect(
|
|
|
746
766
|
console.print(formatted_json.decode("utf-8"))
|
|
747
767
|
|
|
748
768
|
except Exception as e:
|
|
749
|
-
logger.
|
|
769
|
+
logger.exception(
|
|
750
770
|
f"Failed to inspect server: {e}",
|
|
751
771
|
extra={
|
|
752
772
|
"server_spec": server_spec,
|
fastmcp/cli/install/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ import cyclopts
|
|
|
5
5
|
from .claude_code import claude_code_command
|
|
6
6
|
from .claude_desktop import claude_desktop_command
|
|
7
7
|
from .cursor import cursor_command
|
|
8
|
+
from .gemini_cli import gemini_cli_command
|
|
8
9
|
from .mcp_json import mcp_json_command
|
|
9
10
|
|
|
10
11
|
# Create a cyclopts app for install subcommands
|
|
@@ -17,4 +18,5 @@ install_app = cyclopts.App(
|
|
|
17
18
|
install_app.command(claude_code_command, name="claude-code")
|
|
18
19
|
install_app.command(claude_desktop_command, name="claude-desktop")
|
|
19
20
|
install_app.command(cursor_command, name="cursor")
|
|
21
|
+
install_app.command(gemini_cli_command, name="gemini-cli")
|
|
20
22
|
install_app.command(mcp_json_command, name="mcp-json")
|
|
@@ -107,21 +107,12 @@ def install_claude_code(
|
|
|
107
107
|
)
|
|
108
108
|
return False
|
|
109
109
|
|
|
110
|
-
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
|
|
111
|
-
deduplicated_packages = None
|
|
112
|
-
if with_packages:
|
|
113
|
-
deduplicated = list(dict.fromkeys(with_packages))
|
|
114
|
-
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
|
|
115
|
-
if not deduplicated_packages:
|
|
116
|
-
deduplicated_packages = None
|
|
117
|
-
|
|
118
|
-
# Build uv run command using Environment.build_uv_run_command()
|
|
119
110
|
env_config = UVEnvironment(
|
|
120
111
|
python=python_version,
|
|
121
|
-
dependencies=
|
|
122
|
-
requirements=
|
|
123
|
-
project=
|
|
124
|
-
editable=
|
|
112
|
+
dependencies=(with_packages or []) + ["fastmcp"],
|
|
113
|
+
requirements=with_requirements,
|
|
114
|
+
project=project,
|
|
115
|
+
editable=with_editable,
|
|
125
116
|
)
|
|
126
117
|
|
|
127
118
|
# Build server spec from parsed components
|
|
@@ -134,15 +125,15 @@ def install_claude_code(
|
|
|
134
125
|
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
|
135
126
|
|
|
136
127
|
# Build claude mcp add command
|
|
137
|
-
cmd_parts = [claude_cmd, "mcp", "add"]
|
|
128
|
+
cmd_parts = [claude_cmd, "mcp", "add", name]
|
|
138
129
|
|
|
139
|
-
# Add environment variables if specified
|
|
130
|
+
# Add environment variables if specified
|
|
140
131
|
if env_vars:
|
|
141
132
|
for key, value in env_vars.items():
|
|
142
133
|
cmd_parts.extend(["-e", f"{key}={value}"])
|
|
143
134
|
|
|
144
135
|
# Add server name and command
|
|
145
|
-
cmd_parts.
|
|
136
|
+
cmd_parts.append("--")
|
|
146
137
|
cmd_parts.extend(full_command)
|
|
147
138
|
|
|
148
139
|
try:
|
|
@@ -73,20 +73,12 @@ def install_claude_desktop(
|
|
|
73
73
|
|
|
74
74
|
config_file = config_dir / "claude_desktop_config.json"
|
|
75
75
|
|
|
76
|
-
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
|
|
77
|
-
deduplicated_packages = None
|
|
78
|
-
if with_packages:
|
|
79
|
-
deduplicated = list(dict.fromkeys(with_packages))
|
|
80
|
-
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
|
|
81
|
-
if not deduplicated_packages:
|
|
82
|
-
deduplicated_packages = None
|
|
83
|
-
|
|
84
76
|
env_config = UVEnvironment(
|
|
85
77
|
python=python_version,
|
|
86
|
-
dependencies=
|
|
87
|
-
requirements=
|
|
88
|
-
project=
|
|
89
|
-
editable=
|
|
78
|
+
dependencies=(with_packages or []) + ["fastmcp"],
|
|
79
|
+
requirements=with_requirements,
|
|
80
|
+
project=project,
|
|
81
|
+
editable=with_editable,
|
|
90
82
|
)
|
|
91
83
|
# Build server spec from parsed components
|
|
92
84
|
if server_object:
|
fastmcp/cli/install/cursor.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Cursor integration for FastMCP install using Cyclopts."""
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import os
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Annotated
|
|
9
|
+
from urllib.parse import quote, urlparse
|
|
8
10
|
|
|
9
11
|
import cyclopts
|
|
10
12
|
from rich import print
|
|
@@ -36,8 +38,9 @@ def generate_cursor_deeplink(
|
|
|
36
38
|
config_json = server_config.model_dump_json(exclude_none=True)
|
|
37
39
|
config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode()
|
|
38
40
|
|
|
39
|
-
# Generate the deeplink URL
|
|
40
|
-
|
|
41
|
+
# Generate the deeplink URL with properly encoded server name
|
|
42
|
+
encoded_name = quote(server_name, safe="")
|
|
43
|
+
deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={encoded_name}&config={config_b64}"
|
|
41
44
|
|
|
42
45
|
return deeplink
|
|
43
46
|
|
|
@@ -51,17 +54,20 @@ def open_deeplink(deeplink: str) -> bool:
|
|
|
51
54
|
Returns:
|
|
52
55
|
True if the command succeeded, False otherwise
|
|
53
56
|
"""
|
|
57
|
+
parsed = urlparse(deeplink)
|
|
58
|
+
if parsed.scheme != "cursor":
|
|
59
|
+
logger.warning(f"Invalid deeplink scheme: {parsed.scheme}")
|
|
60
|
+
return False
|
|
61
|
+
|
|
54
62
|
try:
|
|
55
63
|
if sys.platform == "darwin": # macOS
|
|
56
64
|
subprocess.run(["open", deeplink], check=True, capture_output=True)
|
|
57
65
|
elif sys.platform == "win32": # Windows
|
|
58
|
-
|
|
59
|
-
["start", deeplink], shell=True, check=True, capture_output=True
|
|
60
|
-
)
|
|
66
|
+
os.startfile(deeplink)
|
|
61
67
|
else: # Linux and others
|
|
62
68
|
subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
|
|
63
69
|
return True
|
|
64
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
70
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
65
71
|
return False
|
|
66
72
|
|
|
67
73
|
|
|
@@ -107,20 +113,12 @@ def install_cursor_workspace(
|
|
|
107
113
|
|
|
108
114
|
config_file = cursor_dir / "mcp.json"
|
|
109
115
|
|
|
110
|
-
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
|
|
111
|
-
deduplicated_packages = None
|
|
112
|
-
if with_packages:
|
|
113
|
-
deduplicated = list(dict.fromkeys(with_packages))
|
|
114
|
-
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
|
|
115
|
-
if not deduplicated_packages:
|
|
116
|
-
deduplicated_packages = None
|
|
117
|
-
|
|
118
116
|
env_config = UVEnvironment(
|
|
119
117
|
python=python_version,
|
|
120
|
-
dependencies=
|
|
121
|
-
requirements=
|
|
122
|
-
project=
|
|
123
|
-
editable=
|
|
118
|
+
dependencies=(with_packages or []) + ["fastmcp"],
|
|
119
|
+
requirements=with_requirements,
|
|
120
|
+
project=project,
|
|
121
|
+
editable=with_editable,
|
|
124
122
|
)
|
|
125
123
|
# Build server spec from parsed components
|
|
126
124
|
if server_object:
|
|
@@ -185,20 +183,12 @@ def install_cursor(
|
|
|
185
183
|
True if installation was successful, False otherwise
|
|
186
184
|
"""
|
|
187
185
|
|
|
188
|
-
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
|
|
189
|
-
deduplicated_packages = None
|
|
190
|
-
if with_packages:
|
|
191
|
-
deduplicated = list(dict.fromkeys(with_packages))
|
|
192
|
-
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
|
|
193
|
-
if not deduplicated_packages:
|
|
194
|
-
deduplicated_packages = None
|
|
195
|
-
|
|
196
186
|
env_config = UVEnvironment(
|
|
197
187
|
python=python_version,
|
|
198
|
-
dependencies=
|
|
199
|
-
requirements=
|
|
200
|
-
project=
|
|
201
|
-
editable=
|
|
188
|
+
dependencies=(with_packages or []) + ["fastmcp"],
|
|
189
|
+
requirements=with_requirements,
|
|
190
|
+
project=project,
|
|
191
|
+
editable=with_editable,
|
|
202
192
|
)
|
|
203
193
|
# Build server spec from parsed components
|
|
204
194
|
if server_object:
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Gemini CLI integration for FastMCP install using Cyclopts."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import cyclopts
|
|
10
|
+
from rich import print
|
|
11
|
+
|
|
12
|
+
from fastmcp.utilities.logging import get_logger
|
|
13
|
+
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
14
|
+
|
|
15
|
+
from .shared import process_common_args
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_gemini_command() -> str | None:
|
|
21
|
+
"""Find the Gemini CLI command."""
|
|
22
|
+
# First try shutil.which() in case it's a real executable in PATH
|
|
23
|
+
gemini_in_path = shutil.which("gemini")
|
|
24
|
+
if gemini_in_path:
|
|
25
|
+
try:
|
|
26
|
+
# If 'gemini --version' fails, it's not the correct path
|
|
27
|
+
subprocess.run(
|
|
28
|
+
[gemini_in_path, "--version"],
|
|
29
|
+
check=True,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
)
|
|
32
|
+
return gemini_in_path
|
|
33
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# Check common installation locations (aliases don't work with subprocess)
|
|
37
|
+
potential_paths = [
|
|
38
|
+
# Default Gemini CLI installation location (after migration)
|
|
39
|
+
Path.home() / ".gemini" / "local" / "gemini",
|
|
40
|
+
# npm global installation on macOS/Linux (default)
|
|
41
|
+
Path("/usr/local/bin/gemini"),
|
|
42
|
+
# npm global installation with custom prefix
|
|
43
|
+
Path.home() / ".npm-global" / "bin" / "gemini",
|
|
44
|
+
# Homebrew installation on macOS
|
|
45
|
+
Path("/opt/homebrew/bin/gemini"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
for path in potential_paths:
|
|
49
|
+
if path.exists():
|
|
50
|
+
# If 'gemini --version' fails, it's not the correct path
|
|
51
|
+
try:
|
|
52
|
+
subprocess.run(
|
|
53
|
+
[str(path), "--version"],
|
|
54
|
+
check=True,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
)
|
|
57
|
+
return str(path)
|
|
58
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_gemini_cli_available() -> bool:
|
|
65
|
+
"""Check if Gemini CLI is available."""
|
|
66
|
+
return find_gemini_command() is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def install_gemini_cli(
|
|
70
|
+
file: Path,
|
|
71
|
+
server_object: str | None,
|
|
72
|
+
name: str,
|
|
73
|
+
*,
|
|
74
|
+
with_editable: list[Path] | None = None,
|
|
75
|
+
with_packages: list[str] | None = None,
|
|
76
|
+
env_vars: dict[str, str] | None = None,
|
|
77
|
+
python_version: str | None = None,
|
|
78
|
+
with_requirements: Path | None = None,
|
|
79
|
+
project: Path | None = None,
|
|
80
|
+
) -> bool:
|
|
81
|
+
"""Install FastMCP server in Gemini CLI.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
file: Path to the server file
|
|
85
|
+
server_object: Optional server object name (for :object suffix)
|
|
86
|
+
name: Name for the server in Gemini CLI
|
|
87
|
+
with_editable: Optional list of directories to install in editable mode
|
|
88
|
+
with_packages: Optional list of additional packages to install
|
|
89
|
+
env_vars: Optional dictionary of environment variables
|
|
90
|
+
python_version: Optional Python version to use
|
|
91
|
+
with_requirements: Optional requirements file to install from
|
|
92
|
+
project: Optional project directory to run within
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if installation was successful, False otherwise
|
|
96
|
+
"""
|
|
97
|
+
# Check if Gemini CLI is available
|
|
98
|
+
gemini_cmd = find_gemini_command()
|
|
99
|
+
if not gemini_cmd:
|
|
100
|
+
print(
|
|
101
|
+
"[red]Gemini CLI not found.[/red]\n"
|
|
102
|
+
"[blue]Please ensure Gemini CLI is installed. Try running 'gemini --version' to verify.[/blue]\n"
|
|
103
|
+
"[blue]You can install it using 'npm install -g @google/gemini-cli'.[/blue]\n"
|
|
104
|
+
)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
env_config = UVEnvironment(
|
|
108
|
+
python=python_version,
|
|
109
|
+
dependencies=(with_packages or []) + ["fastmcp"],
|
|
110
|
+
requirements=with_requirements,
|
|
111
|
+
project=project,
|
|
112
|
+
editable=with_editable,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Build server spec from parsed components
|
|
116
|
+
if server_object:
|
|
117
|
+
server_spec = f"{file.resolve()}:{server_object}"
|
|
118
|
+
else:
|
|
119
|
+
server_spec = str(file.resolve())
|
|
120
|
+
|
|
121
|
+
# Build the full command
|
|
122
|
+
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
|
123
|
+
|
|
124
|
+
# Build gemini mcp add command
|
|
125
|
+
cmd_parts = [gemini_cmd, "mcp", "add"]
|
|
126
|
+
|
|
127
|
+
# Add environment variables if specified (before the name and command)
|
|
128
|
+
if env_vars:
|
|
129
|
+
for key, value in env_vars.items():
|
|
130
|
+
cmd_parts.extend(["-e", f"{key}={value}"])
|
|
131
|
+
|
|
132
|
+
# Add server name and command
|
|
133
|
+
cmd_parts.extend([name, full_command[0], "--"])
|
|
134
|
+
cmd_parts.extend(full_command[1:])
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Run the gemini mcp add command
|
|
138
|
+
subprocess.run(cmd_parts, check=True, capture_output=True, text=True)
|
|
139
|
+
return True
|
|
140
|
+
except subprocess.CalledProcessError as e:
|
|
141
|
+
print(
|
|
142
|
+
f"[red]Failed to install '[bold]{name}[/bold]' in Gemini CLI: {e.stderr.strip() if e.stderr else str(e)}[/red]"
|
|
143
|
+
)
|
|
144
|
+
return False
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"[red]Failed to install '[bold]{name}[/bold]' in Gemini CLI: {e}[/red]")
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def gemini_cli_command(
|
|
151
|
+
server_spec: str,
|
|
152
|
+
*,
|
|
153
|
+
server_name: Annotated[
|
|
154
|
+
str | None,
|
|
155
|
+
cyclopts.Parameter(
|
|
156
|
+
name=["--name", "-n"],
|
|
157
|
+
help="Custom name for the server in Gemini CLI",
|
|
158
|
+
),
|
|
159
|
+
] = None,
|
|
160
|
+
with_editable: Annotated[
|
|
161
|
+
list[Path] | None,
|
|
162
|
+
cyclopts.Parameter(
|
|
163
|
+
"--with-editable",
|
|
164
|
+
help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
|
|
165
|
+
negative="",
|
|
166
|
+
),
|
|
167
|
+
] = None,
|
|
168
|
+
with_packages: Annotated[
|
|
169
|
+
list[str] | None,
|
|
170
|
+
cyclopts.Parameter(
|
|
171
|
+
"--with",
|
|
172
|
+
help="Additional packages to install (can be used multiple times)",
|
|
173
|
+
negative="",
|
|
174
|
+
),
|
|
175
|
+
] = None,
|
|
176
|
+
env_vars: Annotated[
|
|
177
|
+
list[str] | None,
|
|
178
|
+
cyclopts.Parameter(
|
|
179
|
+
"--env",
|
|
180
|
+
help="Environment variables in KEY=VALUE format (can be used multiple times)",
|
|
181
|
+
negative="",
|
|
182
|
+
),
|
|
183
|
+
] = None,
|
|
184
|
+
env_file: Annotated[
|
|
185
|
+
Path | None,
|
|
186
|
+
cyclopts.Parameter(
|
|
187
|
+
"--env-file",
|
|
188
|
+
help="Load environment variables from .env file",
|
|
189
|
+
),
|
|
190
|
+
] = None,
|
|
191
|
+
python: Annotated[
|
|
192
|
+
str | None,
|
|
193
|
+
cyclopts.Parameter(
|
|
194
|
+
"--python",
|
|
195
|
+
help="Python version to use (e.g., 3.10, 3.11)",
|
|
196
|
+
),
|
|
197
|
+
] = None,
|
|
198
|
+
with_requirements: Annotated[
|
|
199
|
+
Path | None,
|
|
200
|
+
cyclopts.Parameter(
|
|
201
|
+
"--with-requirements",
|
|
202
|
+
help="Requirements file to install dependencies from",
|
|
203
|
+
),
|
|
204
|
+
] = None,
|
|
205
|
+
project: Annotated[
|
|
206
|
+
Path | None,
|
|
207
|
+
cyclopts.Parameter(
|
|
208
|
+
"--project",
|
|
209
|
+
help="Run the command within the given project directory",
|
|
210
|
+
),
|
|
211
|
+
] = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Install an MCP server in Gemini CLI.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
server_spec: Python file to install, optionally with :object suffix
|
|
217
|
+
"""
|
|
218
|
+
# Convert None to empty lists for list parameters
|
|
219
|
+
with_editable = with_editable or []
|
|
220
|
+
with_packages = with_packages or []
|
|
221
|
+
env_vars = env_vars or []
|
|
222
|
+
file, server_object, name, packages, env_dict = await process_common_args(
|
|
223
|
+
server_spec, server_name, with_packages, env_vars, env_file
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
success = install_gemini_cli(
|
|
227
|
+
file=file,
|
|
228
|
+
server_object=server_object,
|
|
229
|
+
name=name,
|
|
230
|
+
with_editable=with_editable,
|
|
231
|
+
with_packages=packages,
|
|
232
|
+
env_vars=env_dict,
|
|
233
|
+
python_version=python,
|
|
234
|
+
with_requirements=with_requirements,
|
|
235
|
+
project=project,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if success:
|
|
239
|
+
print(f"[green]Successfully installed '{name}' in Gemini CLI")
|
|
240
|
+
else:
|
|
241
|
+
sys.exit(1)
|
fastmcp/cli/install/mcp_json.py
CHANGED
|
@@ -48,20 +48,12 @@ def install_mcp_json(
|
|
|
48
48
|
True if generation was successful, False otherwise
|
|
49
49
|
"""
|
|
50
50
|
try:
|
|
51
|
-
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
|
|
52
|
-
deduplicated_packages = None
|
|
53
|
-
if with_packages:
|
|
54
|
-
deduplicated = list(dict.fromkeys(with_packages))
|
|
55
|
-
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
|
|
56
|
-
if not deduplicated_packages:
|
|
57
|
-
deduplicated_packages = None
|
|
58
|
-
|
|
59
51
|
env_config = UVEnvironment(
|
|
60
52
|
python=python_version,
|
|
61
|
-
dependencies=
|
|
62
|
-
requirements=
|
|
63
|
-
project=
|
|
64
|
-
editable=
|
|
53
|
+
dependencies=(with_packages or []) + ["fastmcp"],
|
|
54
|
+
requirements=with_requirements,
|
|
55
|
+
project=project,
|
|
56
|
+
editable=with_editable,
|
|
65
57
|
)
|
|
66
58
|
# Build server spec from parsed components
|
|
67
59
|
if server_object:
|