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
@@ -0,0 +1,312 @@
1
+ """Authorization middleware for FastMCP.
2
+
3
+ This module provides middleware-based authorization using callable auth checks.
4
+ AuthMiddleware applies auth checks globally to all components on the server.
5
+
6
+ Example:
7
+ ```python
8
+ from fastmcp import FastMCP
9
+ from fastmcp.server.auth import require_auth, require_scopes, restrict_tag
10
+ from fastmcp.server.middleware import AuthMiddleware
11
+
12
+ # Require auth for all components
13
+ mcp = FastMCP(middleware=[
14
+ AuthMiddleware(auth=require_auth)
15
+ ])
16
+
17
+ # Tag-based: components tagged "admin" require "admin" scope
18
+ mcp = FastMCP(middleware=[
19
+ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
20
+ ])
21
+ ```
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from collections.abc import Sequence
28
+
29
+ import mcp.types as mt
30
+
31
+ from fastmcp.exceptions import AuthorizationError
32
+ from fastmcp.prompts.prompt import Prompt, PromptResult
33
+ from fastmcp.resources.resource import Resource, ResourceResult
34
+ from fastmcp.resources.template import ResourceTemplate
35
+ from fastmcp.server.auth.authorization import (
36
+ AuthCheck,
37
+ AuthContext,
38
+ run_auth_checks,
39
+ )
40
+ from fastmcp.server.dependencies import get_access_token
41
+ from fastmcp.server.middleware.middleware import (
42
+ CallNext,
43
+ Middleware,
44
+ MiddlewareContext,
45
+ )
46
+ from fastmcp.tools.tool import Tool, ToolResult
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class AuthMiddleware(Middleware):
52
+ """Global authorization middleware using callable checks.
53
+
54
+ This middleware applies auth checks to all components (tools, resources,
55
+ prompts) on the server. It uses the same callable API as component-level
56
+ auth checks.
57
+
58
+ The middleware:
59
+ - Filters tools/resources/prompts from list responses based on auth checks
60
+ - Checks auth before tool execution, resource read, and prompt render
61
+ - Skips all auth checks for STDIO transport (no OAuth concept)
62
+
63
+ Args:
64
+ auth: A single auth check function or list of check functions.
65
+ All checks must pass for authorization to succeed (AND logic).
66
+
67
+ Example:
68
+ ```python
69
+ from fastmcp import FastMCP
70
+ from fastmcp.server.auth import require_auth, require_scopes
71
+
72
+ # Require any authentication for all components
73
+ mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)])
74
+
75
+ # Require specific scope for all components
76
+ mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("api"))])
77
+
78
+ # Combined checks (AND logic)
79
+ mcp = FastMCP(middleware=[
80
+ AuthMiddleware(auth=[require_auth, require_scopes("api")])
81
+ ])
82
+ ```
83
+ """
84
+
85
+ def __init__(self, auth: AuthCheck | list[AuthCheck]) -> None:
86
+ self.auth = auth
87
+
88
+ async def on_list_tools(
89
+ self,
90
+ context: MiddlewareContext[mt.ListToolsRequest],
91
+ call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
92
+ ) -> Sequence[Tool]:
93
+ """Filter tools/list response based on auth checks."""
94
+ tools = await call_next(context)
95
+
96
+ # STDIO has no auth concept, skip filtering
97
+ # Late import to avoid circular import with context.py
98
+ from fastmcp.server.context import _current_transport
99
+
100
+ if _current_transport.get() == "stdio":
101
+ return tools
102
+
103
+ token = get_access_token()
104
+
105
+ authorized_tools: list[Tool] = []
106
+ for tool in tools:
107
+ ctx = AuthContext(token=token, component=tool)
108
+ if run_auth_checks(self.auth, ctx):
109
+ authorized_tools.append(tool)
110
+
111
+ return authorized_tools
112
+
113
+ async def on_call_tool(
114
+ self,
115
+ context: MiddlewareContext[mt.CallToolRequestParams],
116
+ call_next: CallNext[mt.CallToolRequestParams, ToolResult],
117
+ ) -> ToolResult:
118
+ """Check auth before tool execution."""
119
+ # STDIO has no auth concept, skip enforcement
120
+ # Late import to avoid circular import with context.py
121
+ from fastmcp.server.context import _current_transport
122
+
123
+ if _current_transport.get() == "stdio":
124
+ return await call_next(context)
125
+
126
+ # Get the tool being called
127
+ tool_name = context.message.name
128
+ fastmcp = context.fastmcp_context
129
+ if fastmcp is None:
130
+ # Fail closed: deny access when context is missing
131
+ logger.warning(
132
+ f"AuthMiddleware: fastmcp_context is None for tool '{tool_name}'. "
133
+ "Denying access for security."
134
+ )
135
+ raise AuthorizationError(
136
+ f"Authorization failed for tool '{tool_name}': missing context"
137
+ )
138
+
139
+ # Get tool (component auth is checked in get_tool, raises if unauthorized)
140
+ tool = await fastmcp.fastmcp.get_tool(tool_name)
141
+ if tool is None:
142
+ raise AuthorizationError(
143
+ f"Authorization failed for tool '{tool_name}': tool not found"
144
+ )
145
+
146
+ # Global auth check
147
+ token = get_access_token()
148
+ ctx = AuthContext(token=token, component=tool)
149
+ if not run_auth_checks(self.auth, ctx):
150
+ raise AuthorizationError(
151
+ f"Authorization failed for tool '{tool_name}': insufficient permissions"
152
+ )
153
+
154
+ return await call_next(context)
155
+
156
+ async def on_list_resources(
157
+ self,
158
+ context: MiddlewareContext[mt.ListResourcesRequest],
159
+ call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]],
160
+ ) -> Sequence[Resource]:
161
+ """Filter resources/list response based on auth checks."""
162
+ resources = await call_next(context)
163
+
164
+ # STDIO has no auth concept, skip filtering
165
+ from fastmcp.server.context import _current_transport
166
+
167
+ if _current_transport.get() == "stdio":
168
+ return resources
169
+
170
+ token = get_access_token()
171
+
172
+ authorized_resources: list[Resource] = []
173
+ for resource in resources:
174
+ ctx = AuthContext(token=token, component=resource)
175
+ if run_auth_checks(self.auth, ctx):
176
+ authorized_resources.append(resource)
177
+
178
+ return authorized_resources
179
+
180
+ async def on_read_resource(
181
+ self,
182
+ context: MiddlewareContext[mt.ReadResourceRequestParams],
183
+ call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult],
184
+ ) -> ResourceResult:
185
+ """Check auth before resource read."""
186
+ # STDIO has no auth concept, skip enforcement
187
+ from fastmcp.server.context import _current_transport
188
+
189
+ if _current_transport.get() == "stdio":
190
+ return await call_next(context)
191
+
192
+ # Get the resource being read
193
+ uri = context.message.uri
194
+ fastmcp = context.fastmcp_context
195
+ if fastmcp is None:
196
+ logger.warning(
197
+ f"AuthMiddleware: fastmcp_context is None for resource '{uri}'. "
198
+ "Denying access for security."
199
+ )
200
+ raise AuthorizationError(
201
+ f"Authorization failed for resource '{uri}': missing context"
202
+ )
203
+
204
+ # Get resource/template (component auth is checked in get_*, raises if unauthorized)
205
+ component = await fastmcp.fastmcp.get_resource(str(uri))
206
+ if component is None:
207
+ component = await fastmcp.fastmcp.get_resource_template(str(uri))
208
+ if component is None:
209
+ raise AuthorizationError(
210
+ f"Authorization failed for resource '{uri}': resource not found"
211
+ )
212
+
213
+ # Global auth check
214
+ token = get_access_token()
215
+ ctx = AuthContext(token=token, component=component)
216
+ if not run_auth_checks(self.auth, ctx):
217
+ raise AuthorizationError(
218
+ f"Authorization failed for resource '{uri}': insufficient permissions"
219
+ )
220
+
221
+ return await call_next(context)
222
+
223
+ async def on_list_resource_templates(
224
+ self,
225
+ context: MiddlewareContext[mt.ListResourceTemplatesRequest],
226
+ call_next: CallNext[
227
+ mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
228
+ ],
229
+ ) -> Sequence[ResourceTemplate]:
230
+ """Filter resource templates/list response based on auth checks."""
231
+ templates = await call_next(context)
232
+
233
+ # STDIO has no auth concept, skip filtering
234
+ from fastmcp.server.context import _current_transport
235
+
236
+ if _current_transport.get() == "stdio":
237
+ return templates
238
+
239
+ token = get_access_token()
240
+
241
+ authorized_templates: list[ResourceTemplate] = []
242
+ for template in templates:
243
+ ctx = AuthContext(token=token, component=template)
244
+ if run_auth_checks(self.auth, ctx):
245
+ authorized_templates.append(template)
246
+
247
+ return authorized_templates
248
+
249
+ async def on_list_prompts(
250
+ self,
251
+ context: MiddlewareContext[mt.ListPromptsRequest],
252
+ call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]],
253
+ ) -> Sequence[Prompt]:
254
+ """Filter prompts/list response based on auth checks."""
255
+ prompts = await call_next(context)
256
+
257
+ # STDIO has no auth concept, skip filtering
258
+ from fastmcp.server.context import _current_transport
259
+
260
+ if _current_transport.get() == "stdio":
261
+ return prompts
262
+
263
+ token = get_access_token()
264
+
265
+ authorized_prompts: list[Prompt] = []
266
+ for prompt in prompts:
267
+ ctx = AuthContext(token=token, component=prompt)
268
+ if run_auth_checks(self.auth, ctx):
269
+ authorized_prompts.append(prompt)
270
+
271
+ return authorized_prompts
272
+
273
+ async def on_get_prompt(
274
+ self,
275
+ context: MiddlewareContext[mt.GetPromptRequestParams],
276
+ call_next: CallNext[mt.GetPromptRequestParams, PromptResult],
277
+ ) -> PromptResult:
278
+ """Check auth before prompt render."""
279
+ # STDIO has no auth concept, skip enforcement
280
+ from fastmcp.server.context import _current_transport
281
+
282
+ if _current_transport.get() == "stdio":
283
+ return await call_next(context)
284
+
285
+ # Get the prompt being rendered
286
+ prompt_name = context.message.name
287
+ fastmcp = context.fastmcp_context
288
+ if fastmcp is None:
289
+ logger.warning(
290
+ f"AuthMiddleware: fastmcp_context is None for prompt '{prompt_name}'. "
291
+ "Denying access for security."
292
+ )
293
+ raise AuthorizationError(
294
+ f"Authorization failed for prompt '{prompt_name}': missing context"
295
+ )
296
+
297
+ # Get prompt (component auth is checked in get_prompt, raises if unauthorized)
298
+ prompt = await fastmcp.fastmcp.get_prompt(prompt_name)
299
+ if prompt is None:
300
+ raise AuthorizationError(
301
+ f"Authorization failed for prompt '{prompt_name}': prompt not found"
302
+ )
303
+
304
+ # Global auth check
305
+ token = get_access_token()
306
+ ctx = AuthContext(token=token, component=prompt)
307
+ if not run_auth_checks(self.auth, ctx):
308
+ raise AuthorizationError(
309
+ f"Authorization failed for prompt '{prompt_name}': insufficient permissions"
310
+ )
311
+
312
+ return await call_next(context)
@@ -14,15 +14,15 @@ from key_value.aio.wrappers.statistics import StatisticsWrapper
14
14
  from key_value.aio.wrappers.statistics.wrapper import (
15
15
  KVStoreCollectionStatistics,
16
16
  )
17
- from mcp.server.lowlevel.helper_types import ReadResourceContents
18
- from pydantic import BaseModel, Field
17
+ from pydantic import Field
19
18
  from typing_extensions import NotRequired, Self, override
20
19
 
21
- from fastmcp.prompts.prompt import Prompt
22
- from fastmcp.resources.resource import Resource
20
+ from fastmcp.prompts.prompt import Message, Prompt, PromptResult
21
+ from fastmcp.resources.resource import Resource, ResourceContent, ResourceResult
23
22
  from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
24
23
  from fastmcp.tools.tool import Tool, ToolResult
25
24
  from fastmcp.utilities.logging import get_logger
25
+ from fastmcp.utilities.types import FastMCPBaseModel
26
26
 
27
27
  logger: Logger = get_logger(name=__name__)
28
28
 
@@ -35,32 +35,48 @@ ONE_MB_IN_BYTES = 1024 * 1024
35
35
  GLOBAL_KEY = "__global__"
36
36
 
37
37
 
38
- class CachableReadResourceContents(BaseModel):
39
- """A wrapper for ReadResourceContents that can be cached."""
38
+ class CachableResourceContent(FastMCPBaseModel):
39
+ """A wrapper for ResourceContent that can be cached."""
40
40
 
41
41
  content: str | bytes
42
42
  mime_type: str | None = None
43
+ meta: dict[str, Any] | None = None
44
+
45
+
46
+ class CachableResourceResult(FastMCPBaseModel):
47
+ """A wrapper for ResourceResult that can be cached."""
48
+
49
+ contents: list[CachableResourceContent]
50
+ meta: dict[str, Any] | None = None
43
51
 
44
52
  def get_size(self) -> int:
45
53
  return len(self.model_dump_json())
46
54
 
47
55
  @classmethod
48
- def get_sizes(cls, values: Sequence[Self]) -> int:
49
- return sum(item.get_size() for item in values)
50
-
51
- @classmethod
52
- def wrap(cls, values: Sequence[ReadResourceContents]) -> list[Self]:
53
- return [cls(content=item.content, mime_type=item.mime_type) for item in values]
56
+ def wrap(cls, value: ResourceResult) -> Self:
57
+ return cls(
58
+ contents=[
59
+ CachableResourceContent(
60
+ content=item.content, mime_type=item.mime_type, meta=item.meta
61
+ )
62
+ for item in value.contents
63
+ ],
64
+ meta=value.meta,
65
+ )
54
66
 
55
- @classmethod
56
- def unwrap(cls, values: Sequence[Self]) -> list[ReadResourceContents]:
57
- return [
58
- ReadResourceContents(content=item.content, mime_type=item.mime_type)
59
- for item in values
60
- ]
67
+ def unwrap(self) -> ResourceResult:
68
+ return ResourceResult(
69
+ contents=[
70
+ ResourceContent(
71
+ content=item.content, mime_type=item.mime_type, meta=item.meta
72
+ )
73
+ for item in self.contents
74
+ ],
75
+ meta=self.meta,
76
+ )
61
77
 
62
78
 
63
- class CachableToolResult(BaseModel):
79
+ class CachableToolResult(FastMCPBaseModel):
64
80
  content: list[mcp.types.ContentBlock]
65
81
  structured_content: dict[str, Any] | None
66
82
  meta: dict[str, Any] | None
@@ -81,6 +97,44 @@ class CachableToolResult(BaseModel):
81
97
  )
82
98
 
83
99
 
100
+ class CachableMessage(FastMCPBaseModel):
101
+ """A wrapper for Message that can be cached."""
102
+
103
+ role: str
104
+ content: mcp.types.TextContent | mcp.types.EmbeddedResource
105
+
106
+
107
+ class CachablePromptResult(FastMCPBaseModel):
108
+ """A wrapper for PromptResult that can be cached."""
109
+
110
+ messages: list[CachableMessage]
111
+ description: str | None = None
112
+ meta: dict[str, Any] | None = None
113
+
114
+ def get_size(self) -> int:
115
+ return len(self.model_dump_json())
116
+
117
+ @classmethod
118
+ def wrap(cls, value: PromptResult) -> Self:
119
+ return cls(
120
+ messages=[
121
+ CachableMessage(role=m.role, content=m.content) for m in value.messages
122
+ ],
123
+ description=value.description,
124
+ meta=value.meta,
125
+ )
126
+
127
+ def unwrap(self) -> PromptResult:
128
+ return PromptResult(
129
+ messages=[
130
+ Message(content=m.content, role=m.role) # type: ignore[arg-type]
131
+ for m in self.messages
132
+ ],
133
+ description=self.description,
134
+ meta=self.meta,
135
+ )
136
+
137
+
84
138
  class SharedMethodSettings(TypedDict):
85
139
  """Shared config for a cache method."""
86
140
 
@@ -115,7 +169,7 @@ class GetPromptSettings(SharedMethodSettings):
115
169
  """Configuration options for Prompt-related caching."""
116
170
 
117
171
 
118
- class ResponseCachingStatistics(BaseModel):
172
+ class ResponseCachingStatistics(FastMCPBaseModel):
119
173
  list_tools: KVStoreCollectionStatistics | None = Field(default=None)
120
174
  list_resources: KVStoreCollectionStatistics | None = Field(default=None)
121
175
  list_prompts: KVStoreCollectionStatistics | None = Field(default=None)
@@ -189,43 +243,43 @@ class ResponseCachingMiddleware(Middleware):
189
243
  call_tool_settings or CallToolSettings()
190
244
  )
191
245
 
246
+ # PydanticAdapter type signature will be fixed to accept generic aliases
247
+ # See: https://github.com/strawgate/py-key-value/pull/250
192
248
  self._list_tools_cache: PydanticAdapter[list[Tool]] = PydanticAdapter(
193
249
  key_value=self._stats,
194
- pydantic_model=list[Tool],
250
+ pydantic_model=list[Tool], # type: ignore[arg-type]
195
251
  default_collection="tools/list",
196
252
  )
197
253
 
198
254
  self._list_resources_cache: PydanticAdapter[list[Resource]] = PydanticAdapter(
199
255
  key_value=self._stats,
200
- pydantic_model=list[Resource],
256
+ pydantic_model=list[Resource], # type: ignore[arg-type]
201
257
  default_collection="resources/list",
202
258
  )
203
259
 
204
260
  self._list_prompts_cache: PydanticAdapter[list[Prompt]] = PydanticAdapter(
205
261
  key_value=self._stats,
206
- pydantic_model=list[Prompt],
262
+ pydantic_model=list[Prompt], # type: ignore[arg-type]
207
263
  default_collection="prompts/list",
208
264
  )
209
265
 
210
- self._read_resource_cache: PydanticAdapter[
211
- list[CachableReadResourceContents]
212
- ] = PydanticAdapter(
213
- key_value=self._stats,
214
- pydantic_model=list[CachableReadResourceContents],
215
- default_collection="resources/read",
216
- )
217
-
218
- self._get_prompt_cache: PydanticAdapter[mcp.types.GetPromptResult] = (
266
+ self._read_resource_cache: PydanticAdapter[CachableResourceResult] = (
219
267
  PydanticAdapter(
220
268
  key_value=self._stats,
221
- pydantic_model=mcp.types.GetPromptResult,
222
- default_collection="prompts/get",
269
+ pydantic_model=CachableResourceResult, # type: ignore[arg-type]
270
+ default_collection="resources/read",
223
271
  )
224
272
  )
225
273
 
274
+ self._get_prompt_cache: PydanticAdapter[CachablePromptResult] = PydanticAdapter(
275
+ key_value=self._stats,
276
+ pydantic_model=CachablePromptResult, # type: ignore[arg-type]
277
+ default_collection="prompts/get",
278
+ )
279
+
226
280
  self._call_tool_cache: PydanticAdapter[CachableToolResult] = PydanticAdapter(
227
281
  key_value=self._stats,
228
- pydantic_model=CachableToolResult,
282
+ pydantic_model=CachableToolResult, # type: ignore[arg-type]
229
283
  default_collection="tools/call",
230
284
  )
231
285
 
@@ -256,7 +310,6 @@ class ResponseCachingMiddleware(Middleware):
256
310
  annotations=tool.annotations,
257
311
  meta=tool.meta,
258
312
  tags=tool.tags,
259
- enabled=tool.enabled,
260
313
  )
261
314
  for tool in tools
262
315
  ]
@@ -295,7 +348,6 @@ class ResponseCachingMiddleware(Middleware):
295
348
  meta=resource.meta,
296
349
  mime_type=resource.mime_type,
297
350
  annotations=resource.annotations,
298
- enabled=resource.enabled,
299
351
  uri=resource.uri,
300
352
  )
301
353
  for resource in resources
@@ -333,7 +385,6 @@ class ResponseCachingMiddleware(Middleware):
333
385
  description=prompt.description,
334
386
  tags=prompt.tags,
335
387
  meta=prompt.meta,
336
- enabled=prompt.enabled,
337
388
  arguments=prompt.arguments,
338
389
  )
339
390
  for prompt in prompts
@@ -384,23 +435,21 @@ class ResponseCachingMiddleware(Middleware):
384
435
  async def on_read_resource(
385
436
  self,
386
437
  context: MiddlewareContext[mcp.types.ReadResourceRequestParams],
387
- call_next: CallNext[
388
- mcp.types.ReadResourceRequestParams, Sequence[ReadResourceContents]
389
- ],
390
- ) -> Sequence[ReadResourceContents]:
438
+ call_next: CallNext[mcp.types.ReadResourceRequestParams, ResourceResult],
439
+ ) -> ResourceResult:
391
440
  """Read a resource from the cache, if caching is enabled, and the result is in the cache. Otherwise,
392
441
  otherwise call the next middleware and store the result in the cache if caching is enabled."""
393
442
  if self._read_resource_settings.get("enabled") is False:
394
443
  return await call_next(context=context)
395
444
 
396
445
  cache_key: str = str(context.message.uri)
397
- cached_value: list[CachableReadResourceContents] | None
446
+ cached_value: CachableResourceResult | None
398
447
 
399
448
  if cached_value := await self._read_resource_cache.get(key=cache_key):
400
- return CachableReadResourceContents.unwrap(values=cached_value)
449
+ return cached_value.unwrap()
401
450
 
402
- value: Sequence[ReadResourceContents] = await call_next(context=context)
403
- cached_value = CachableReadResourceContents.wrap(values=value)
451
+ value: ResourceResult = await call_next(context=context)
452
+ cached_value = CachableResourceResult.wrap(value)
404
453
 
405
454
  await self._read_resource_cache.put(
406
455
  key=cache_key,
@@ -408,16 +457,14 @@ class ResponseCachingMiddleware(Middleware):
408
457
  ttl=self._read_resource_settings.get("ttl", ONE_HOUR_IN_SECONDS),
409
458
  )
410
459
 
411
- return CachableReadResourceContents.unwrap(values=cached_value)
460
+ return cached_value.unwrap()
412
461
 
413
462
  @override
414
463
  async def on_get_prompt(
415
464
  self,
416
465
  context: MiddlewareContext[mcp.types.GetPromptRequestParams],
417
- call_next: CallNext[
418
- mcp.types.GetPromptRequestParams, mcp.types.GetPromptResult
419
- ],
420
- ) -> mcp.types.GetPromptResult:
466
+ call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult],
467
+ ) -> PromptResult:
421
468
  """Get a prompt from the cache, if caching is enabled, and the result is in the cache. Otherwise,
422
469
  otherwise call the next middleware and store the result in the cache if caching is enabled."""
423
470
  if self._get_prompt_settings.get("enabled") is False:
@@ -426,13 +473,13 @@ class ResponseCachingMiddleware(Middleware):
426
473
  cache_key: str = f"{context.message.name}:{_get_arguments_str(arguments=context.message.arguments)}"
427
474
 
428
475
  if cached_value := await self._get_prompt_cache.get(key=cache_key):
429
- return cached_value
476
+ return cached_value.unwrap()
430
477
 
431
- value: mcp.types.GetPromptResult = await call_next(context=context)
478
+ value: PromptResult = await call_next(context=context)
432
479
 
433
480
  await self._get_prompt_cache.put(
434
481
  key=cache_key,
435
- value=value,
482
+ value=CachablePromptResult.wrap(value),
436
483
  ttl=self._get_prompt_settings.get("ttl", ONE_HOUR_IN_SECONDS),
437
484
  )
438
485
 
@@ -15,11 +15,10 @@ from typing import (
15
15
  )
16
16
 
17
17
  import mcp.types as mt
18
- from mcp.server.lowlevel.helper_types import ReadResourceContents
19
18
  from typing_extensions import TypeVar
20
19
 
21
- from fastmcp.prompts.prompt import Prompt
22
- from fastmcp.resources.resource import Resource
20
+ from fastmcp.prompts.prompt import Prompt, PromptResult
21
+ from fastmcp.resources.resource import Resource, ResourceResult
23
22
  from fastmcp.resources.template import ResourceTemplate
24
23
  from fastmcp.tools.tool import Tool, ToolResult
25
24
 
@@ -164,17 +163,15 @@ class Middleware:
164
163
  async def on_read_resource(
165
164
  self,
166
165
  context: MiddlewareContext[mt.ReadResourceRequestParams],
167
- call_next: CallNext[
168
- mt.ReadResourceRequestParams, Sequence[ReadResourceContents]
169
- ],
170
- ) -> Sequence[ReadResourceContents]:
166
+ call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult],
167
+ ) -> ResourceResult:
171
168
  return await call_next(context)
172
169
 
173
170
  async def on_get_prompt(
174
171
  self,
175
172
  context: MiddlewareContext[mt.GetPromptRequestParams],
176
- call_next: CallNext[mt.GetPromptRequestParams, mt.GetPromptResult],
177
- ) -> mt.GetPromptResult:
173
+ call_next: CallNext[mt.GetPromptRequestParams, PromptResult],
174
+ ) -> PromptResult:
178
175
  return await call_next(context)
179
176
 
180
177
  async def on_list_tools(