fastmcp 2.12.5__py3-none-any.whl → 2.13.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py CHANGED
@@ -4,12 +4,11 @@ from __future__ import annotations as _annotations
4
4
 
5
5
  import inspect
6
6
  import json
7
- from abc import ABC, abstractmethod
8
7
  from collections.abc import Awaitable, Callable, Sequence
9
8
  from typing import Any
10
9
 
11
10
  import pydantic_core
12
- from mcp.types import ContentBlock, PromptMessage, Role, TextContent
11
+ from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
13
12
  from mcp.types import Prompt as MCPPrompt
14
13
  from mcp.types import PromptArgument as MCPPromptArgument
15
14
  from pydantic import Field, TypeAdapter
@@ -62,7 +61,7 @@ class PromptArgument(FastMCPBaseModel):
62
61
  )
63
62
 
64
63
 
65
- class Prompt(FastMCPComponent, ABC):
64
+ class Prompt(FastMCPComponent):
66
65
  """A prompt template that can be rendered with parameters."""
67
66
 
68
67
  arguments: list[PromptArgument] | None = Field(
@@ -106,6 +105,7 @@ class Prompt(FastMCPComponent, ABC):
106
105
  description=overrides.get("description", self.description),
107
106
  arguments=arguments,
108
107
  title=overrides.get("title", self.title),
108
+ icons=overrides.get("icons", self.icons),
109
109
  _meta=overrides.get(
110
110
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
111
111
  ),
@@ -117,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
117
117
  name: str | None = None,
118
118
  title: str | None = None,
119
119
  description: str | None = None,
120
+ icons: list[Icon] | None = None,
120
121
  tags: set[str] | None = None,
121
122
  enabled: bool | None = None,
122
123
  meta: dict[str, Any] | None = None,
@@ -134,18 +135,22 @@ class Prompt(FastMCPComponent, ABC):
134
135
  name=name,
135
136
  title=title,
136
137
  description=description,
138
+ icons=icons,
137
139
  tags=tags,
138
140
  enabled=enabled,
139
141
  meta=meta,
140
142
  )
141
143
 
142
- @abstractmethod
143
144
  async def render(
144
145
  self,
145
146
  arguments: dict[str, Any] | None = None,
146
147
  ) -> list[PromptMessage]:
147
- """Render the prompt with arguments."""
148
- raise NotImplementedError("Prompt.render() must be implemented by subclasses")
148
+ """Render the prompt with arguments.
149
+
150
+ This method is not implemented in the base Prompt class and must be
151
+ implemented by subclasses.
152
+ """
153
+ raise NotImplementedError("Subclasses must implement render()")
149
154
 
150
155
 
151
156
  class FunctionPrompt(Prompt):
@@ -160,6 +165,7 @@ class FunctionPrompt(Prompt):
160
165
  name: str | None = None,
161
166
  title: str | None = None,
162
167
  description: str | None = None,
168
+ icons: list[Icon] | None = None,
163
169
  tags: set[str] | None = None,
164
170
  enabled: bool | None = None,
165
171
  meta: dict[str, Any] | None = None,
@@ -201,10 +207,7 @@ class FunctionPrompt(Prompt):
201
207
  # Auto-detect context parameter if not provided
202
208
 
203
209
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
204
- if context_kwarg:
205
- prune_params = [context_kwarg]
206
- else:
207
- prune_params = None
210
+ prune_params = [context_kwarg] if context_kwarg else None
208
211
 
209
212
  parameters = compress_schema(parameters, prune_params=prune_params)
210
213
 
@@ -253,6 +256,7 @@ class FunctionPrompt(Prompt):
253
256
  name=func_name,
254
257
  title=title,
255
258
  description=description,
259
+ icons=icons,
256
260
  arguments=arguments,
257
261
  tags=tags or set(),
258
262
  enabled=enabled if enabled is not None else True,
@@ -283,10 +287,7 @@ class FunctionPrompt(Prompt):
283
287
  if (
284
288
  param.annotation == inspect.Parameter.empty
285
289
  or param.annotation is str
286
- ):
287
- converted_kwargs[param_name] = param_value
288
- # If argument is not a string, pass as-is (already properly typed)
289
- elif not isinstance(param_value, str):
290
+ ) or not isinstance(param_value, str):
290
291
  converted_kwargs[param_name] = param_value
291
292
  else:
292
293
  # Try to convert string argument using type adapter
@@ -307,7 +308,7 @@ class FunctionPrompt(Prompt):
307
308
  raise PromptError(
308
309
  f"Could not convert argument '{param_name}' with value '{param_value}' "
309
310
  f"to expected type {param.annotation}. Error: {e}"
310
- )
311
+ ) from e
311
312
  else:
312
313
  # Parameter not in function signature, pass as-is
313
314
  converted_kwargs[param_name] = param_value
@@ -369,10 +370,12 @@ class FunctionPrompt(Prompt):
369
370
  content=TextContent(type="text", text=content),
370
371
  )
371
372
  )
372
- except Exception:
373
- raise PromptError("Could not convert prompt result to message.")
373
+ except Exception as e:
374
+ raise PromptError(
375
+ "Could not convert prompt result to message."
376
+ ) from e
374
377
 
375
378
  return messages
376
- except Exception:
379
+ except Exception as e:
377
380
  logger.exception(f"Error rendering prompt {self.name}")
378
- raise PromptError(f"Error rendering prompt {self.name}.")
381
+ raise PromptError(f"Error rendering prompt {self.name}.") from e
@@ -2,7 +2,7 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import warnings
4
4
  from collections.abc import Awaitable, Callable
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import Any
6
6
 
7
7
  from mcp import GetPromptResult
8
8
 
@@ -12,9 +12,6 @@ from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
12
  from fastmcp.settings import DuplicateBehavior
13
13
  from fastmcp.utilities.logging import get_logger
14
14
 
15
- if TYPE_CHECKING:
16
- from fastmcp.server.server import MountedServer
17
-
18
15
  logger = get_logger(__name__)
19
16
 
20
17
 
@@ -27,7 +24,6 @@ class PromptManager:
27
24
  mask_error_details: bool | None = None,
28
25
  ):
29
26
  self._prompts: dict[str, Prompt] = {}
30
- self._mounted_servers: list[MountedServer] = []
31
27
  self.mask_error_details = mask_error_details or settings.mask_error_details
32
28
 
33
29
  # Default to "warn" if None is provided
@@ -42,52 +38,6 @@ class PromptManager:
42
38
 
43
39
  self.duplicate_behavior = duplicate_behavior
44
40
 
45
- def mount(self, server: MountedServer) -> None:
46
- """Adds a mounted server as a source for prompts."""
47
- self._mounted_servers.append(server)
48
-
49
- async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
50
- """
51
- The single, consolidated recursive method for fetching prompts. The 'via_server'
52
- parameter determines the communication path.
53
-
54
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
55
- - via_server=True: Server-to-server path for filtered MCP requests
56
- """
57
- all_prompts: dict[str, Prompt] = {}
58
-
59
- for mounted in self._mounted_servers:
60
- try:
61
- if via_server:
62
- # Use the server-to-server filtered path
63
- child_results = await mounted.server._list_prompts()
64
- else:
65
- # Use the manager-to-manager unfiltered path
66
- child_results = await mounted.server._prompt_manager.list_prompts()
67
-
68
- # The combination logic is the same for both paths
69
- child_dict = {p.key: p for p in child_results}
70
- if mounted.prefix:
71
- for prompt in child_dict.values():
72
- prefixed_prompt = prompt.model_copy(
73
- key=f"{mounted.prefix}_{prompt.key}"
74
- )
75
- all_prompts[prefixed_prompt.key] = prefixed_prompt
76
- else:
77
- all_prompts.update(child_dict)
78
- except Exception as e:
79
- # Skip failed mounts silently, matches existing behavior
80
- logger.warning(
81
- f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
82
- )
83
- if settings.mounted_components_raise_on_load_error:
84
- raise
85
- continue
86
-
87
- # Finally, add local prompts, which always take precedence
88
- all_prompts.update(self._prompts)
89
- return all_prompts
90
-
91
41
  async def has_prompt(self, key: str) -> bool:
92
42
  """Check if a prompt exists."""
93
43
  prompts = await self.get_prompts()
@@ -102,16 +52,9 @@ class PromptManager:
102
52
 
103
53
  async def get_prompts(self) -> dict[str, Prompt]:
104
54
  """
105
- Gets the complete, unfiltered inventory of all prompts.
106
- """
107
- return await self._load_prompts(via_server=False)
108
-
109
- async def list_prompts(self) -> list[Prompt]:
110
- """
111
- Lists all prompts, applying protocol filtering.
55
+ Gets the complete, unfiltered inventory of local prompts.
112
56
  """
113
- prompts_dict = await self._load_prompts(via_server=True)
114
- return list(prompts_dict.values())
57
+ return dict(self._prompts)
115
58
 
116
59
  def add_prompt_from_fn(
117
60
  self,
@@ -160,44 +103,16 @@ class PromptManager:
160
103
  Internal API for servers: Finds and renders a prompt, respecting the
161
104
  filtered protocol path.
162
105
  """
163
- # 1. Check local prompts first. The server will have already applied its filter.
164
- if name in self._prompts:
165
- prompt = await self.get_prompt(name)
166
- if not prompt:
167
- raise NotFoundError(f"Unknown prompt: {name}")
168
-
169
- try:
170
- messages = await prompt.render(arguments)
171
- return GetPromptResult(
172
- description=prompt.description, messages=messages
173
- )
174
-
175
- # Pass through PromptErrors as-is
176
- except PromptError as e:
177
- logger.exception(f"Error rendering prompt {name!r}")
178
- raise e
179
-
180
- # Handle other exceptions
181
- except Exception as e:
182
- logger.exception(f"Error rendering prompt {name!r}")
183
- if self.mask_error_details:
184
- # Mask internal details
185
- raise PromptError(f"Error rendering prompt {name!r}") from e
186
- else:
187
- # Include original error details
188
- raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
189
-
190
- # 2. Check mounted servers using the filtered protocol path.
191
- for mounted in reversed(self._mounted_servers):
192
- prompt_key = name
193
- if mounted.prefix:
194
- if name.startswith(f"{mounted.prefix}_"):
195
- prompt_key = name.removeprefix(f"{mounted.prefix}_")
196
- else:
197
- continue
198
- try:
199
- return await mounted.server._get_prompt(prompt_key, arguments)
200
- except NotFoundError:
201
- continue
202
-
203
- raise NotFoundError(f"Unknown prompt: {name}")
106
+ prompt = await self.get_prompt(name)
107
+ try:
108
+ messages = await prompt.render(arguments)
109
+ return GetPromptResult(description=prompt.description, messages=messages)
110
+ except PromptError as e:
111
+ logger.exception(f"Error rendering prompt {name!r}")
112
+ raise e
113
+ except Exception as e:
114
+ logger.exception(f"Error rendering prompt {name!r}")
115
+ if self.mask_error_details:
116
+ raise PromptError(f"Error rendering prompt {name!r}") from e
117
+ else:
118
+ raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
@@ -10,13 +10,13 @@ from .types import (
10
10
  from .resource_manager import ResourceManager
11
11
 
12
12
  __all__ = [
13
- "Resource",
14
- "TextResource",
15
13
  "BinaryResource",
16
- "FunctionResource",
14
+ "DirectoryResource",
17
15
  "FileResource",
16
+ "FunctionResource",
18
17
  "HttpResource",
19
- "DirectoryResource",
20
- "ResourceTemplate",
18
+ "Resource",
21
19
  "ResourceManager",
20
+ "ResourceTemplate",
21
+ "TextResource",
22
22
  ]
@@ -2,13 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import abc
6
5
  import inspect
7
6
  from collections.abc import Callable
8
7
  from typing import TYPE_CHECKING, Annotated, Any
9
8
 
10
9
  import pydantic_core
11
- from mcp.types import Annotations
10
+ from mcp.types import Annotations, Icon
12
11
  from mcp.types import Resource as MCPResource
13
12
  from pydantic import (
14
13
  AnyUrl,
@@ -31,7 +30,7 @@ if TYPE_CHECKING:
31
30
  pass
32
31
 
33
32
 
34
- class Resource(FastMCPComponent, abc.ABC):
33
+ class Resource(FastMCPComponent):
35
34
  """Base class for all resources."""
36
35
 
37
36
  model_config = ConfigDict(validate_default=True)
@@ -73,6 +72,7 @@ class Resource(FastMCPComponent, abc.ABC):
73
72
  name: str | None = None,
74
73
  title: str | None = None,
75
74
  description: str | None = None,
75
+ icons: list[Icon] | None = None,
76
76
  mime_type: str | None = None,
77
77
  tags: set[str] | None = None,
78
78
  enabled: bool | None = None,
@@ -85,6 +85,7 @@ class Resource(FastMCPComponent, abc.ABC):
85
85
  name=name,
86
86
  title=title,
87
87
  description=description,
88
+ icons=icons,
88
89
  mime_type=mime_type,
89
90
  tags=tags,
90
91
  enabled=enabled,
@@ -111,10 +112,13 @@ class Resource(FastMCPComponent, abc.ABC):
111
112
  raise ValueError("Either name or uri must be provided")
112
113
  return self
113
114
 
114
- @abc.abstractmethod
115
115
  async def read(self) -> str | bytes:
116
- """Read the resource content."""
117
- pass
116
+ """Read the resource content.
117
+
118
+ This method is not implemented in the base Resource class and must be
119
+ implemented by subclasses.
120
+ """
121
+ raise NotImplementedError("Subclasses must implement read()")
118
122
 
119
123
  def to_mcp_resource(
120
124
  self,
@@ -130,6 +134,7 @@ class Resource(FastMCPComponent, abc.ABC):
130
134
  description=overrides.get("description", self.description),
131
135
  mimeType=overrides.get("mimeType", self.mime_type),
132
136
  title=overrides.get("title", self.title),
137
+ icons=overrides.get("icons", self.icons),
133
138
  annotations=overrides.get("annotations", self.annotations),
134
139
  _meta=overrides.get(
135
140
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -173,6 +178,7 @@ class FunctionResource(Resource):
173
178
  name: str | None = None,
174
179
  title: str | None = None,
175
180
  description: str | None = None,
181
+ icons: list[Icon] | None = None,
176
182
  mime_type: str | None = None,
177
183
  tags: set[str] | None = None,
178
184
  enabled: bool | None = None,
@@ -188,6 +194,7 @@ class FunctionResource(Resource):
188
194
  name=name or get_fn_name(fn),
189
195
  title=title,
190
196
  description=description or inspect.getdoc(fn),
197
+ icons=icons,
191
198
  mime_type=mime_type or "text/plain",
192
199
  tags=tags or set(),
193
200
  enabled=enabled if enabled is not None else True,
@@ -210,9 +217,7 @@ class FunctionResource(Resource):
210
217
 
211
218
  if isinstance(result, Resource):
212
219
  return await result.read()
213
- elif isinstance(result, bytes):
214
- return result
215
- elif isinstance(result, str):
220
+ elif isinstance(result, bytes | str):
216
221
  return result
217
222
  else:
218
223
  return pydantic_core.to_json(result, fallback=str).decode()
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import warnings
7
7
  from collections.abc import Callable
8
- from typing import TYPE_CHECKING, Any
8
+ from typing import Any
9
9
 
10
10
  from pydantic import AnyUrl
11
11
 
@@ -19,9 +19,6 @@ from fastmcp.resources.template import (
19
19
  from fastmcp.settings import DuplicateBehavior
20
20
  from fastmcp.utilities.logging import get_logger
21
21
 
22
- if TYPE_CHECKING:
23
- from fastmcp.server.server import MountedServer
24
-
25
22
  logger = get_logger(__name__)
26
23
 
27
24
 
@@ -43,7 +40,6 @@ class ResourceManager:
43
40
  """
44
41
  self._resources: dict[str, Resource] = {}
45
42
  self._templates: dict[str, ResourceTemplate] = {}
46
- self._mounted_servers: list[MountedServer] = []
47
43
  self.mask_error_details = mask_error_details or settings.mask_error_details
48
44
 
49
45
  # Default to "warn" if None is provided
@@ -57,137 +53,13 @@ class ResourceManager:
57
53
  )
58
54
  self.duplicate_behavior = duplicate_behavior
59
55
 
60
- def mount(self, server: MountedServer) -> None:
61
- """Adds a mounted server as a source for resources and templates."""
62
- self._mounted_servers.append(server)
63
-
64
56
  async def get_resources(self) -> dict[str, Resource]:
65
57
  """Get all registered resources, keyed by URI."""
66
- return await self._load_resources(via_server=False)
58
+ return dict(self._resources)
67
59
 
68
60
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
69
61
  """Get all registered templates, keyed by URI template."""
70
- return await self._load_resource_templates(via_server=False)
71
-
72
- async def _load_resources(self, *, via_server: bool = False) -> dict[str, Resource]:
73
- """
74
- The single, consolidated recursive method for fetching resources. The 'via_server'
75
- parameter determines the communication path.
76
-
77
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
78
- - via_server=True: Server-to-server path for filtered MCP requests
79
- """
80
- all_resources: dict[str, Resource] = {}
81
-
82
- for mounted in self._mounted_servers:
83
- try:
84
- if via_server:
85
- # Use the server-to-server filtered path
86
- child_resources_list = await mounted.server._list_resources()
87
- child_resources = {
88
- resource.key: resource for resource in child_resources_list
89
- }
90
- else:
91
- # Use the manager-to-manager unfiltered path
92
- child_resources = (
93
- await mounted.server._resource_manager.get_resources()
94
- )
95
-
96
- # Apply prefix if needed
97
- if mounted.prefix:
98
- from fastmcp.server.server import add_resource_prefix
99
-
100
- for uri, resource in child_resources.items():
101
- prefixed_uri = add_resource_prefix(
102
- uri, mounted.prefix, mounted.resource_prefix_format
103
- )
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
- )
109
- all_resources[prefixed_uri] = prefixed_resource
110
- else:
111
- all_resources.update(child_resources)
112
- except Exception as e:
113
- # Skip failed mounts silently, matches existing behavior
114
- logger.warning(
115
- f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
116
- )
117
- if settings.mounted_components_raise_on_load_error:
118
- raise
119
- continue
120
-
121
- # Finally, add local resources, which always take precedence
122
- all_resources.update(self._resources)
123
- return all_resources
124
-
125
- async def _load_resource_templates(
126
- self, *, via_server: bool = False
127
- ) -> dict[str, ResourceTemplate]:
128
- """
129
- The single, consolidated recursive method for fetching templates. The 'via_server'
130
- parameter determines the communication path.
131
-
132
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
133
- - via_server=True: Server-to-server path for filtered MCP requests
134
- """
135
- all_templates: dict[str, ResourceTemplate] = {}
136
-
137
- for mounted in self._mounted_servers:
138
- try:
139
- if via_server:
140
- # Use the server-to-server filtered path
141
- child_templates = await mounted.server._list_resource_templates()
142
- else:
143
- # Use the manager-to-manager unfiltered path
144
- child_templates = (
145
- await mounted.server._resource_manager.list_resource_templates()
146
- )
147
- child_dict = {template.key: template for template in child_templates}
148
-
149
- # Apply prefix if needed
150
- if mounted.prefix:
151
- from fastmcp.server.server import add_resource_prefix
152
-
153
- for uri_template, template in child_dict.items():
154
- prefixed_uri_template = add_resource_prefix(
155
- uri_template, mounted.prefix, mounted.resource_prefix_format
156
- )
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
- )
162
- all_templates[prefixed_uri_template] = prefixed_template
163
- else:
164
- all_templates.update(child_dict)
165
- except Exception as e:
166
- # Skip failed mounts silently, matches existing behavior
167
- logger.warning(
168
- f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
169
- )
170
- if settings.mounted_components_raise_on_load_error:
171
- raise
172
- continue
173
-
174
- # Finally, add local templates, which always take precedence
175
- all_templates.update(self._templates)
176
- return all_templates
177
-
178
- async def list_resources(self) -> list[Resource]:
179
- """
180
- Lists all resources, applying protocol filtering.
181
- """
182
- resources_dict = await self._load_resources(via_server=True)
183
- return list(resources_dict.values())
184
-
185
- async def list_resource_templates(self) -> list[ResourceTemplate]:
186
- """
187
- Lists all templates, applying protocol filtering.
188
- """
189
- templates_dict = await self._load_resource_templates(via_server=True)
190
- return list(templates_dict.values())
62
+ return dict(self._templates)
191
63
 
192
64
  def add_resource_or_template_from_fn(
193
65
  self,
@@ -363,8 +235,8 @@ class ResourceManager:
363
235
 
364
236
  # Then check templates (local and mounted) only if not found in concrete resources
365
237
  templates = await self.get_resource_templates()
366
- for template_key in templates.keys():
367
- if match_uri_template(uri_str, template_key):
238
+ for template_key in templates:
239
+ if match_uri_template(uri_str, template_key) is not None:
368
240
  return True
369
241
 
370
242
  return False
@@ -381,16 +253,16 @@ class ResourceManager:
381
253
  uri_str = str(uri)
382
254
  logger.debug("Getting resource", extra={"uri": uri_str})
383
255
 
384
- # First check concrete resources (local and mounted)
256
+ # First check concrete resources
385
257
  resources = await self.get_resources()
386
258
  if resource := resources.get(uri_str):
387
259
  return resource
388
260
 
389
- # Then check templates (local and mounted) - use the utility function to match against storage keys
261
+ # Then check templates
390
262
  templates = await self.get_resource_templates()
391
263
  for storage_key, template in templates.items():
392
264
  # Try to match against the storage key (which might be a custom key)
393
- if params := match_uri_template(uri_str, storage_key):
265
+ if (params := match_uri_template(uri_str, storage_key)) is not None:
394
266
  try:
395
267
  return await template.create_resource(
396
268
  uri_str,
@@ -424,9 +296,6 @@ class ResourceManager:
424
296
  # 1. Check local resources first. The server will have already applied its filter.
425
297
  if uri_str in self._resources:
426
298
  resource = await self.get_resource(uri_str)
427
- if not resource:
428
- raise NotFoundError(f"Resource {uri_str!r} not found")
429
-
430
299
  try:
431
300
  return await resource.read()
432
301
 
@@ -449,7 +318,7 @@ class ResourceManager:
449
318
 
450
319
  # 1b. Check local templates if not found in concrete resources
451
320
  for key, template in self._templates.items():
452
- if params := match_uri_template(uri_str, key):
321
+ if (params := match_uri_template(uri_str, key)) is not None:
453
322
  try:
454
323
  resource = await template.create_resource(uri_str, params=params)
455
324
  return await resource.read()
@@ -471,32 +340,4 @@ class ResourceManager:
471
340
  f"Error reading resource from template {uri_str!r}: {e}"
472
341
  ) from e
473
342
 
474
- # 2. Check mounted servers using the filtered protocol path.
475
- from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
476
-
477
- for mounted in reversed(self._mounted_servers):
478
- key = uri_str
479
- try:
480
- if mounted.prefix:
481
- if has_resource_prefix(
482
- key,
483
- mounted.prefix,
484
- mounted.resource_prefix_format,
485
- ):
486
- key = remove_resource_prefix(
487
- key,
488
- mounted.prefix,
489
- mounted.resource_prefix_format,
490
- )
491
- else:
492
- continue
493
-
494
- try:
495
- result = await mounted.server._read_resource(key)
496
- return result[0].content
497
- except NotFoundError:
498
- continue
499
- except NotFoundError:
500
- continue
501
-
502
343
  raise NotFoundError(f"Resource {uri_str!r} not found.")