fastmcp 2.11.3__py3-none-any.whl → 2.12.0__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 (69) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/oauth_callback.py +91 -91
  13. fastmcp/client/sampling.py +12 -4
  14. fastmcp/client/transports.py +139 -64
  15. fastmcp/experimental/sampling/__init__.py +0 -0
  16. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  17. fastmcp/experimental/sampling/handlers/base.py +21 -0
  18. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  19. fastmcp/experimental/server/openapi/routing.py +0 -2
  20. fastmcp/experimental/server/openapi/server.py +0 -2
  21. fastmcp/experimental/utilities/openapi/parser.py +5 -1
  22. fastmcp/mcp_config.py +40 -20
  23. fastmcp/prompts/prompt_manager.py +2 -0
  24. fastmcp/resources/resource_manager.py +4 -0
  25. fastmcp/server/auth/__init__.py +2 -0
  26. fastmcp/server/auth/auth.py +2 -1
  27. fastmcp/server/auth/oauth_proxy.py +1047 -0
  28. fastmcp/server/auth/providers/azure.py +270 -0
  29. fastmcp/server/auth/providers/github.py +287 -0
  30. fastmcp/server/auth/providers/google.py +305 -0
  31. fastmcp/server/auth/providers/jwt.py +24 -12
  32. fastmcp/server/auth/providers/workos.py +256 -2
  33. fastmcp/server/auth/redirect_validation.py +65 -0
  34. fastmcp/server/context.py +91 -41
  35. fastmcp/server/elicitation.py +60 -1
  36. fastmcp/server/http.py +3 -3
  37. fastmcp/server/middleware/logging.py +66 -28
  38. fastmcp/server/proxy.py +2 -0
  39. fastmcp/server/sampling/handler.py +19 -0
  40. fastmcp/server/server.py +76 -15
  41. fastmcp/settings.py +16 -1
  42. fastmcp/tools/tool.py +22 -9
  43. fastmcp/tools/tool_manager.py +2 -0
  44. fastmcp/tools/tool_transform.py +39 -10
  45. fastmcp/utilities/auth.py +34 -0
  46. fastmcp/utilities/cli.py +148 -15
  47. fastmcp/utilities/components.py +2 -1
  48. fastmcp/utilities/inspect.py +166 -37
  49. fastmcp/utilities/json_schema_type.py +4 -2
  50. fastmcp/utilities/logging.py +4 -1
  51. fastmcp/utilities/mcp_config.py +47 -18
  52. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  53. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  54. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  55. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  59. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  60. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  61. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  62. fastmcp/utilities/tests.py +7 -2
  63. fastmcp/utilities/types.py +15 -2
  64. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
  65. fastmcp-2.12.0.dist-info/RECORD +129 -0
  66. fastmcp-2.11.3.dist-info/RECORD +0 -108
  67. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  68. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  69. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,28 +1,57 @@
1
1
  from typing import Any
2
2
 
3
- from fastmcp.mcp_config import MCPConfig
3
+ from fastmcp.client.transports import (
4
+ ClientTransport,
5
+ SSETransport,
6
+ StdioTransport,
7
+ StreamableHttpTransport,
8
+ )
9
+ from fastmcp.mcp_config import (
10
+ MCPConfig,
11
+ MCPServerTypes,
12
+ )
13
+ from fastmcp.server.proxy import FastMCPProxy, ProxyClient
4
14
  from fastmcp.server.server import FastMCP
5
15
 
6
16
 
7
- def composite_server_from_mcp_config(
8
- config: MCPConfig, name_as_prefix: bool = True
9
- ) -> FastMCP[None]:
10
- """A utility function to create a composite server from an MCPConfig."""
11
- composite_server = FastMCP[None]()
17
+ def mcp_config_to_servers_and_transports(
18
+ config: MCPConfig,
19
+ ) -> list[tuple[str, FastMCP[Any], ClientTransport]]:
20
+ """A utility function to convert each entry of an MCP Config into a transport and server."""
21
+ return [
22
+ mcp_server_type_to_servers_and_transports(name, mcp_server)
23
+ for name, mcp_server in config.mcpServers.items()
24
+ ]
12
25
 
13
- mount_mcp_config_into_server(config, composite_server, name_as_prefix)
14
26
 
15
- return composite_server
27
+ def mcp_server_type_to_servers_and_transports(
28
+ name: str,
29
+ mcp_server: MCPServerTypes,
30
+ ) -> tuple[str, FastMCP[Any], ClientTransport]:
31
+ """A utility function to convert each entry of an MCP Config into a transport and server."""
16
32
 
33
+ from fastmcp.mcp_config import (
34
+ TransformingRemoteMCPServer,
35
+ TransformingStdioMCPServer,
36
+ )
17
37
 
18
- def mount_mcp_config_into_server(
19
- config: MCPConfig,
20
- server: FastMCP[Any],
21
- name_as_prefix: bool = True,
22
- ) -> None:
23
- """A utility function to mount the servers from an MCPConfig into a FastMCP server."""
24
- for name, mcp_server in config.mcpServers.items():
25
- server.mount(
26
- prefix=name if name_as_prefix else None,
27
- server=FastMCP.as_proxy(backend=mcp_server.to_transport()),
38
+ server: FastMCP[Any]
39
+ transport: ClientTransport
40
+
41
+ client_name = ProxyClient.generate_name(f"MCP_{name}")
42
+ server_name = FastMCPProxy.generate_name(f"MCP_{name}")
43
+
44
+ if isinstance(mcp_server, TransformingRemoteMCPServer | TransformingStdioMCPServer):
45
+ server, transport = mcp_server._to_server_and_underlying_transport(
46
+ server_name=server_name,
47
+ client_name=client_name,
28
48
  )
49
+ else:
50
+ transport = mcp_server.to_transport()
51
+ client: ProxyClient[StreamableHttpTransport | SSETransport | StdioTransport] = (
52
+ ProxyClient(transport=transport, name=client_name)
53
+ )
54
+
55
+ server = FastMCP.as_proxy(name=server_name, backend=client)
56
+
57
+ return name, server, transport
@@ -0,0 +1,25 @@
1
+ """FastMCP Configuration module.
2
+
3
+ This module provides versioned configuration support for FastMCP servers.
4
+ The current version is v1, which is re-exported here for convenience.
5
+ """
6
+
7
+ from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
8
+ from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
9
+ from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import (
10
+ Deployment,
11
+ MCPServerConfig,
12
+ generate_schema,
13
+ )
14
+ from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
15
+ from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
16
+
17
+ __all__ = [
18
+ "Source",
19
+ "Deployment",
20
+ "Environment",
21
+ "UVEnvironment",
22
+ "MCPServerConfig",
23
+ "FileSystemSource",
24
+ "generate_schema",
25
+ ]
File without changes
@@ -0,0 +1,6 @@
1
+ """Environment configuration for MCP servers."""
2
+
3
+ from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
4
+ from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
5
+
6
+ __all__ = ["Environment", "UVEnvironment"]
@@ -0,0 +1,30 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class Environment(BaseModel, ABC):
8
+ """Base class for environment configuration."""
9
+
10
+ type: str = Field(description="Environment type identifier")
11
+
12
+ @abstractmethod
13
+ def build_command(self, command: list[str]) -> list[str]:
14
+ """Build the full command with environment setup.
15
+
16
+ Args:
17
+ command: Base command to wrap with environment setup
18
+
19
+ Returns:
20
+ Full command ready for subprocess execution
21
+ """
22
+ pass
23
+
24
+ async def prepare(self, output_dir: Path | None = None) -> None:
25
+ """Prepare the environment (optional, can be no-op).
26
+
27
+ Args:
28
+ output_dir: Directory for persistent environment setup
29
+ """
30
+ pass # Default no-op implementation
@@ -0,0 +1,306 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import Field
9
+
10
+ from fastmcp.utilities.logging import get_logger
11
+ from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
12
+
13
+ logger = get_logger("cli.config")
14
+
15
+
16
+ class UVEnvironment(Environment):
17
+ """Configuration for Python environment setup."""
18
+
19
+ type: Literal["uv"] = "uv"
20
+
21
+ python: str | None = Field(
22
+ default=None,
23
+ description="Python version constraint",
24
+ examples=["3.10", "3.11", "3.12"],
25
+ )
26
+
27
+ dependencies: list[str] | None = Field(
28
+ default=None,
29
+ description="Python packages to install with PEP 508 specifiers",
30
+ examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
31
+ )
32
+
33
+ requirements: str | None = Field(
34
+ default=None,
35
+ description="Path to requirements.txt file",
36
+ examples=["requirements.txt", "../requirements/prod.txt"],
37
+ )
38
+
39
+ project: str | None = Field(
40
+ default=None,
41
+ description="Path to project directory containing pyproject.toml",
42
+ examples=[".", "../my-project"],
43
+ )
44
+
45
+ editable: list[str] | None = Field(
46
+ default=None,
47
+ description="Directories to install in editable mode",
48
+ examples=[[".", "../my-package"], ["/path/to/package"]],
49
+ )
50
+
51
+ def build_command(self, command: list[str]) -> list[str]:
52
+ """Build complete uv run command with environment args and command to execute.
53
+
54
+ Args:
55
+ command: Command to execute (e.g., ["fastmcp", "run", "server.py"])
56
+
57
+ Returns:
58
+ Complete command ready for subprocess.run, including "uv" prefix if needed.
59
+ If no environment configuration is set, returns the command unchanged.
60
+ """
61
+ # If no environment setup is needed, return command as-is
62
+ if not self._needs_setup():
63
+ return command
64
+
65
+ args = ["uv", "run"]
66
+
67
+ # Add project if specified
68
+ if self.project:
69
+ args.extend(["--project", str(self.project)])
70
+
71
+ # Add Python version if specified (only if no project, as project has its own Python)
72
+ if self.python and not self.project:
73
+ args.extend(["--python", self.python])
74
+
75
+ # Always add dependencies, requirements, and editable packages
76
+ # These work with --project to add additional packages on top of the project env
77
+ if self.dependencies:
78
+ for dep in self.dependencies:
79
+ args.extend(["--with", dep])
80
+
81
+ # Add requirements file
82
+ if self.requirements:
83
+ args.extend(["--with-requirements", str(self.requirements)])
84
+
85
+ # Add editable packages
86
+ if self.editable:
87
+ for editable_path in self.editable:
88
+ args.extend(["--with-editable", str(editable_path)])
89
+
90
+ # Add the command
91
+ args.extend(command)
92
+
93
+ return args
94
+
95
+ def run_with_uv(self, command: list[str]) -> None:
96
+ """Execute a command using uv run with this environment configuration.
97
+
98
+ Args:
99
+ command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
100
+ """
101
+ import subprocess
102
+
103
+ # Build the full uv command
104
+ cmd = self.build_command(command)
105
+
106
+ # Set marker to prevent infinite loops when subprocess calls FastMCP again
107
+ env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
108
+
109
+ logger.debug(f"Running command: {' '.join(cmd)}")
110
+
111
+ try:
112
+ # Run without capturing output so it flows through naturally
113
+ process = subprocess.run(cmd, check=True, env=env)
114
+ sys.exit(process.returncode)
115
+ except subprocess.CalledProcessError as e:
116
+ logger.error(f"Command failed: {e}")
117
+ sys.exit(e.returncode)
118
+
119
+ def _needs_setup(self) -> bool:
120
+ """Check if this environment config requires uv to set up.
121
+
122
+ Returns:
123
+ True if any environment settings require uv run
124
+ """
125
+ return any(
126
+ [
127
+ self.python is not None,
128
+ self.dependencies is not None,
129
+ self.requirements is not None,
130
+ self.project is not None,
131
+ self.editable is not None,
132
+ ]
133
+ )
134
+
135
+ # Backward compatibility aliases
136
+ def needs_uv(self) -> bool:
137
+ """Deprecated: Use _needs_setup() internally or check if build_command modifies the command."""
138
+ return self._needs_setup()
139
+
140
+ def build_uv_run_command(self, command: list[str]) -> list[str]:
141
+ """Deprecated: Use build_command() instead."""
142
+ return self.build_command(command)
143
+
144
+ async def prepare(self, output_dir: Path | None = None) -> None:
145
+ """Prepare the Python environment using uv.
146
+
147
+ Args:
148
+ output_dir: Directory where the persistent uv project will be created.
149
+ If None, creates a temporary directory for ephemeral use.
150
+ """
151
+
152
+ # Check if uv is available
153
+ if not shutil.which("uv"):
154
+ raise RuntimeError(
155
+ "uv is not installed. Please install it with: "
156
+ "curl -LsSf https://astral.sh/uv/install.sh | sh"
157
+ )
158
+
159
+ # Only prepare environment if there are actual settings to apply
160
+ if not self._needs_setup():
161
+ logger.debug("No environment settings configured, skipping preparation")
162
+ return
163
+
164
+ # Handle None case for ephemeral use
165
+ if output_dir is None:
166
+ import tempfile
167
+
168
+ output_dir = Path(tempfile.mkdtemp(prefix="fastmcp-env-"))
169
+ logger.info(f"Creating ephemeral environment in {output_dir}")
170
+ else:
171
+ logger.info(f"Creating persistent environment in {output_dir}")
172
+ output_dir = Path(output_dir).resolve()
173
+
174
+ # Initialize the project
175
+ logger.debug(f"Initializing uv project in {output_dir}")
176
+ try:
177
+ subprocess.run(
178
+ [
179
+ "uv",
180
+ "init",
181
+ "--project",
182
+ str(output_dir),
183
+ "--name",
184
+ "fastmcp-env",
185
+ ],
186
+ check=True,
187
+ capture_output=True,
188
+ text=True,
189
+ )
190
+ except subprocess.CalledProcessError as e:
191
+ # If project already exists, that's fine - continue
192
+ if "already initialized" in e.stderr.lower():
193
+ logger.debug(
194
+ f"Project already initialized at {output_dir}, continuing..."
195
+ )
196
+ else:
197
+ logger.error(f"Failed to initialize project: {e.stderr}")
198
+ raise RuntimeError(f"Failed to initialize project: {e.stderr}") from e
199
+
200
+ # Pin Python version if specified
201
+ if self.python:
202
+ logger.debug(f"Pinning Python version to {self.python}")
203
+ try:
204
+ subprocess.run(
205
+ [
206
+ "uv",
207
+ "python",
208
+ "pin",
209
+ self.python,
210
+ "--project",
211
+ str(output_dir),
212
+ ],
213
+ check=True,
214
+ capture_output=True,
215
+ text=True,
216
+ )
217
+ except subprocess.CalledProcessError as e:
218
+ logger.error(f"Failed to pin Python version: {e.stderr}")
219
+ raise RuntimeError(f"Failed to pin Python version: {e.stderr}") from e
220
+
221
+ # Add dependencies with --no-sync to defer installation
222
+ # dependencies ALWAYS include fastmcp; this is compatible with
223
+ # specific fastmcp versions that might be in the dependencies list
224
+ dependencies = (self.dependencies or []) + ["fastmcp"]
225
+ logger.debug(f"Adding dependencies: {', '.join(dependencies)}")
226
+ try:
227
+ subprocess.run(
228
+ [
229
+ "uv",
230
+ "add",
231
+ *dependencies,
232
+ "--no-sync",
233
+ "--project",
234
+ str(output_dir),
235
+ ],
236
+ check=True,
237
+ capture_output=True,
238
+ text=True,
239
+ )
240
+ except subprocess.CalledProcessError as e:
241
+ logger.error(f"Failed to add dependencies: {e.stderr}")
242
+ raise RuntimeError(f"Failed to add dependencies: {e.stderr}") from e
243
+
244
+ # Add requirements file if specified
245
+ if self.requirements:
246
+ logger.debug(f"Adding requirements from {self.requirements}")
247
+ # Resolve requirements path relative to current directory
248
+ req_path = Path(self.requirements).resolve()
249
+ try:
250
+ subprocess.run(
251
+ [
252
+ "uv",
253
+ "add",
254
+ "-r",
255
+ str(req_path),
256
+ "--no-sync",
257
+ "--project",
258
+ str(output_dir),
259
+ ],
260
+ check=True,
261
+ capture_output=True,
262
+ text=True,
263
+ )
264
+ except subprocess.CalledProcessError as e:
265
+ logger.error(f"Failed to add requirements: {e.stderr}")
266
+ raise RuntimeError(f"Failed to add requirements: {e.stderr}") from e
267
+
268
+ # Add editable packages if specified
269
+ if self.editable:
270
+ editable_paths = [str(Path(e).resolve()) for e in self.editable]
271
+ logger.debug(f"Adding editable packages: {', '.join(editable_paths)}")
272
+ try:
273
+ subprocess.run(
274
+ [
275
+ "uv",
276
+ "add",
277
+ "--editable",
278
+ *editable_paths,
279
+ "--no-sync",
280
+ "--project",
281
+ str(output_dir),
282
+ ],
283
+ check=True,
284
+ capture_output=True,
285
+ text=True,
286
+ )
287
+ except subprocess.CalledProcessError as e:
288
+ logger.error(f"Failed to add editable packages: {e.stderr}")
289
+ raise RuntimeError(
290
+ f"Failed to add editable packages: {e.stderr}"
291
+ ) from e
292
+
293
+ # Final sync to install everything
294
+ logger.info("Installing dependencies...")
295
+ try:
296
+ subprocess.run(
297
+ ["uv", "sync", "--project", str(output_dir)],
298
+ check=True,
299
+ capture_output=True,
300
+ text=True,
301
+ )
302
+ except subprocess.CalledProcessError as e:
303
+ logger.error(f"Failed to sync dependencies: {e.stderr}")
304
+ raise RuntimeError(f"Failed to sync dependencies: {e.stderr}") from e
305
+
306
+ logger.info(f"Environment prepared successfully in {output_dir}")