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.
Files changed (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/__init__.py CHANGED
@@ -48,9 +48,9 @@ def __getattr__(name: str):
48
48
 
49
49
 
50
50
  __all__ = [
51
- "FastMCP",
51
+ "Client",
52
52
  "Context",
53
+ "FastMCP",
53
54
  "client",
54
- "Client",
55
55
  "settings",
56
56
  ]
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]] + args
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 with shell=True on Windows
281
- shell = sys.platform == "win32"
278
+ # Run the MCP Inspector command
282
279
  process = subprocess.run(
283
- [npx_cmd, inspector_cmd] + uv_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
- # Use uv run subprocess - always use run_with_uv which handles output correctly
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
- run_module.run_with_uv(
472
- server_spec=server_spec,
473
- python_version=config.environment.python,
474
- with_packages=config.environment.dependencies,
475
- with_requirements=Path(config.environment.requirements)
476
- if config.environment.requirements
477
- else None,
478
- project=Path(config.environment.project)
479
- if config.environment.project
480
- else None,
481
- transport=final_transport,
482
- host=final_host,
483
- port=final_port,
484
- path=final_path,
485
- log_level=final_log_level,
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(1)
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 no_banner,
525
+ show_banner=not final_no_banner,
510
526
  skip_source=skip_source,
511
527
  )
512
528
  except Exception as e:
513
- logger.error(
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.error(
769
+ logger.exception(
750
770
  f"Failed to inspect server: {e}",
751
771
  extra={
752
772
  "server_spec": server_spec,
@@ -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=deduplicated_packages,
122
- requirements=str(with_requirements) if with_requirements else None,
123
- project=str(project) if project else None,
124
- editable=[str(p) for p in with_editable] if with_editable else None,
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 (before the name and command)
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.extend([name, "--"])
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=deduplicated_packages,
87
- requirements=str(with_requirements) if with_requirements else None,
88
- project=str(project) if project else None,
89
- editable=[str(p) for p in with_editable] if with_editable else None,
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:
@@ -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
- deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_b64}"
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
- subprocess.run(
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=deduplicated_packages,
121
- requirements=str(with_requirements.resolve()) if with_requirements else None,
122
- project=str(project.resolve()) if project else None,
123
- editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
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=deduplicated_packages,
199
- requirements=str(with_requirements.resolve()) if with_requirements else None,
200
- project=str(project.resolve()) if project else None,
201
- editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
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)
@@ -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=deduplicated_packages,
62
- requirements=str(with_requirements) if with_requirements else None,
63
- project=str(project) if project else None,
64
- editable=[str(p) for p in with_editable] if with_editable else None,
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: