fastmcp 2.12.2__py3-none-any.whl → 2.12.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.
Files changed (37) hide show
  1. fastmcp/cli/claude.py +1 -10
  2. fastmcp/cli/cli.py +45 -25
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +1 -10
  5. fastmcp/cli/install/claude_desktop.py +1 -9
  6. fastmcp/cli/install/cursor.py +2 -18
  7. fastmcp/cli/install/gemini_cli.py +242 -0
  8. fastmcp/cli/install/mcp_json.py +1 -9
  9. fastmcp/cli/run.py +0 -84
  10. fastmcp/client/auth/oauth.py +1 -1
  11. fastmcp/client/client.py +6 -6
  12. fastmcp/client/elicitation.py +6 -1
  13. fastmcp/client/transports.py +1 -1
  14. fastmcp/contrib/component_manager/component_service.py +1 -1
  15. fastmcp/contrib/mcp_mixin/README.md +1 -1
  16. fastmcp/contrib/mcp_mixin/mcp_mixin.py +41 -6
  17. fastmcp/experimental/utilities/openapi/director.py +8 -1
  18. fastmcp/prompts/prompt.py +10 -8
  19. fastmcp/resources/resource.py +14 -11
  20. fastmcp/resources/template.py +12 -10
  21. fastmcp/server/auth/auth.py +7 -1
  22. fastmcp/server/auth/oauth_proxy.py +51 -11
  23. fastmcp/server/context.py +10 -10
  24. fastmcp/server/dependencies.py +18 -5
  25. fastmcp/server/server.py +7 -5
  26. fastmcp/settings.py +15 -1
  27. fastmcp/tools/tool.py +101 -85
  28. fastmcp/tools/tool_transform.py +1 -1
  29. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +4 -39
  30. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  31. fastmcp/utilities/mcp_server_config/v1/schema.json +2 -1
  32. fastmcp/utilities/types.py +9 -5
  33. {fastmcp-2.12.2.dist-info → fastmcp-2.12.3.dist-info}/METADATA +2 -2
  34. {fastmcp-2.12.2.dist-info → fastmcp-2.12.3.dist-info}/RECORD +37 -36
  35. {fastmcp-2.12.2.dist-info → fastmcp-2.12.3.dist-info}/WHEEL +0 -0
  36. {fastmcp-2.12.2.dist-info → fastmcp-2.12.3.dist-info}/entry_points.txt +0 -0
  37. {fastmcp-2.12.2.dist-info → fastmcp-2.12.3.dist-info}/licenses/LICENSE +0 -0
fastmcp/cli/claude.py CHANGED
@@ -90,17 +90,8 @@ def update_claude_config(
90
90
  else:
91
91
  env_vars = existing_env
92
92
 
93
- # Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
94
- deduplicated_packages = None
95
- if with_packages:
96
- deduplicated = list(dict.fromkeys(with_packages))
97
- deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
98
- if not deduplicated_packages:
99
- deduplicated_packages = None
100
-
101
- # Build uv run command using Environment.build_uv_run_command()
102
93
  env_config = UVEnvironment(
103
- dependencies=deduplicated_packages,
94
+ dependencies=(with_packages or []) + ["fastmcp"],
104
95
  editable=[str(p) for p in with_editable] if with_editable else None,
105
96
  )
106
97
 
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,
@@ -193,7 +194,6 @@ async def dev(
193
194
  Args:
194
195
  server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
195
196
  """
196
- from fastmcp.utilities.cli import load_and_merge_config
197
197
 
198
198
  try:
199
199
  # Load config and apply CLI overrides
@@ -415,7 +415,6 @@ async def run(
415
415
  Args:
416
416
  server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
417
417
  """
418
- from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
419
418
 
420
419
  # Check if we were spawned by uv (or user explicitly set --skip-env)
421
420
  if skip_env or is_already_in_uv_subprocess():
@@ -446,6 +445,9 @@ async def run(
446
445
  final_path = path or config.deployment.path
447
446
  final_log_level = log_level or config.deployment.log_level
448
447
  final_server_args = server_args or config.deployment.args
448
+ # Use CLI override if provided, otherwise use settings
449
+ # no_banner CLI flag overrides the show_cli_banner setting
450
+ final_no_banner = no_banner if no_banner else not fastmcp.settings.show_cli_banner
449
451
 
450
452
  logger.debug(
451
453
  "Running server or client",
@@ -466,35 +468,53 @@ async def run(
466
468
  needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env
467
469
 
468
470
  if needs_uv:
469
- # Use uv run subprocess - always use run_with_uv which handles output correctly
471
+ # Build the inner fastmcp command
472
+ inner_cmd = ["fastmcp", "run", server_spec]
473
+
474
+ # Add transport options to the inner command
475
+ if final_transport:
476
+ inner_cmd.extend(["--transport", final_transport])
477
+ # Only add HTTP-specific options for non-stdio transports
478
+ if final_transport != "stdio":
479
+ if final_host:
480
+ inner_cmd.extend(["--host", final_host])
481
+ if final_port:
482
+ inner_cmd.extend(["--port", str(final_port)])
483
+ if final_path:
484
+ inner_cmd.extend(["--path", final_path])
485
+ if final_log_level:
486
+ inner_cmd.extend(["--log-level", final_log_level])
487
+ if final_no_banner:
488
+ inner_cmd.append("--no-banner")
489
+ # Add skip-env flag to prevent infinite recursion
490
+ inner_cmd.append("--skip-env")
491
+
492
+ # Add server args if any
493
+ if final_server_args:
494
+ inner_cmd.append("--")
495
+ inner_cmd.extend(final_server_args)
496
+
497
+ # Build the full uv command using the config's environment
498
+ cmd = config.environment.build_command(inner_cmd)
499
+
500
+ # Set marker to prevent infinite loops when subprocess calls FastMCP again
501
+ env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
502
+
503
+ # Run the command
504
+ logger.debug(f"Running command: {' '.join(cmd)}")
470
505
  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:
506
+ process = subprocess.run(cmd, check=True, env=env)
507
+ sys.exit(process.returncode)
508
+ except subprocess.CalledProcessError as e:
490
509
  logger.error(
491
510
  f"Failed to run: {e}",
492
511
  extra={
493
512
  "server_spec": server_spec,
494
513
  "error": str(e),
514
+ "returncode": e.returncode,
495
515
  },
496
516
  )
497
- sys.exit(1)
517
+ sys.exit(e.returncode)
498
518
  else:
499
519
  # Use direct import for backwards compatibility
500
520
  try:
@@ -506,7 +526,7 @@ async def run(
506
526
  path=final_path,
507
527
  log_level=final_log_level,
508
528
  server_args=list(final_server_args) if final_server_args else [],
509
- show_banner=not no_banner,
529
+ show_banner=not final_no_banner,
510
530
  skip_source=skip_source,
511
531
  )
512
532
  except Exception as e:
@@ -598,7 +618,6 @@ async def inspect(
598
618
  Args:
599
619
  server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
600
620
  """
601
- from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
602
621
 
603
622
  # Check if we were spawned by uv (or user explicitly set --skip-env)
604
623
  if skip_env or is_already_in_uv_subprocess():
@@ -641,6 +660,7 @@ async def inspect(
641
660
  "fastmcp",
642
661
  "inspect",
643
662
  server_spec,
663
+ "--skip-env", # Prevent infinite recursion
644
664
  ]
645
665
 
646
666
  # Add format and output flags if specified
@@ -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,18 +107,9 @@ 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,
112
+ dependencies=(with_packages or []) + ["fastmcp"],
122
113
  requirements=str(with_requirements) if with_requirements else None,
123
114
  project=str(project) if project else None,
124
115
  editable=[str(p) for p in with_editable] if with_editable else None,
@@ -73,17 +73,9 @@ 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,
78
+ dependencies=(with_packages or []) + ["fastmcp"],
87
79
  requirements=str(with_requirements) if with_requirements else None,
88
80
  project=str(project) if project else None,
89
81
  editable=[str(p) for p in with_editable] if with_editable else None,
@@ -107,17 +107,9 @@ def install_cursor_workspace(
107
107
 
108
108
  config_file = cursor_dir / "mcp.json"
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
110
  env_config = UVEnvironment(
119
111
  python=python_version,
120
- dependencies=deduplicated_packages,
112
+ dependencies=(with_packages or []) + ["fastmcp"],
121
113
  requirements=str(with_requirements.resolve()) if with_requirements else None,
122
114
  project=str(project.resolve()) if project else None,
123
115
  editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
@@ -185,17 +177,9 @@ def install_cursor(
185
177
  True if installation was successful, False otherwise
186
178
  """
187
179
 
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
180
  env_config = UVEnvironment(
197
181
  python=python_version,
198
- dependencies=deduplicated_packages,
182
+ dependencies=(with_packages or []) + ["fastmcp"],
199
183
  requirements=str(with_requirements.resolve()) if with_requirements else None,
200
184
  project=str(project.resolve()) if project else None,
201
185
  editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
@@ -0,0 +1,242 @@
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
+ # Build uv run command using Environment.build_uv_run_command()
108
+ env_config = UVEnvironment(
109
+ python=python_version,
110
+ dependencies=(with_packages or []) + ["fastmcp"],
111
+ requirements=str(with_requirements) if with_requirements else None,
112
+ project=str(project) if project else None,
113
+ editable=[str(p) for p in with_editable] if with_editable else None,
114
+ )
115
+
116
+ # Build server spec from parsed components
117
+ if server_object:
118
+ server_spec = f"{file.resolve()}:{server_object}"
119
+ else:
120
+ server_spec = str(file.resolve())
121
+
122
+ # Build the full command
123
+ full_command = env_config.build_command(["fastmcp", "run", server_spec])
124
+
125
+ # Build gemini mcp add command
126
+ cmd_parts = [gemini_cmd, "mcp", "add"]
127
+
128
+ # Add environment variables if specified (before the name and command)
129
+ if env_vars:
130
+ for key, value in env_vars.items():
131
+ cmd_parts.extend(["-e", f"{key}={value}"])
132
+
133
+ # Add server name and command
134
+ cmd_parts.extend([name, full_command[0], "--"])
135
+ cmd_parts.extend(full_command[1:])
136
+
137
+ try:
138
+ # Run the gemini mcp add command
139
+ subprocess.run(cmd_parts, check=True, capture_output=True, text=True)
140
+ return True
141
+ except subprocess.CalledProcessError as e:
142
+ print(
143
+ f"[red]Failed to install '[bold]{name}[/bold]' in Gemini CLI: {e.stderr.strip() if e.stderr else str(e)}[/red]"
144
+ )
145
+ return False
146
+ except Exception as e:
147
+ print(f"[red]Failed to install '[bold]{name}[/bold]' in Gemini CLI: {e}[/red]")
148
+ return False
149
+
150
+
151
+ async def gemini_cli_command(
152
+ server_spec: str,
153
+ *,
154
+ server_name: Annotated[
155
+ str | None,
156
+ cyclopts.Parameter(
157
+ name=["--name", "-n"],
158
+ help="Custom name for the server in Gemini CLI",
159
+ ),
160
+ ] = None,
161
+ with_editable: Annotated[
162
+ list[Path] | None,
163
+ cyclopts.Parameter(
164
+ "--with-editable",
165
+ help="Directory with pyproject.toml to install in editable mode (can be used multiple times)",
166
+ negative="",
167
+ ),
168
+ ] = None,
169
+ with_packages: Annotated[
170
+ list[str] | None,
171
+ cyclopts.Parameter(
172
+ "--with",
173
+ help="Additional packages to install (can be used multiple times)",
174
+ negative="",
175
+ ),
176
+ ] = None,
177
+ env_vars: Annotated[
178
+ list[str] | None,
179
+ cyclopts.Parameter(
180
+ "--env",
181
+ help="Environment variables in KEY=VALUE format (can be used multiple times)",
182
+ negative="",
183
+ ),
184
+ ] = None,
185
+ env_file: Annotated[
186
+ Path | None,
187
+ cyclopts.Parameter(
188
+ "--env-file",
189
+ help="Load environment variables from .env file",
190
+ ),
191
+ ] = None,
192
+ python: Annotated[
193
+ str | None,
194
+ cyclopts.Parameter(
195
+ "--python",
196
+ help="Python version to use (e.g., 3.10, 3.11)",
197
+ ),
198
+ ] = None,
199
+ with_requirements: Annotated[
200
+ Path | None,
201
+ cyclopts.Parameter(
202
+ "--with-requirements",
203
+ help="Requirements file to install dependencies from",
204
+ ),
205
+ ] = None,
206
+ project: Annotated[
207
+ Path | None,
208
+ cyclopts.Parameter(
209
+ "--project",
210
+ help="Run the command within the given project directory",
211
+ ),
212
+ ] = None,
213
+ ) -> None:
214
+ """Install an MCP server in Gemini CLI.
215
+
216
+ Args:
217
+ server_spec: Python file to install, optionally with :object suffix
218
+ """
219
+ # Convert None to empty lists for list parameters
220
+ with_editable = with_editable or []
221
+ with_packages = with_packages or []
222
+ env_vars = env_vars or []
223
+ file, server_object, name, packages, env_dict = await process_common_args(
224
+ server_spec, server_name, with_packages, env_vars, env_file
225
+ )
226
+
227
+ success = install_gemini_cli(
228
+ file=file,
229
+ server_object=server_object,
230
+ name=name,
231
+ with_editable=with_editable,
232
+ with_packages=packages,
233
+ env_vars=env_dict,
234
+ python_version=python,
235
+ with_requirements=with_requirements,
236
+ project=project,
237
+ )
238
+
239
+ if success:
240
+ print(f"[green]Successfully installed '{name}' in Gemini CLI")
241
+ else:
242
+ sys.exit(1)
@@ -48,17 +48,9 @@ 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,
53
+ dependencies=(with_packages or []) + ["fastmcp"],
62
54
  requirements=str(with_requirements) if with_requirements else None,
63
55
  project=str(project) if project else None,
64
56
  editable=[str(p) for p in with_editable] if with_editable else None,
fastmcp/cli/run.py CHANGED
@@ -1,9 +1,7 @@
1
1
  """FastMCP run command implementation with enhanced type hints."""
2
2
 
3
3
  import json
4
- import os
5
4
  import re
6
- import subprocess
7
5
  import sys
8
6
  from pathlib import Path
9
7
  from typing import Any, Literal
@@ -15,7 +13,6 @@ from fastmcp.utilities.logging import get_logger
15
13
  from fastmcp.utilities.mcp_server_config import (
16
14
  MCPServerConfig,
17
15
  )
18
- from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
19
16
  from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
20
17
 
21
18
  logger = get_logger("cli.run")
@@ -31,87 +28,6 @@ def is_url(path: str) -> bool:
31
28
  return bool(url_pattern.match(path))
32
29
 
33
30
 
34
- def run_with_uv(
35
- server_spec: str,
36
- python_version: str | None = None,
37
- with_packages: list[str] | None = None,
38
- with_requirements: Path | None = None,
39
- project: Path | None = None,
40
- transport: TransportType | None = None,
41
- host: str | None = None,
42
- port: int | None = None,
43
- path: str | None = None,
44
- log_level: LogLevelType | None = None,
45
- show_banner: bool = True,
46
- editable: str | list[str] | None = None,
47
- ) -> None:
48
- """Run a MCP server using uv run subprocess.
49
-
50
- This function is called when we need to set up a Python environment with specific
51
- dependencies before running the server. The config parsing and merging should already
52
- be done by the caller.
53
-
54
- Args:
55
- server_spec: Python file, object specification (file:obj), config file, or URL
56
- python_version: Python version to use (e.g. "3.10")
57
- with_packages: Additional packages to install
58
- with_requirements: Requirements file to use
59
- project: Run the command within the given project directory
60
- transport: Transport protocol to use
61
- host: Host to bind to when using http transport
62
- port: Port to bind to when using http transport
63
- path: Path to bind to when using http transport
64
- log_level: Log level
65
- show_banner: Whether to show the server banner
66
- editable: Editable package paths
67
- """
68
-
69
- # Build uv command using Environment.build_uv_run_command()
70
- env_config = UVEnvironment(
71
- python=python_version,
72
- dependencies=with_packages if with_packages else None,
73
- requirements=str(with_requirements.resolve()) if with_requirements else None,
74
- project=str(project.resolve()) if project else None,
75
- editable=editable
76
- if isinstance(editable, list)
77
- else ([editable] if editable else None),
78
- )
79
-
80
- # Build the inner fastmcp command (environment variable prevents infinite recursion)
81
- inner_cmd = ["fastmcp", "run", server_spec]
82
-
83
- # Add transport options to the inner command
84
- if transport:
85
- inner_cmd.extend(["--transport", transport])
86
- # Only add HTTP-specific options for non-stdio transports
87
- if transport != "stdio":
88
- if host:
89
- inner_cmd.extend(["--host", host])
90
- if port:
91
- inner_cmd.extend(["--port", str(port)])
92
- if path:
93
- inner_cmd.extend(["--path", path])
94
- if log_level:
95
- inner_cmd.extend(["--log-level", log_level])
96
- if not show_banner:
97
- inner_cmd.append("--no-banner")
98
-
99
- # Build the full uv command
100
- cmd = env_config.build_command(inner_cmd)
101
-
102
- # Set marker to prevent infinite loops when subprocess calls FastMCP again
103
- env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
104
-
105
- # Run the command
106
- logger.debug(f"Running command: {' '.join(cmd)}")
107
- try:
108
- process = subprocess.run(cmd, check=True, env=env)
109
- sys.exit(process.returncode)
110
- except subprocess.CalledProcessError as e:
111
- logger.error(f"Failed to run server: {e}")
112
- sys.exit(e.returncode)
113
-
114
-
115
31
  def create_client_server(url: str) -> Any:
116
32
  """Create a FastMCP server from a client URL.
117
33
 
@@ -355,7 +355,7 @@ class OAuth(OAuthClientProvider):
355
355
  raise TimeoutError(f"OAuth callback timed out after {TIMEOUT} seconds")
356
356
  finally:
357
357
  server.should_exit = True
358
- await asyncio.sleep(0.1) # Allow server to shutdown gracefully
358
+ await asyncio.sleep(0.1) # Allow server to shut down gracefully
359
359
  tg.cancel_scope.cancel()
360
360
 
361
361
  raise RuntimeError("OAuth callback handler could not be started")
fastmcp/client/client.py CHANGED
@@ -454,7 +454,7 @@ class Client(Generic[ClientTransportT]):
454
454
  if self._session_state.nesting_counter > 0:
455
455
  return
456
456
 
457
- # stop the active seesion
457
+ # stop the active session
458
458
  if self._session_state.session_task is None:
459
459
  return
460
460
  self._session_state.stop_event.set()
@@ -505,7 +505,7 @@ class Client(Generic[ClientTransportT]):
505
505
  ) -> None:
506
506
  """Send a cancellation notification for an in-progress request."""
507
507
  notification = mcp.types.ClientNotification(
508
- mcp.types.CancelledNotification(
508
+ root=mcp.types.CancelledNotification(
509
509
  method="notifications/cancelled",
510
510
  params=mcp.types.CancelledNotificationParams(
511
511
  requestId=request_id,
@@ -743,13 +743,13 @@ class Client(Generic[ClientTransportT]):
743
743
 
744
744
  async def complete_mcp(
745
745
  self,
746
- ref: mcp.types.ResourceReference | mcp.types.PromptReference,
746
+ ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference,
747
747
  argument: dict[str, str],
748
748
  ) -> mcp.types.CompleteResult:
749
749
  """Send a completion request and return the complete MCP protocol result.
750
750
 
751
751
  Args:
752
- ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
752
+ ref (mcp.types.ResourceTemplateReference | mcp.types.PromptReference): The reference to complete.
753
753
  argument (dict[str, str]): Arguments to pass to the completion request.
754
754
 
755
755
  Returns:
@@ -766,13 +766,13 @@ class Client(Generic[ClientTransportT]):
766
766
 
767
767
  async def complete(
768
768
  self,
769
- ref: mcp.types.ResourceReference | mcp.types.PromptReference,
769
+ ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference,
770
770
  argument: dict[str, str],
771
771
  ) -> mcp.types.Completion:
772
772
  """Send a completion request to the server.
773
773
 
774
774
  Args:
775
- ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
775
+ ref (mcp.types.ResourceTemplateReference | mcp.types.PromptReference): The reference to complete.
776
776
  argument (dict[str, str]): Arguments to pass to the completion request.
777
777
 
778
778
  Returns:
@@ -59,7 +59,12 @@ def create_elicitation_callback(
59
59
  "Elicitation responses must be serializable as a JSON object (dict). Received: "
60
60
  f"{result.content!r}"
61
61
  )
62
- return MCPElicitResult(**result.model_dump() | {"content": content})
62
+ return MCPElicitResult(
63
+ _meta=result.meta,
64
+ action=result.action,
65
+ content=content,
66
+ )
67
+
63
68
  except Exception as e:
64
69
  return mcp.types.ErrorData(
65
70
  code=mcp.types.INTERNAL_ERROR,