fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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 (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,209 +0,0 @@
1
- """
2
- ComponentService: Provides async management of tools, resources, and prompts for FastMCP servers.
3
- Handles enabling/disabling components both locally and across mounted servers.
4
- """
5
-
6
- from fastmcp.exceptions import NotFoundError
7
- from fastmcp.prompts.prompt import Prompt
8
- from fastmcp.resources.resource import Resource
9
- from fastmcp.resources.template import ResourceTemplate
10
- from fastmcp.server.server import FastMCP, has_resource_prefix, remove_resource_prefix
11
- from fastmcp.tools.tool import Tool
12
- from fastmcp.utilities.logging import get_logger
13
-
14
- logger = get_logger(__name__)
15
-
16
-
17
- class ComponentService:
18
- """Service for managing components like tools, resources, and prompts."""
19
-
20
- def __init__(self, server: FastMCP):
21
- self._server = server
22
- self._tool_manager = server._tool_manager
23
- self._resource_manager = server._resource_manager
24
- self._prompt_manager = server._prompt_manager
25
-
26
- async def _enable_tool(self, key: str) -> Tool:
27
- """Handle 'enableTool' requests.
28
-
29
- Args:
30
- key: The key of the tool to enable
31
-
32
- Returns:
33
- The tool that was enabled
34
- """
35
- logger.debug("Enabling tool: %s", key)
36
-
37
- # 1. Check local tools first. The server will have already applied its filter.
38
- if key in self._server._tool_manager._tools:
39
- tool: Tool = await self._server.get_tool(key)
40
- tool.enable()
41
- return tool
42
-
43
- # 2. Check mounted servers using the filtered protocol path.
44
- for mounted in reversed(self._server._mounted_servers):
45
- if mounted.prefix:
46
- if key.startswith(f"{mounted.prefix}_"):
47
- tool_key = key.removeprefix(f"{mounted.prefix}_")
48
- mounted_service = ComponentService(mounted.server)
49
- tool = await mounted_service._enable_tool(tool_key)
50
- return tool
51
- else:
52
- continue
53
- raise NotFoundError(f"Unknown tool: {key}")
54
-
55
- async def _disable_tool(self, key: str) -> Tool:
56
- """Handle 'disableTool' requests.
57
-
58
- Args:
59
- key: The key of the tool to disable
60
-
61
- Returns:
62
- The tool that was disabled
63
- """
64
- logger.debug("Disable tool: %s", key)
65
-
66
- # 1. Check local tools first. The server will have already applied its filter.
67
- if key in self._server._tool_manager._tools:
68
- tool: Tool = await self._server.get_tool(key)
69
- tool.disable()
70
- return tool
71
-
72
- # 2. Check mounted servers using the filtered protocol path.
73
- for mounted in reversed(self._server._mounted_servers):
74
- if mounted.prefix:
75
- if key.startswith(f"{mounted.prefix}_"):
76
- tool_key = key.removeprefix(f"{mounted.prefix}_")
77
- mounted_service = ComponentService(mounted.server)
78
- tool = await mounted_service._disable_tool(tool_key)
79
- return tool
80
- else:
81
- continue
82
- raise NotFoundError(f"Unknown tool: {key}")
83
-
84
- async def _enable_resource(self, key: str) -> Resource | ResourceTemplate:
85
- """Handle 'enableResource' requests.
86
-
87
- Args:
88
- key: The key of the resource to enable
89
-
90
- Returns:
91
- The resource that was enabled
92
- """
93
- logger.debug("Enabling resource: %s", key)
94
-
95
- # 1. Check local resources first. The server will have already applied its filter.
96
- if key in self._resource_manager._resources:
97
- resource: Resource = await self._server.get_resource(key)
98
- resource.enable()
99
- return resource
100
- if key in self._resource_manager._templates:
101
- template: ResourceTemplate = await self._server.get_resource_template(key)
102
- template.enable()
103
- return template
104
-
105
- # 2. Check mounted servers using the filtered protocol path.
106
- for mounted in reversed(self._server._mounted_servers):
107
- if mounted.prefix:
108
- if has_resource_prefix(key, mounted.prefix):
109
- key = remove_resource_prefix(key, mounted.prefix)
110
- mounted_service = ComponentService(mounted.server)
111
- mounted_resource: (
112
- Resource | ResourceTemplate
113
- ) = await mounted_service._enable_resource(key)
114
- return mounted_resource
115
- else:
116
- continue
117
- raise NotFoundError(f"Unknown resource: {key}")
118
-
119
- async def _disable_resource(self, key: str) -> Resource | ResourceTemplate:
120
- """Handle 'disableResource' requests.
121
-
122
- Args:
123
- key: The key of the resource to disable
124
-
125
- Returns:
126
- The resource that was disabled
127
- """
128
- logger.debug("Disable resource: %s", key)
129
-
130
- # 1. Check local resources first. The server will have already applied its filter.
131
- if key in self._resource_manager._resources:
132
- resource: Resource = await self._server.get_resource(key)
133
- resource.disable()
134
- return resource
135
- if key in self._resource_manager._templates:
136
- template: ResourceTemplate = await self._server.get_resource_template(key)
137
- template.disable()
138
- return template
139
-
140
- # 2. Check mounted servers using the filtered protocol path.
141
- for mounted in reversed(self._server._mounted_servers):
142
- if mounted.prefix:
143
- if has_resource_prefix(key, mounted.prefix):
144
- key = remove_resource_prefix(key, mounted.prefix)
145
- mounted_service = ComponentService(mounted.server)
146
- mounted_resource: (
147
- Resource | ResourceTemplate
148
- ) = await mounted_service._disable_resource(key)
149
- return mounted_resource
150
- else:
151
- continue
152
- raise NotFoundError(f"Unknown resource: {key}")
153
-
154
- async def _enable_prompt(self, key: str) -> Prompt:
155
- """Handle 'enablePrompt' requests.
156
-
157
- Args:
158
- key: The key of the prompt to enable
159
-
160
- Returns:
161
- The prompt that was enabled
162
- """
163
- logger.debug("Enabling prompt: %s", key)
164
-
165
- # 1. Check local prompts first. The server will have already applied its filter.
166
- if key in self._server._prompt_manager._prompts:
167
- prompt: Prompt = await self._server.get_prompt(key)
168
- prompt.enable()
169
- return prompt
170
-
171
- # 2. Check mounted servers using the filtered protocol path.
172
- for mounted in reversed(self._server._mounted_servers):
173
- if mounted.prefix:
174
- if key.startswith(f"{mounted.prefix}_"):
175
- prompt_key = key.removeprefix(f"{mounted.prefix}_")
176
- mounted_service = ComponentService(mounted.server)
177
- prompt = await mounted_service._enable_prompt(prompt_key)
178
- return prompt
179
- else:
180
- continue
181
- raise NotFoundError(f"Unknown prompt: {key}")
182
-
183
- async def _disable_prompt(self, key: str) -> Prompt:
184
- """Handle 'disablePrompt' requests.
185
-
186
- Args:
187
- key: The key of the prompt to disable
188
-
189
- Returns:
190
- The prompt that was disabled
191
- """
192
-
193
- # 1. Check local prompts first. The server will have already applied its filter.
194
- if key in self._server._prompt_manager._prompts:
195
- prompt: Prompt = await self._server.get_prompt(key)
196
- prompt.disable()
197
- return prompt
198
-
199
- # 2. Check mounted servers using the filtered protocol path.
200
- for mounted in reversed(self._server._mounted_servers):
201
- if mounted.prefix:
202
- if key.startswith(f"{mounted.prefix}_"):
203
- prompt_key = key.removeprefix(f"{mounted.prefix}_")
204
- mounted_service = ComponentService(mounted.server)
205
- prompt = await mounted_service._disable_prompt(prompt_key)
206
- return prompt
207
- else:
208
- continue
209
- raise NotFoundError(f"Unknown prompt: {key}")
@@ -1,117 +0,0 @@
1
- from __future__ import annotations as _annotations
2
-
3
- import warnings
4
- from collections.abc import Awaitable, Callable
5
- from typing import Any
6
-
7
- from mcp import GetPromptResult
8
-
9
- from fastmcp import settings
10
- from fastmcp.exceptions import FastMCPError, NotFoundError, PromptError
11
- from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
- from fastmcp.settings import DuplicateBehavior
13
- from fastmcp.utilities.logging import get_logger
14
-
15
- logger = get_logger(__name__)
16
-
17
-
18
- class PromptManager:
19
- """Manages FastMCP prompts."""
20
-
21
- def __init__(
22
- self,
23
- duplicate_behavior: DuplicateBehavior | None = None,
24
- mask_error_details: bool | None = None,
25
- ):
26
- self._prompts: dict[str, Prompt] = {}
27
- self.mask_error_details = mask_error_details or settings.mask_error_details
28
-
29
- # Default to "warn" if None is provided
30
- if duplicate_behavior is None:
31
- duplicate_behavior = "warn"
32
-
33
- if duplicate_behavior not in DuplicateBehavior.__args__:
34
- raise ValueError(
35
- f"Invalid duplicate_behavior: {duplicate_behavior}. "
36
- f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
37
- )
38
-
39
- self.duplicate_behavior = duplicate_behavior
40
-
41
- async def has_prompt(self, key: str) -> bool:
42
- """Check if a prompt exists."""
43
- prompts = await self.get_prompts()
44
- return key in prompts
45
-
46
- async def get_prompt(self, key: str) -> Prompt:
47
- """Get prompt by key."""
48
- prompts = await self.get_prompts()
49
- if key in prompts:
50
- return prompts[key]
51
- raise NotFoundError(f"Unknown prompt: {key}")
52
-
53
- async def get_prompts(self) -> dict[str, Prompt]:
54
- """
55
- Gets the complete, unfiltered inventory of local prompts.
56
- """
57
- return dict(self._prompts)
58
-
59
- def add_prompt_from_fn(
60
- self,
61
- fn: Callable[..., PromptResult | Awaitable[PromptResult]],
62
- name: str | None = None,
63
- description: str | None = None,
64
- tags: set[str] | None = None,
65
- ) -> FunctionPrompt:
66
- """Create a prompt from a function."""
67
- # deprecated in 2.7.0
68
- if settings.deprecation_warnings:
69
- warnings.warn(
70
- "PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
71
- DeprecationWarning,
72
- stacklevel=2,
73
- )
74
- prompt = FunctionPrompt.from_function(
75
- fn, name=name, description=description, tags=tags
76
- )
77
- return self.add_prompt(prompt) # type: ignore
78
-
79
- def add_prompt(self, prompt: Prompt) -> Prompt:
80
- """Add a prompt to the manager."""
81
- # Check for duplicates
82
- existing = self._prompts.get(prompt.key)
83
- if existing:
84
- if self.duplicate_behavior == "warn":
85
- logger.warning(f"Prompt already exists: {prompt.key}")
86
- self._prompts[prompt.key] = prompt
87
- elif self.duplicate_behavior == "replace":
88
- self._prompts[prompt.key] = prompt
89
- elif self.duplicate_behavior == "error":
90
- raise ValueError(f"Prompt already exists: {prompt.key}")
91
- elif self.duplicate_behavior == "ignore":
92
- return existing
93
- else:
94
- self._prompts[prompt.key] = prompt
95
- return prompt
96
-
97
- async def render_prompt(
98
- self,
99
- name: str,
100
- arguments: dict[str, Any] | None = None,
101
- ) -> GetPromptResult:
102
- """
103
- Internal API for servers: Finds and renders a prompt, respecting the
104
- filtered protocol path.
105
- """
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 FastMCPError:
111
- raise
112
- except Exception as e:
113
- logger.exception(f"Error rendering prompt {name!r}")
114
- if self.mask_error_details:
115
- raise PromptError(f"Error rendering prompt {name!r}") from e
116
- else:
117
- raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
@@ -1,338 +0,0 @@
1
- """Resource manager functionality."""
2
-
3
- from __future__ import annotations
4
-
5
- import inspect
6
- import warnings
7
- from collections.abc import Callable
8
- from typing import Any
9
-
10
- from pydantic import AnyUrl
11
-
12
- from fastmcp import settings
13
- from fastmcp.exceptions import FastMCPError, NotFoundError, ResourceError
14
- from fastmcp.resources.resource import Resource
15
- from fastmcp.resources.template import (
16
- ResourceTemplate,
17
- match_uri_template,
18
- )
19
- from fastmcp.settings import DuplicateBehavior
20
- from fastmcp.utilities.logging import get_logger
21
-
22
- logger = get_logger(__name__)
23
-
24
-
25
- class ResourceManager:
26
- """Manages FastMCP resources."""
27
-
28
- def __init__(
29
- self,
30
- duplicate_behavior: DuplicateBehavior | None = None,
31
- mask_error_details: bool | None = None,
32
- ):
33
- """Initialize the ResourceManager.
34
-
35
- Args:
36
- duplicate_behavior: How to handle duplicate resources
37
- (warn, error, replace, ignore)
38
- mask_error_details: Whether to mask error details from exceptions
39
- other than ResourceError
40
- """
41
- self._resources: dict[str, Resource] = {}
42
- self._templates: dict[str, ResourceTemplate] = {}
43
- self.mask_error_details = mask_error_details or settings.mask_error_details
44
-
45
- # Default to "warn" if None is provided
46
- if duplicate_behavior is None:
47
- duplicate_behavior = "warn"
48
-
49
- if duplicate_behavior not in DuplicateBehavior.__args__:
50
- raise ValueError(
51
- f"Invalid duplicate_behavior: {duplicate_behavior}. "
52
- f"Must be one of: {', '.join(DuplicateBehavior.__args__)}"
53
- )
54
- self.duplicate_behavior = duplicate_behavior
55
-
56
- async def get_resources(self) -> dict[str, Resource]:
57
- """Get all registered resources, keyed by URI."""
58
- return dict(self._resources)
59
-
60
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
61
- """Get all registered templates, keyed by URI template."""
62
- return dict(self._templates)
63
-
64
- def add_resource_or_template_from_fn(
65
- self,
66
- fn: Callable[..., Any],
67
- uri: str,
68
- name: str | None = None,
69
- description: str | None = None,
70
- mime_type: str | None = None,
71
- tags: set[str] | None = None,
72
- ) -> Resource | ResourceTemplate:
73
- """Add a resource or template to the manager from a function.
74
-
75
- Args:
76
- fn: The function to register as a resource or template
77
- uri: The URI for the resource or template
78
- name: Optional name for the resource or template
79
- description: Optional description of the resource or template
80
- mime_type: Optional MIME type for the resource or template
81
- tags: Optional set of tags for categorizing the resource or template
82
-
83
- Returns:
84
- The added resource or template. If a resource or template with the same URI already exists,
85
- returns the existing resource or template.
86
- """
87
- from fastmcp.server.context import Context
88
-
89
- # Check if this should be a template
90
- has_uri_params = "{" in uri and "}" in uri
91
- # check if the function has any parameters (other than injected context)
92
- has_func_params = any(
93
- p
94
- for p in inspect.signature(fn).parameters.values()
95
- if p.annotation is not Context
96
- )
97
-
98
- if has_uri_params or has_func_params:
99
- return self.add_template_from_fn(
100
- fn, uri, name, description, mime_type, tags
101
- )
102
- elif not has_uri_params and not has_func_params:
103
- return self.add_resource_from_fn(
104
- fn, uri, name, description, mime_type, tags
105
- )
106
- else:
107
- raise ValueError(
108
- "Invalid resource or template definition due to a "
109
- "mismatch between URI parameters and function parameters."
110
- )
111
-
112
- def add_resource_from_fn(
113
- self,
114
- fn: Callable[..., Any],
115
- uri: str,
116
- name: str | None = None,
117
- description: str | None = None,
118
- mime_type: str | None = None,
119
- tags: set[str] | None = None,
120
- ) -> Resource:
121
- """Add a resource to the manager from a function.
122
-
123
- Args:
124
- fn: The function to register as a resource
125
- uri: The URI for the resource
126
- name: Optional name for the resource
127
- description: Optional description of the resource
128
- mime_type: Optional MIME type for the resource
129
- tags: Optional set of tags for categorizing the resource
130
-
131
- Returns:
132
- The added resource. If a resource with the same URI already exists,
133
- returns the existing resource.
134
- """
135
- # deprecated in 2.7.0
136
- if settings.deprecation_warnings:
137
- warnings.warn(
138
- "add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
139
- DeprecationWarning,
140
- stacklevel=2,
141
- )
142
- resource = Resource.from_function(
143
- fn=fn,
144
- uri=uri,
145
- name=name,
146
- description=description,
147
- mime_type=mime_type,
148
- tags=tags,
149
- )
150
- return self.add_resource(resource)
151
-
152
- def add_resource(self, resource: Resource) -> Resource:
153
- """Add a resource to the manager.
154
-
155
- Args:
156
- resource: A Resource instance to add. The resource's .key attribute
157
- will be used as the storage key. To overwrite it, call
158
- Resource.model_copy(key=new_key) before calling this method.
159
- """
160
- existing = self._resources.get(resource.key)
161
- if existing:
162
- if self.duplicate_behavior == "warn":
163
- logger.warning(f"Resource already exists: {resource.key}")
164
- self._resources[resource.key] = resource
165
- elif self.duplicate_behavior == "replace":
166
- self._resources[resource.key] = resource
167
- elif self.duplicate_behavior == "error":
168
- raise ValueError(f"Resource already exists: {resource.key}")
169
- elif self.duplicate_behavior == "ignore":
170
- return existing
171
- self._resources[resource.key] = resource
172
- return resource
173
-
174
- def add_template_from_fn(
175
- self,
176
- fn: Callable[..., Any],
177
- uri_template: str,
178
- name: str | None = None,
179
- description: str | None = None,
180
- mime_type: str | None = None,
181
- tags: set[str] | None = None,
182
- ) -> ResourceTemplate:
183
- """Create a template from a function."""
184
- # deprecated in 2.7.0
185
- if settings.deprecation_warnings:
186
- warnings.warn(
187
- "add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
188
- DeprecationWarning,
189
- stacklevel=2,
190
- )
191
- template = ResourceTemplate.from_function(
192
- fn,
193
- uri_template=uri_template,
194
- name=name,
195
- description=description,
196
- mime_type=mime_type,
197
- tags=tags,
198
- )
199
- return self.add_template(template)
200
-
201
- def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
202
- """Add a template to the manager.
203
-
204
- Args:
205
- template: A ResourceTemplate instance to add. The template's .key attribute
206
- will be used as the storage key. To overwrite it, call
207
- ResourceTemplate.model_copy(key=new_key) before calling this method.
208
-
209
- Returns:
210
- The added template. If a template with the same URI already exists,
211
- returns the existing template.
212
- """
213
- existing = self._templates.get(template.key)
214
- if existing:
215
- if self.duplicate_behavior == "warn":
216
- logger.warning(f"Template already exists: {template.key}")
217
- self._templates[template.key] = template
218
- elif self.duplicate_behavior == "replace":
219
- self._templates[template.key] = template
220
- elif self.duplicate_behavior == "error":
221
- raise ValueError(f"Template already exists: {template.key}")
222
- elif self.duplicate_behavior == "ignore":
223
- return existing
224
- self._templates[template.key] = template
225
- return template
226
-
227
- async def has_resource(self, uri: AnyUrl | str) -> bool:
228
- """Check if a resource exists."""
229
- uri_str = str(uri)
230
-
231
- # First check concrete resources (local and mounted)
232
- resources = await self.get_resources()
233
- if uri_str in resources:
234
- return True
235
-
236
- # Then check templates (local and mounted) only if not found in concrete resources
237
- templates = await self.get_resource_templates()
238
- for template_key in templates:
239
- if match_uri_template(uri_str, template_key) is not None:
240
- return True
241
-
242
- return False
243
-
244
- async def get_resource(self, uri: AnyUrl | str) -> Resource:
245
- """Get resource by URI, checking concrete resources first, then templates.
246
-
247
- Args:
248
- uri: The URI of the resource to get
249
-
250
- Raises:
251
- NotFoundError: If no resource or template matching the URI is found.
252
- """
253
- uri_str = str(uri)
254
- logger.debug("Getting resource", extra={"uri": uri_str})
255
-
256
- # First check concrete resources
257
- resources = await self.get_resources()
258
- if resource := resources.get(uri_str):
259
- return resource
260
-
261
- # Then check templates
262
- templates = await self.get_resource_templates()
263
- for storage_key, template in templates.items():
264
- # Try to match against the storage key (which might be a custom key)
265
- if (params := match_uri_template(uri_str, storage_key)) is not None:
266
- try:
267
- return await template.create_resource(
268
- uri_str,
269
- params=params,
270
- )
271
- # Pass through FastMCPErrors as-is
272
- except FastMCPError:
273
- raise
274
- # Handle other exceptions
275
- except Exception as e:
276
- logger.error(f"Error creating resource from template: {e}")
277
- if self.mask_error_details:
278
- # Mask internal details
279
- raise ValueError("Error creating resource from template") from e
280
- else:
281
- # Include original error details
282
- raise ValueError(
283
- f"Error creating resource from template: {e}"
284
- ) from e
285
-
286
- raise NotFoundError(f"Unknown resource: {uri_str}")
287
-
288
- async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
289
- """
290
- Internal API for servers: Finds and reads a resource, respecting the
291
- filtered protocol path.
292
- """
293
- uri_str = str(uri)
294
-
295
- # 1. Check local resources first. The server will have already applied its filter.
296
- if uri_str in self._resources:
297
- resource = await self.get_resource(uri_str)
298
- try:
299
- return await resource.read()
300
-
301
- # raise FastMCPErrors as-is
302
- except FastMCPError:
303
- raise
304
-
305
- # Handle other exceptions
306
- except Exception as e:
307
- logger.exception(f"Error reading resource {uri_str!r}")
308
- if self.mask_error_details:
309
- # Mask internal details
310
- raise ResourceError(f"Error reading resource {uri_str!r}") from e
311
- else:
312
- # Include original error details
313
- raise ResourceError(
314
- f"Error reading resource {uri_str!r}: {e}"
315
- ) from e
316
-
317
- # 1b. Check local templates if not found in concrete resources
318
- for key, template in self._templates.items():
319
- if (params := match_uri_template(uri_str, key)) is not None:
320
- try:
321
- resource = await template.create_resource(uri_str, params=params)
322
- return await resource.read()
323
- except FastMCPError:
324
- raise
325
- except Exception as e:
326
- logger.exception(
327
- f"Error reading resource from template {uri_str!r}"
328
- )
329
- if self.mask_error_details:
330
- raise ResourceError(
331
- f"Error reading resource from template {uri_str!r}"
332
- ) from e
333
- else:
334
- raise ResourceError(
335
- f"Error reading resource from template {uri_str!r}: {e}"
336
- ) from e
337
-
338
- raise NotFoundError(f"Resource {uri_str!r} not found.")