fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__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 (77) 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/logging.py +25 -1
  13. fastmcp/client/oauth_callback.py +91 -91
  14. fastmcp/client/sampling.py +12 -4
  15. fastmcp/client/transports.py +143 -67
  16. fastmcp/experimental/sampling/__init__.py +0 -0
  17. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  18. fastmcp/experimental/sampling/handlers/base.py +21 -0
  19. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  20. fastmcp/experimental/server/openapi/routing.py +1 -3
  21. fastmcp/experimental/server/openapi/server.py +10 -25
  22. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  23. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  24. fastmcp/experimental/utilities/openapi/models.py +5 -2
  25. fastmcp/experimental/utilities/openapi/parser.py +252 -70
  26. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  27. fastmcp/mcp_config.py +40 -20
  28. fastmcp/prompts/prompt_manager.py +4 -2
  29. fastmcp/resources/resource_manager.py +16 -6
  30. fastmcp/server/auth/__init__.py +11 -1
  31. fastmcp/server/auth/auth.py +19 -2
  32. fastmcp/server/auth/oauth_proxy.py +1047 -0
  33. fastmcp/server/auth/providers/azure.py +270 -0
  34. fastmcp/server/auth/providers/github.py +287 -0
  35. fastmcp/server/auth/providers/google.py +305 -0
  36. fastmcp/server/auth/providers/jwt.py +27 -16
  37. fastmcp/server/auth/providers/workos.py +256 -2
  38. fastmcp/server/auth/redirect_validation.py +65 -0
  39. fastmcp/server/auth/registry.py +1 -1
  40. fastmcp/server/context.py +91 -41
  41. fastmcp/server/dependencies.py +32 -2
  42. fastmcp/server/elicitation.py +60 -1
  43. fastmcp/server/http.py +44 -37
  44. fastmcp/server/middleware/logging.py +66 -28
  45. fastmcp/server/proxy.py +2 -0
  46. fastmcp/server/sampling/handler.py +19 -0
  47. fastmcp/server/server.py +85 -20
  48. fastmcp/settings.py +18 -3
  49. fastmcp/tools/tool.py +23 -10
  50. fastmcp/tools/tool_manager.py +5 -1
  51. fastmcp/tools/tool_transform.py +75 -32
  52. fastmcp/utilities/auth.py +34 -0
  53. fastmcp/utilities/cli.py +148 -15
  54. fastmcp/utilities/components.py +21 -5
  55. fastmcp/utilities/inspect.py +166 -37
  56. fastmcp/utilities/json_schema_type.py +4 -2
  57. fastmcp/utilities/logging.py +4 -1
  58. fastmcp/utilities/mcp_config.py +47 -18
  59. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  60. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  61. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  62. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  63. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  64. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  65. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  66. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  67. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  68. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  69. fastmcp/utilities/openapi.py +4 -4
  70. fastmcp/utilities/tests.py +7 -2
  71. fastmcp/utilities/types.py +15 -2
  72. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0rc1.dist-info/RECORD +129 -0
  74. fastmcp-2.11.2.dist-info/RECORD +0 -108
  75. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
  77. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -75,7 +75,7 @@ def _replace_ref_with_defs(
75
75
  info: dict[str, Any], description: str | None = None
76
76
  ) -> dict[str, Any]:
77
77
  """
78
- Replace openapi $ref with jsonschema $defs
78
+ Replace openapi $ref with jsonschema $defs recursively.
79
79
 
80
80
  Examples:
81
81
  - {"type": "object", "properties": {"$ref": "#/components/schemas/..."}}
@@ -202,6 +202,7 @@ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:
202
202
 
203
203
  def _combine_schemas_and_map_params(
204
204
  route: HTTPRoute,
205
+ convert_refs: bool = True,
205
206
  ) -> tuple[dict[str, Any], dict[str, dict[str, str]]]:
206
207
  """
207
208
  Combines parameter and request body schemas into a single schema.
@@ -233,10 +234,43 @@ def _combine_schemas_and_map_params(
233
234
 
234
235
  if route.request_body and route.request_body.content_schema:
235
236
  content_type = next(iter(route.request_body.content_schema))
236
- body_schema = _replace_ref_with_defs(
237
- route.request_body.content_schema[content_type].copy(),
238
- route.request_body.description,
239
- )
237
+
238
+ # Convert refs if needed
239
+ if convert_refs:
240
+ body_schema = _replace_ref_with_defs(
241
+ route.request_body.content_schema[content_type]
242
+ )
243
+ else:
244
+ body_schema = route.request_body.content_schema[content_type]
245
+
246
+ if route.request_body.description and not body_schema.get("description"):
247
+ body_schema["description"] = route.request_body.description
248
+
249
+ # Handle allOf at the top level by merging all schemas
250
+ if "allOf" in body_schema and isinstance(body_schema["allOf"], list):
251
+ merged_props = {}
252
+ merged_required = []
253
+
254
+ for sub_schema in body_schema["allOf"]:
255
+ if isinstance(sub_schema, dict):
256
+ # Merge properties
257
+ if "properties" in sub_schema:
258
+ merged_props.update(sub_schema["properties"])
259
+ # Merge required fields
260
+ if "required" in sub_schema:
261
+ merged_required.extend(sub_schema["required"])
262
+
263
+ # Update body_schema with merged properties
264
+ body_schema["properties"] = merged_props
265
+ if merged_required:
266
+ # Remove duplicates while preserving order
267
+ seen = set()
268
+ body_schema["required"] = [
269
+ x for x in merged_required if not (x in seen or seen.add(x))
270
+ ]
271
+ # Remove the allOf since we've merged it
272
+ body_schema.pop("allOf", None)
273
+
240
274
  body_props = body_schema.get("properties", {})
241
275
 
242
276
  # Detect collisions: parameters that exist in both body and path/query/header
@@ -261,10 +295,11 @@ def _combine_schemas_and_map_params(
261
295
  "openapi_name": param.name,
262
296
  }
263
297
 
264
- # Add location info to description
265
- param_schema = _replace_ref_with_defs(
266
- param.schema_.copy(), param.description
267
- )
298
+ # Convert refs if needed
299
+ if convert_refs:
300
+ param_schema = _replace_ref_with_defs(param.schema_)
301
+ else:
302
+ param_schema = param.schema_
268
303
  original_desc = param_schema.get("description", "")
269
304
  location_desc = f"({param.location.capitalize()} parameter)"
270
305
  if original_desc:
@@ -287,9 +322,11 @@ def _combine_schemas_and_map_params(
287
322
  "openapi_name": param.name,
288
323
  }
289
324
 
290
- param_schema = _replace_ref_with_defs(
291
- param.schema_.copy(), param.description
292
- )
325
+ # Convert refs if needed
326
+ if convert_refs:
327
+ param_schema = _replace_ref_with_defs(param.schema_)
328
+ else:
329
+ param_schema = param.schema_
293
330
 
294
331
  # Don't make optional parameters nullable - they can simply be omitted
295
332
  # The OpenAPI specification doesn't require optional parameters to accept null values
@@ -298,14 +335,28 @@ def _combine_schemas_and_map_params(
298
335
 
299
336
  # Add request body properties (no suffixes for body parameters)
300
337
  if route.request_body and route.request_body.content_schema:
301
- for prop_name, prop_schema in body_props.items():
302
- properties[prop_name] = prop_schema
338
+ # If body is just a $ref, we need to handle it differently
339
+ if "$ref" in body_schema and not body_props:
340
+ # The entire body is a reference to a schema
341
+ # We need to expand this inline or keep the ref
342
+ # For simplicity, we'll keep it as a single property
343
+ properties["body"] = body_schema
344
+ if route.request_body.required:
345
+ required.append("body")
346
+ parameter_map["body"] = {"location": "body", "openapi_name": "body"}
347
+ else:
348
+ # Normal case: body has properties
349
+ for prop_name, prop_schema in body_props.items():
350
+ properties[prop_name] = prop_schema
303
351
 
304
- # Track parameter mapping for body properties
305
- parameter_map[prop_name] = {"location": "body", "openapi_name": prop_name}
352
+ # Track parameter mapping for body properties
353
+ parameter_map[prop_name] = {
354
+ "location": "body",
355
+ "openapi_name": prop_name,
356
+ }
306
357
 
307
- if route.request_body.required:
308
- required.extend(body_schema.get("required", []))
358
+ if route.request_body.required:
359
+ required.extend(body_schema.get("required", []))
309
360
 
310
361
  result = {
311
362
  "type": "object",
@@ -313,43 +364,53 @@ def _combine_schemas_and_map_params(
313
364
  "required": required,
314
365
  }
315
366
  # Add schema definitions if available
316
- if route.schema_definitions:
317
- result["$defs"] = route.schema_definitions.copy()
318
-
319
- # Use lightweight compression - prune additionalProperties and unused definitions
320
- if result.get("additionalProperties") is False:
321
- result.pop("additionalProperties")
322
-
323
- # Remove unused definitions (lightweight approach - just check direct $ref usage)
324
- if "$defs" in result:
325
- used_refs = set()
326
-
327
- def find_refs_in_value(value):
328
- if isinstance(value, dict):
329
- if "$ref" in value and isinstance(value["$ref"], str):
330
- ref = value["$ref"]
331
- if ref.startswith("#/$defs/"):
332
- used_refs.add(ref.split("/")[-1])
333
- for v in value.values():
334
- find_refs_in_value(v)
335
- elif isinstance(value, list):
336
- for item in value:
337
- find_refs_in_value(item)
338
-
339
- # Find refs in the main schema (excluding $defs section)
340
- for key, value in result.items():
341
- if key != "$defs":
342
- find_refs_in_value(value)
343
-
344
- # Remove unused definitions
345
- if used_refs:
346
- result["$defs"] = {
347
- name: def_schema
348
- for name, def_schema in result["$defs"].items()
349
- if name in used_refs
350
- }
367
+ schema_defs = route.request_schemas
368
+ if schema_defs:
369
+ if convert_refs:
370
+ # Need to convert refs and prune
371
+ all_defs = schema_defs.copy()
372
+ # Convert each schema definition recursively
373
+ for name, schema in all_defs.items():
374
+ if isinstance(schema, dict):
375
+ all_defs[name] = _replace_ref_with_defs(schema)
376
+
377
+ # Prune to only needed schemas
378
+ used_refs = set()
379
+
380
+ def find_refs_in_value(value):
381
+ """Recursively find all $ref references."""
382
+ if isinstance(value, dict):
383
+ if "$ref" in value and isinstance(value["$ref"], str):
384
+ ref = value["$ref"]
385
+ if ref.startswith("#/$defs/"):
386
+ used_refs.add(ref.split("/")[-1])
387
+ for v in value.values():
388
+ find_refs_in_value(v)
389
+ elif isinstance(value, list):
390
+ for item in value:
391
+ find_refs_in_value(item)
392
+
393
+ # Find refs in properties
394
+ find_refs_in_value(properties)
395
+
396
+ # Collect transitive dependencies
397
+ if used_refs:
398
+ collected_all = False
399
+ while not collected_all:
400
+ initial_count = len(used_refs)
401
+ for name in list(used_refs):
402
+ if name in all_defs:
403
+ find_refs_in_value(all_defs[name])
404
+ collected_all = len(used_refs) == initial_count
405
+
406
+ result["$defs"] = {
407
+ name: def_schema
408
+ for name, def_schema in all_defs.items()
409
+ if name in used_refs
410
+ }
351
411
  else:
352
- result.pop("$defs")
412
+ # From parser - already converted and pruned
413
+ result["$defs"] = schema_defs
353
414
 
354
415
  return result, parameter_map
355
416
 
@@ -440,17 +501,17 @@ def extract_output_schema_from_responses(
440
501
  if not schema or not isinstance(schema, dict):
441
502
  return None
442
503
 
443
- # Clean and copy the schema
444
- output_schema = schema.copy()
504
+ # Convert refs if needed
505
+ output_schema = _replace_ref_with_defs(schema)
445
506
 
446
507
  # If schema has a $ref, resolve it first before processing nullable fields
447
508
  if "$ref" in output_schema and schema_definitions:
448
509
  ref_path = output_schema["$ref"]
449
- if ref_path.startswith("#/components/schemas/"):
510
+ if ref_path.startswith("#/$defs/"):
450
511
  schema_name = ref_path.split("/")[-1]
451
512
  if schema_name in schema_definitions:
452
513
  # Replace $ref with the actual schema definition
453
- output_schema = schema_definitions[schema_name].copy()
514
+ output_schema = _replace_ref_with_defs(schema_definitions[schema_name])
454
515
 
455
516
  # Convert OpenAPI schema to JSON Schema format
456
517
  # Only needed for OpenAPI 3.0 - 3.1 uses standard JSON Schema null types
@@ -473,56 +534,25 @@ def extract_output_schema_from_responses(
473
534
  }
474
535
  output_schema = wrapped_schema
475
536
 
476
- # Add schema definitions if available and handle nullable fields in them
477
- # Only add $defs if we didn't resolve the $ref inline above
478
- if schema_definitions and "$ref" not in schema.copy():
479
- processed_defs = {}
480
- for def_name, def_schema in schema_definitions.items():
481
- # Convert OpenAPI schema definitions to JSON Schema format
482
- if openapi_version and openapi_version.startswith("3.0"):
483
- from .json_schema_converter import convert_openapi_schema_to_json_schema
484
-
537
+ # Add schema definitions if available
538
+ if schema_definitions:
539
+ # Convert refs if needed
540
+ processed_defs = schema_definitions.copy()
541
+ # Convert each schema definition recursively
542
+ for name, schema in processed_defs.items():
543
+ if isinstance(schema, dict):
544
+ processed_defs[name] = _replace_ref_with_defs(schema)
545
+
546
+ # Convert OpenAPI schema definitions to JSON Schema format if needed
547
+ if openapi_version and openapi_version.startswith("3.0"):
548
+ from .json_schema_converter import convert_openapi_schema_to_json_schema
549
+
550
+ for def_name in list(processed_defs.keys()):
485
551
  processed_defs[def_name] = convert_openapi_schema_to_json_schema(
486
- def_schema, openapi_version
552
+ processed_defs[def_name], openapi_version
487
553
  )
488
- else:
489
- processed_defs[def_name] = def_schema
490
- output_schema["$defs"] = processed_defs
491
554
 
492
- # Use lightweight compression - prune additionalProperties and unused definitions
493
- if output_schema.get("additionalProperties") is False:
494
- output_schema.pop("additionalProperties")
495
-
496
- # Remove unused definitions (lightweight approach - just check direct $ref usage)
497
- if "$defs" in output_schema:
498
- used_refs = set()
499
-
500
- def find_refs_in_value(value):
501
- if isinstance(value, dict):
502
- if "$ref" in value and isinstance(value["$ref"], str):
503
- ref = value["$ref"]
504
- if ref.startswith("#/$defs/"):
505
- used_refs.add(ref.split("/")[-1])
506
- for v in value.values():
507
- find_refs_in_value(v)
508
- elif isinstance(value, list):
509
- for item in value:
510
- find_refs_in_value(item)
511
-
512
- # Find refs in the main schema (excluding $defs section)
513
- for key, value in output_schema.items():
514
- if key != "$defs":
515
- find_refs_in_value(value)
516
-
517
- # Remove unused definitions
518
- if used_refs:
519
- output_schema["$defs"] = {
520
- name: def_schema
521
- for name, def_schema in output_schema["$defs"].items()
522
- if name in used_refs
523
- }
524
- else:
525
- output_schema.pop("$defs")
555
+ output_schema["$defs"] = processed_defs
526
556
 
527
557
  return output_schema
528
558
 
@@ -533,6 +563,5 @@ __all__ = [
533
563
  "_combine_schemas",
534
564
  "_combine_schemas_and_map_params",
535
565
  "extract_output_schema_from_responses",
536
- "_replace_ref_with_defs",
537
566
  "_make_optional_parameter_nullable",
538
567
  ]
fastmcp/mcp_config.py CHANGED
@@ -27,7 +27,7 @@ from __future__ import annotations
27
27
  import datetime
28
28
  import re
29
29
  from pathlib import Path
30
- from typing import TYPE_CHECKING, Annotated, Any, Literal
30
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
31
31
  from urllib.parse import urlparse
32
32
 
33
33
  import httpx
@@ -36,7 +36,6 @@ from pydantic import (
36
36
  BaseModel,
37
37
  ConfigDict,
38
38
  Field,
39
- ValidationInfo,
40
39
  model_validator,
41
40
  )
42
41
  from typing_extensions import Self, override
@@ -47,11 +46,11 @@ from fastmcp.utilities.types import FastMCPBaseModel
47
46
  if TYPE_CHECKING:
48
47
  from fastmcp.client.transports import (
49
48
  ClientTransport,
50
- FastMCPTransport,
51
49
  SSETransport,
52
50
  StdioTransport,
53
51
  StreamableHttpTransport,
54
52
  )
53
+ from fastmcp.server.server import FastMCP
55
54
 
56
55
 
57
56
  def infer_transport_type_from_url(
@@ -90,21 +89,38 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
90
89
  description="The tags to exclude in the proxy.",
91
90
  )
92
91
 
93
- def to_transport(self) -> FastMCPTransport:
94
- """Get the transport for the server."""
95
- from fastmcp.client.transports import FastMCPTransport
96
- from fastmcp.server.server import FastMCP
92
+ def _to_server_and_underlying_transport(
93
+ self,
94
+ server_name: str | None = None,
95
+ client_name: str | None = None,
96
+ ) -> tuple[FastMCP[Any], ClientTransport]:
97
+ """Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport."""
98
+ from fastmcp import FastMCP
99
+ from fastmcp.client import Client
100
+ from fastmcp.client.transports import (
101
+ ClientTransport, # pyright: ignore[reportUnusedImport]
102
+ )
97
103
 
98
104
  transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
105
+ transport = cast(ClientTransport, transport)
106
+
107
+ client: Client[ClientTransport] = Client(transport=transport, name=client_name)
99
108
 
100
109
  wrapped_mcp_server = FastMCP.as_proxy(
101
- transport,
110
+ name=server_name,
111
+ backend=client,
102
112
  tool_transformations=self.tools,
103
113
  include_tags=self.include_tags,
104
114
  exclude_tags=self.exclude_tags,
105
115
  )
106
116
 
107
- return FastMCPTransport(wrapped_mcp_server)
117
+ return wrapped_mcp_server, transport
118
+
119
+ def to_transport(self) -> ClientTransport:
120
+ """Get the transport for the transforming MCP server."""
121
+ from fastmcp.client.transports import FastMCPTransport
122
+
123
+ return FastMCPTransport(mcp=self._to_server_and_underlying_transport()[0])
108
124
 
109
125
 
110
126
  class StdioMCPServer(BaseModel):
@@ -232,20 +248,24 @@ class MCPConfig(BaseModel):
232
248
  For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
233
249
  """
234
250
 
235
- mcpServers: dict[str, MCPServerTypes]
251
+ mcpServers: dict[str, MCPServerTypes] = Field(default_factory=dict)
236
252
 
237
253
  model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields
238
254
 
239
255
  @model_validator(mode="before")
240
- def validate_mcp_servers(self, info: ValidationInfo) -> dict[str, Any]:
241
- """Validate the MCP servers."""
242
- if not isinstance(self, dict):
243
- raise ValueError("MCPConfig format requires a dictionary of servers.")
244
-
245
- if "mcpServers" not in self:
246
- self = {"mcpServers": self}
247
-
248
- return self
256
+ @classmethod
257
+ def wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]:
258
+ """If there's no mcpServers key but there are server configs at root, wrap them."""
259
+ if "mcpServers" not in values:
260
+ # Check if any values look like server configs
261
+ has_servers = any(
262
+ isinstance(v, dict) and ("command" in v or "url" in v)
263
+ for v in values.values()
264
+ )
265
+ if has_servers:
266
+ # Move all server-like configs under mcpServers
267
+ return {"mcpServers": values}
268
+ return values
249
269
 
250
270
  def add_server(self, name: str, server: MCPServerTypes) -> None:
251
271
  """Add or update a server in the configuration."""
@@ -282,7 +302,7 @@ class CanonicalMCPConfig(MCPConfig):
282
302
  The format is designed to be client-agnostic and extensible for future use cases.
283
303
  """
284
304
 
285
- mcpServers: dict[str, CanonicalMCPServerTypes]
305
+ mcpServers: dict[str, CanonicalMCPServerTypes] = Field(default_factory=dict)
286
306
 
287
307
  @override
288
308
  def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:
@@ -69,8 +69,8 @@ class PromptManager:
69
69
  child_dict = {p.key: p for p in child_results}
70
70
  if mounted.prefix:
71
71
  for prompt in child_dict.values():
72
- prefixed_prompt = prompt.with_key(
73
- f"{mounted.prefix}_{prompt.key}"
72
+ prefixed_prompt = prompt.model_copy(
73
+ key=f"{mounted.prefix}_{prompt.key}"
74
74
  )
75
75
  all_prompts[prefixed_prompt.key] = prefixed_prompt
76
76
  else:
@@ -80,6 +80,8 @@ class PromptManager:
80
80
  logger.warning(
81
81
  f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
82
82
  )
83
+ if settings.mounted_components_raise_on_load_error:
84
+ raise
83
85
  continue
84
86
 
85
87
  # Finally, add local prompts, which always take precedence
@@ -101,8 +101,11 @@ class ResourceManager:
101
101
  prefixed_uri = add_resource_prefix(
102
102
  uri, mounted.prefix, mounted.resource_prefix_format
103
103
  )
104
- # Create a copy of the resource with the prefixed key
105
- prefixed_resource = resource.with_key(prefixed_uri)
104
+ # Create a copy of the resource with the prefixed key and name
105
+ prefixed_resource = resource.model_copy(
106
+ update={"name": f"{mounted.prefix}_{resource.name}"},
107
+ key=prefixed_uri,
108
+ )
106
109
  all_resources[prefixed_uri] = prefixed_resource
107
110
  else:
108
111
  all_resources.update(child_resources)
@@ -111,6 +114,8 @@ class ResourceManager:
111
114
  logger.warning(
112
115
  f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
113
116
  )
117
+ if settings.mounted_components_raise_on_load_error:
118
+ raise
114
119
  continue
115
120
 
116
121
  # Finally, add local resources, which always take precedence
@@ -149,8 +154,11 @@ class ResourceManager:
149
154
  prefixed_uri_template = add_resource_prefix(
150
155
  uri_template, mounted.prefix, mounted.resource_prefix_format
151
156
  )
152
- # Create a copy of the template with the prefixed key
153
- prefixed_template = template.with_key(prefixed_uri_template)
157
+ # Create a copy of the template with the prefixed key and name
158
+ prefixed_template = template.model_copy(
159
+ update={"name": f"{mounted.prefix}_{template.name}"},
160
+ key=prefixed_uri_template,
161
+ )
154
162
  all_templates[prefixed_uri_template] = prefixed_template
155
163
  else:
156
164
  all_templates.update(child_dict)
@@ -159,6 +167,8 @@ class ResourceManager:
159
167
  logger.warning(
160
168
  f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
161
169
  )
170
+ if settings.mounted_components_raise_on_load_error:
171
+ raise
162
172
  continue
163
173
 
164
174
  # Finally, add local templates, which always take precedence
@@ -273,7 +283,7 @@ class ResourceManager:
273
283
  Args:
274
284
  resource: A Resource instance to add. The resource's .key attribute
275
285
  will be used as the storage key. To overwrite it, call
276
- Resource.with_key() before calling this method.
286
+ Resource.model_copy(key=new_key) before calling this method.
277
287
  """
278
288
  existing = self._resources.get(resource.key)
279
289
  if existing:
@@ -322,7 +332,7 @@ class ResourceManager:
322
332
  Args:
323
333
  template: A ResourceTemplate instance to add. The template's .key attribute
324
334
  will be used as the storage key. To overwrite it, call
325
- ResourceTemplate.with_key() before calling this method.
335
+ ResourceTemplate.model_copy(key=new_key) before calling this method.
326
336
 
327
337
  Returns:
328
338
  The added template. If a template with the same URI already exists,
@@ -1,13 +1,23 @@
1
- from .auth import OAuthProvider, TokenVerifier, RemoteAuthProvider
1
+ from .auth import (
2
+ OAuthProvider,
3
+ TokenVerifier,
4
+ RemoteAuthProvider,
5
+ AccessToken,
6
+ AuthProvider,
7
+ )
2
8
  from .providers.jwt import JWTVerifier, StaticTokenVerifier
9
+ from .oauth_proxy import OAuthProxy
3
10
 
4
11
 
5
12
  __all__ = [
13
+ "AuthProvider",
6
14
  "OAuthProvider",
7
15
  "TokenVerifier",
8
16
  "JWTVerifier",
9
17
  "StaticTokenVerifier",
10
18
  "RemoteAuthProvider",
19
+ "AccessToken",
20
+ "OAuthProxy",
11
21
  ]
12
22
 
13
23
 
@@ -1,7 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any
4
+
5
+ from mcp.server.auth.provider import (
6
+ AccessToken as _SDKAccessToken,
7
+ )
3
8
  from mcp.server.auth.provider import (
4
- AccessToken,
5
9
  AuthorizationCode,
6
10
  OAuthAuthorizationServerProvider,
7
11
  RefreshToken,
@@ -21,6 +25,12 @@ from pydantic import AnyHttpUrl
21
25
  from starlette.routing import Route
22
26
 
23
27
 
28
+ class AccessToken(_SDKAccessToken):
29
+ """AccessToken that includes all JWT claims."""
30
+
31
+ claims: dict[str, Any] = {}
32
+
33
+
24
34
  class AuthProvider(TokenVerifierProtocol):
25
35
  """Base class for all FastMCP authentication providers.
26
36
 
@@ -125,11 +135,15 @@ class RemoteAuthProvider(AuthProvider):
125
135
  the authorization servers that issue valid tokens.
126
136
  """
127
137
 
138
+ resource_server_url: AnyHttpUrl
139
+
128
140
  def __init__(
129
141
  self,
130
142
  token_verifier: TokenVerifier,
131
143
  authorization_servers: list[AnyHttpUrl],
132
144
  resource_server_url: AnyHttpUrl | str,
145
+ resource_name: str | None = None,
146
+ resource_documentation: AnyHttpUrl | None = None,
133
147
  ):
134
148
  """Initialize the remote auth provider.
135
149
 
@@ -143,6 +157,8 @@ class RemoteAuthProvider(AuthProvider):
143
157
  super().__init__(resource_server_url=resource_server_url)
144
158
  self.token_verifier = token_verifier
145
159
  self.authorization_servers = authorization_servers
160
+ self.resource_name = resource_name
161
+ self.resource_documentation = resource_documentation
146
162
 
147
163
  async def verify_token(self, token: str) -> AccessToken | None:
148
164
  """Verify token using the configured token verifier."""
@@ -155,12 +171,13 @@ class RemoteAuthProvider(AuthProvider):
155
171
  Subclasses can override this method to add additional routes by calling
156
172
  super().get_routes() and extending the returned list.
157
173
  """
158
- assert self.resource_server_url is not None
159
174
 
160
175
  return create_protected_resource_routes(
161
176
  resource_url=self.resource_server_url,
162
177
  authorization_servers=self.authorization_servers,
163
178
  scopes_supported=self.token_verifier.required_scopes,
179
+ resource_name=self.resource_name,
180
+ resource_documentation=self.resource_documentation,
164
181
  )
165
182
 
166
183