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
@@ -0,0 +1,446 @@
1
+ """FastMCP Configuration File Support.
2
+
3
+ This module provides support for fastmcp.json configuration files that allow
4
+ users to specify server settings in a declarative format instead of using
5
+ command-line arguments.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast, overload
15
+
16
+ from pydantic import BaseModel, Field, field_validator
17
+
18
+ from fastmcp.utilities.logging import get_logger
19
+ from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
20
+ from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
21
+ from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
22
+
23
+ logger = get_logger("cli.config")
24
+
25
+ # JSON Schema for IDE support
26
+ FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
27
+
28
+
29
+ # Type alias for source union (will expand with GitSource, etc in future)
30
+ SourceType: TypeAlias = FileSystemSource
31
+
32
+ # Type alias for environment union (will expand with other environments in future)
33
+ EnvironmentType: TypeAlias = UVEnvironment
34
+
35
+
36
+ class Deployment(BaseModel):
37
+ """Configuration for server deployment and runtime settings."""
38
+
39
+ transport: Literal["stdio", "http", "sse"] | None = Field(
40
+ default=None,
41
+ description="Transport protocol to use",
42
+ )
43
+
44
+ host: str | None = Field(
45
+ default=None,
46
+ description="Host to bind to when using HTTP transport",
47
+ examples=["127.0.0.1", "0.0.0.0", "localhost"],
48
+ )
49
+
50
+ port: int | None = Field(
51
+ default=None,
52
+ description="Port to bind to when using HTTP transport",
53
+ examples=[8000, 3000, 5000],
54
+ )
55
+
56
+ path: str | None = Field(
57
+ default=None,
58
+ description="URL path for the server endpoint",
59
+ examples=["/mcp/", "/api/mcp/", "/sse/"],
60
+ )
61
+
62
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = Field(
63
+ default=None,
64
+ description="Log level for the server",
65
+ )
66
+
67
+ cwd: str | None = Field(
68
+ default=None,
69
+ description="Working directory for the server process",
70
+ examples=[".", "./src", "/app"],
71
+ )
72
+
73
+ env: dict[str, str] | None = Field(
74
+ default=None,
75
+ description="Environment variables to set when running the server",
76
+ examples=[{"API_KEY": "secret", "DEBUG": "true"}],
77
+ )
78
+
79
+ args: list[str] | None = Field(
80
+ default=None,
81
+ description="Arguments to pass to the server (after --)",
82
+ examples=[["--config", "config.json", "--debug"]],
83
+ )
84
+
85
+ def apply_runtime_settings(self, config_path: Path | None = None) -> None:
86
+ """Apply runtime settings like environment variables and working directory.
87
+
88
+ Args:
89
+ config_path: Path to config file for resolving relative paths
90
+
91
+ Environment variables support interpolation with ${VAR_NAME} syntax.
92
+ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com"
93
+ will substitute the value of the ENVIRONMENT variable at runtime.
94
+ """
95
+ import os
96
+ from pathlib import Path
97
+
98
+ # Set environment variables with interpolation support
99
+ if self.env:
100
+ for key, value in self.env.items():
101
+ # Interpolate environment variables in the value
102
+ interpolated_value = self._interpolate_env_vars(value)
103
+ os.environ[key] = interpolated_value
104
+
105
+ # Change working directory
106
+ if self.cwd:
107
+ cwd_path = Path(self.cwd)
108
+ if not cwd_path.is_absolute() and config_path:
109
+ cwd_path = (config_path.parent / cwd_path).resolve()
110
+ os.chdir(cwd_path)
111
+
112
+ def _interpolate_env_vars(self, value: str) -> str:
113
+ """Interpolate environment variables in a string.
114
+
115
+ Replaces ${VAR_NAME} with the value of VAR_NAME from the environment.
116
+ If the variable is not set, the placeholder is left unchanged.
117
+
118
+ Args:
119
+ value: String potentially containing ${VAR_NAME} placeholders
120
+
121
+ Returns:
122
+ String with environment variables interpolated
123
+ """
124
+
125
+ def replace_var(match: re.Match) -> str:
126
+ var_name = match.group(1)
127
+ # Return the environment variable value if it exists, otherwise keep the placeholder
128
+ return os.environ.get(var_name, match.group(0))
129
+
130
+ # Match ${VAR_NAME} pattern and replace with environment variable values
131
+ return re.sub(r"\$\{([^}]+)\}", replace_var, value)
132
+
133
+
134
+ class MCPServerConfig(BaseModel):
135
+ """Configuration for a FastMCP server.
136
+
137
+ This configuration file allows you to specify all settings needed to run
138
+ a FastMCP server in a declarative format.
139
+ """
140
+
141
+ # Schema field for IDE support
142
+ schema_: str | None = Field(
143
+ default="https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
144
+ alias="$schema",
145
+ description="JSON schema for IDE support and validation",
146
+ )
147
+
148
+ # Server source - defines where and how to load the server
149
+ source: SourceType = Field(
150
+ description="Source configuration for the server",
151
+ examples=[
152
+ {"path": "server.py"},
153
+ {"path": "server.py", "entrypoint": "app"},
154
+ {"type": "filesystem", "path": "src/server.py", "entrypoint": "mcp"},
155
+ ],
156
+ )
157
+
158
+ # Environment configuration
159
+ environment: EnvironmentType = Field(
160
+ default_factory=lambda: UVEnvironment(),
161
+ description="Python environment setup configuration",
162
+ )
163
+
164
+ # Deployment configuration
165
+ deployment: Deployment = Field(
166
+ default_factory=lambda: Deployment(),
167
+ description="Server deployment and runtime settings",
168
+ )
169
+
170
+ # purely for static type checkers to avoid issues with providing dict source
171
+ if TYPE_CHECKING:
172
+
173
+ @overload
174
+ def __init__(self, *, source: dict | FileSystemSource, **data) -> None: ...
175
+ @overload
176
+ def __init__(self, *, environment: dict | UVEnvironment, **data) -> None: ...
177
+ @overload
178
+ def __init__(self, *, deployment: dict | Deployment, **data) -> None: ...
179
+ def __init__(self, **data) -> None: ...
180
+
181
+ @field_validator("source", mode="before")
182
+ @classmethod
183
+ def validate_source(cls, v: dict | Source) -> SourceType:
184
+ """Validate and convert source to proper format.
185
+
186
+ Supports:
187
+ - Dict format: {"path": "server.py", "entrypoint": "app"}
188
+ - FileSystemSource instance (passed through)
189
+
190
+ No string parsing happens here - that's only at CLI boundaries.
191
+ MCPServerConfig works only with properly typed objects.
192
+ """
193
+ if isinstance(v, dict):
194
+ return FileSystemSource(**v)
195
+ return v
196
+
197
+ @field_validator("environment", mode="before")
198
+ @classmethod
199
+ def validate_environment(cls, v: dict | Any) -> EnvironmentType:
200
+ """Ensure environment has a type field for discrimination.
201
+
202
+ For backward compatibility, if no type is specified, default to "uv".
203
+ """
204
+ if isinstance(v, dict):
205
+ return UVEnvironment(**v)
206
+ return v
207
+
208
+ @field_validator("deployment", mode="before")
209
+ @classmethod
210
+ def validate_deployment(cls, v: dict | Deployment) -> Deployment:
211
+ """Validate and convert deployment to Deployment.
212
+
213
+ Accepts:
214
+ - Deployment instance
215
+ - dict that can be converted to Deployment
216
+
217
+ """
218
+ if isinstance(v, dict):
219
+ return Deployment(**v) # type: ignore[arg-type]
220
+ return cast(Deployment, v)
221
+
222
+ @classmethod
223
+ def from_file(cls, file_path: Path) -> MCPServerConfig:
224
+ """Load configuration from a JSON file.
225
+
226
+ Args:
227
+ file_path: Path to the configuration file
228
+
229
+ Returns:
230
+ MCPServerConfig instance
231
+
232
+ Raises:
233
+ FileNotFoundError: If the file doesn't exist
234
+ json.JSONDecodeError: If the file is not valid JSON
235
+ pydantic.ValidationError: If the configuration is invalid
236
+ """
237
+ if not file_path.exists():
238
+ raise FileNotFoundError(f"Configuration file not found: {file_path}")
239
+
240
+ with file_path.open("r", encoding="utf-8") as f:
241
+ data = json.load(f)
242
+
243
+ return cls.model_validate(data)
244
+
245
+ @classmethod
246
+ def from_cli_args(
247
+ cls,
248
+ source: FileSystemSource,
249
+ transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
250
+ host: str | None = None,
251
+ port: int | None = None,
252
+ path: str | None = None,
253
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
254
+ | None = None,
255
+ python: str | None = None,
256
+ dependencies: list[str] | None = None,
257
+ requirements: str | None = None,
258
+ project: str | None = None,
259
+ editable: str | None = None,
260
+ env: dict[str, str] | None = None,
261
+ cwd: str | None = None,
262
+ args: list[str] | None = None,
263
+ ) -> MCPServerConfig:
264
+ """Create a config from CLI arguments.
265
+
266
+ This allows us to have a single code path where everything
267
+ goes through a config object.
268
+
269
+ Args:
270
+ source: Server source (FileSystemSource instance)
271
+ transport: Transport protocol
272
+ host: Host for HTTP transport
273
+ port: Port for HTTP transport
274
+ path: URL path for server
275
+ log_level: Logging level
276
+ python: Python version
277
+ dependencies: Python packages to install
278
+ requirements: Path to requirements file
279
+ project: Path to project directory
280
+ editable: Path to install in editable mode
281
+ env: Environment variables
282
+ cwd: Working directory
283
+ args: Server arguments
284
+
285
+ Returns:
286
+ MCPServerConfig instance
287
+ """
288
+ # Build environment config if any env args provided
289
+ environment = None
290
+ if any([python, dependencies, requirements, project, editable]):
291
+ environment = UVEnvironment(
292
+ python=python,
293
+ dependencies=dependencies,
294
+ requirements=requirements,
295
+ project=project,
296
+ editable=[editable] if editable else None,
297
+ )
298
+
299
+ # Build deployment config if any deployment args provided
300
+ deployment = None
301
+ if any([transport, host, port, path, log_level, env, cwd, args]):
302
+ # Convert streamable-http to http for backward compatibility
303
+ if transport == "streamable-http":
304
+ transport = "http" # type: ignore[assignment]
305
+ deployment = Deployment(
306
+ transport=transport, # type: ignore[arg-type]
307
+ host=host,
308
+ port=port,
309
+ path=path,
310
+ log_level=log_level,
311
+ env=env,
312
+ cwd=cwd,
313
+ args=args,
314
+ )
315
+
316
+ return cls(
317
+ source=source,
318
+ environment=environment,
319
+ deployment=deployment,
320
+ )
321
+
322
+ @classmethod
323
+ def find_config(cls, start_path: Path | None = None) -> Path | None:
324
+ """Find a fastmcp.json file in the specified directory.
325
+
326
+ Args:
327
+ start_path: Directory to look in (defaults to current directory)
328
+
329
+ Returns:
330
+ Path to the configuration file, or None if not found
331
+ """
332
+ if start_path is None:
333
+ start_path = Path.cwd()
334
+
335
+ config_path = start_path / "fastmcp.json"
336
+ if config_path.exists():
337
+ logger.debug(f"Found configuration file: {config_path}")
338
+ return config_path
339
+
340
+ return None
341
+
342
+ async def prepare(
343
+ self,
344
+ skip_source: bool = False,
345
+ output_dir: Path | None = None,
346
+ ) -> None:
347
+ """Prepare environment and source for execution.
348
+
349
+ When output_dir is provided, creates a persistent uv project.
350
+ When output_dir is None, does ephemeral caching (for backwards compatibility).
351
+
352
+ Args:
353
+ skip_source: Skip source preparation if True
354
+ output_dir: Directory to create the persistent uv project in (optional)
355
+ """
356
+ # Prepare environment (persistent if output_dir provided, ephemeral otherwise)
357
+ if self.environment:
358
+ await self.prepare_environment(output_dir=output_dir)
359
+
360
+ if not skip_source:
361
+ await self.prepare_source()
362
+
363
+ async def prepare_environment(self, output_dir: Path | None = None) -> None:
364
+ """Prepare the Python environment.
365
+
366
+ Args:
367
+ output_dir: If provided, creates a persistent uv project in this directory.
368
+ If None, just populates uv's cache for ephemeral use.
369
+
370
+ Delegates to the environment's prepare() method
371
+ """
372
+ await self.environment.prepare(output_dir=output_dir)
373
+
374
+ async def prepare_source(self) -> None:
375
+ """Prepare the source for loading.
376
+
377
+ Delegates to the source's prepare() method.
378
+ """
379
+ await self.source.prepare()
380
+
381
+ async def run_server(self, **kwargs: Any) -> None:
382
+ """Load and run the server with this configuration.
383
+
384
+ Args:
385
+ **kwargs: Additional arguments to pass to server.run_async()
386
+ These override config settings
387
+ """
388
+ # Apply deployment settings (env vars, cwd)
389
+ if self.deployment:
390
+ self.deployment.apply_runtime_settings()
391
+
392
+ # Load the server
393
+ server = await self.source.load_server()
394
+
395
+ # Build run arguments from config
396
+ run_args = {}
397
+ if self.deployment:
398
+ if self.deployment.transport:
399
+ run_args["transport"] = self.deployment.transport
400
+ if self.deployment.host:
401
+ run_args["host"] = self.deployment.host
402
+ if self.deployment.port:
403
+ run_args["port"] = self.deployment.port
404
+ if self.deployment.path:
405
+ run_args["path"] = self.deployment.path
406
+ # Note: log_level not currently supported by run_async
407
+
408
+ # Override with any provided kwargs
409
+ run_args.update(kwargs)
410
+
411
+ # Run the server
412
+ await server.run_async(**run_args)
413
+
414
+
415
+ def generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None:
416
+ """Generate JSON schema for fastmcp.json files.
417
+
418
+ This is used to create the schema file that IDEs can use for
419
+ validation and auto-completion.
420
+
421
+ Args:
422
+ output_path: Optional path to write the schema to. If provided,
423
+ writes the schema and returns None. If not provided,
424
+ returns the schema as a dictionary.
425
+
426
+ Returns:
427
+ JSON schema as a dictionary if output_path is None, otherwise None
428
+ """
429
+ schema = MCPServerConfig.model_json_schema()
430
+
431
+ # Add some metadata
432
+ schema["$id"] = FASTMCP_JSON_SCHEMA
433
+ schema["title"] = "FastMCP Configuration"
434
+ schema["description"] = "Configuration file for FastMCP servers"
435
+
436
+ if output_path:
437
+ import json
438
+
439
+ output = Path(output_path)
440
+ output.parent.mkdir(parents=True, exist_ok=True)
441
+ with open(output, "w") as f:
442
+ json.dump(schema, f, indent=2)
443
+ f.write("\n") # Add trailing newline
444
+ return None
445
+
446
+ return schema