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
@@ -5,10 +5,15 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
- from typing import Annotated, Any
8
+ from typing import TYPE_CHECKING, Any, ClassVar, overload
9
9
  from urllib.parse import parse_qs, unquote
10
10
 
11
+ import mcp.types
11
12
  from mcp.types import Annotations, Icon
13
+
14
+ if TYPE_CHECKING:
15
+ from docket import Docket
16
+ from docket.execution import Execution
12
17
  from mcp.types import ResourceTemplate as SDKResourceTemplate
13
18
  from pydantic import (
14
19
  Field,
@@ -16,9 +21,13 @@ from pydantic import (
16
21
  validate_call,
17
22
  )
18
23
 
19
- from fastmcp.resources.resource import Resource
20
- from fastmcp.server.dependencies import get_context, without_injected_parameters
21
- from fastmcp.server.tasks.config import TaskConfig
24
+ from fastmcp.resources.resource import Resource, ResourceResult
25
+ from fastmcp.server.dependencies import (
26
+ transform_context_annotations,
27
+ without_injected_parameters,
28
+ )
29
+ from fastmcp.server.tasks.config import TaskConfig, TaskMeta
30
+ from fastmcp.tools.tool import AuthCheckCallable
22
31
  from fastmcp.utilities.components import FastMCPComponent
23
32
  from fastmcp.utilities.json_schema import compress_schema
24
33
  from fastmcp.utilities.types import get_cached_typeadapter
@@ -84,7 +93,7 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
84
93
  for name in query_param_names:
85
94
  if name in parsed_query:
86
95
  # Take first value if multiple provided
87
- params[name] = parsed_query[name][0] # type: ignore[index]
96
+ params[name] = parsed_query[name][0]
88
97
 
89
98
  return params
90
99
 
@@ -92,6 +101,8 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
92
101
  class ResourceTemplate(FastMCPComponent):
93
102
  """A template for dynamically creating resources."""
94
103
 
104
+ KEY_PREFIX: ClassVar[str] = "template"
105
+
95
106
  uri_template: str = Field(
96
107
  description="URI template with parameters (e.g. weather://{city}/current)"
97
108
  )
@@ -104,54 +115,45 @@ class ResourceTemplate(FastMCPComponent):
104
115
  annotations: Annotations | None = Field(
105
116
  default=None, description="Optional annotations about the resource's behavior"
106
117
  )
118
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = Field(
119
+ default=None,
120
+ description="Authorization checks for this resource template",
121
+ exclude=True,
122
+ )
107
123
 
108
124
  def __repr__(self) -> str:
109
125
  return f"{self.__class__.__name__}(uri_template={self.uri_template!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
110
126
 
111
- def enable(self) -> None:
112
- super().enable()
113
- try:
114
- context = get_context()
115
- context._queue_resource_list_changed() # type: ignore[private-use]
116
- except RuntimeError:
117
- pass # No context available
118
-
119
- def disable(self) -> None:
120
- super().disable()
121
- try:
122
- context = get_context()
123
- context._queue_resource_list_changed() # type: ignore[private-use]
124
- except RuntimeError:
125
- pass # No context available
126
-
127
127
  @staticmethod
128
128
  def from_function(
129
129
  fn: Callable[..., Any],
130
130
  uri_template: str,
131
131
  name: str | None = None,
132
+ version: str | int | None = None,
132
133
  title: str | None = None,
133
134
  description: str | None = None,
134
135
  icons: list[Icon] | None = None,
135
136
  mime_type: str | None = None,
136
137
  tags: set[str] | None = None,
137
- enabled: bool | None = None,
138
138
  annotations: Annotations | None = None,
139
139
  meta: dict[str, Any] | None = None,
140
140
  task: bool | TaskConfig | None = None,
141
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
141
142
  ) -> FunctionResourceTemplate:
142
143
  return FunctionResourceTemplate.from_function(
143
144
  fn=fn,
144
145
  uri_template=uri_template,
145
146
  name=name,
147
+ version=version,
146
148
  title=title,
147
149
  description=description,
148
150
  icons=icons,
149
151
  mime_type=mime_type,
150
152
  tags=tags,
151
- enabled=enabled,
152
153
  annotations=annotations,
153
154
  meta=meta,
154
155
  task=task,
156
+ auth=auth,
155
157
  )
156
158
 
157
159
  @field_validator("mime_type", mode="before")
@@ -166,12 +168,76 @@ class ResourceTemplate(FastMCPComponent):
166
168
  """Check if URI matches template and extract parameters."""
167
169
  return match_uri_template(uri, self.uri_template)
168
170
 
169
- async def read(self, arguments: dict[str, Any]) -> str | bytes:
171
+ async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
170
172
  """Read the resource content."""
171
173
  raise NotImplementedError(
172
174
  "Subclasses must implement read() or override create_resource()"
173
175
  )
174
176
 
177
+ def convert_result(self, raw_value: Any) -> ResourceResult:
178
+ """Convert a raw result to ResourceResult.
179
+
180
+ This is used in two contexts:
181
+ 1. In _read() to convert user function return values to ResourceResult
182
+ 2. In tasks_result_handler() to convert Docket task results to ResourceResult
183
+
184
+ Handles ResourceResult passthrough and converts raw values using
185
+ ResourceResult's normalization.
186
+ """
187
+ if isinstance(raw_value, ResourceResult):
188
+ return raw_value
189
+
190
+ # ResourceResult.__init__ handles all normalization
191
+ return ResourceResult(raw_value)
192
+
193
+ @overload
194
+ async def _read(
195
+ self, uri: str, params: dict[str, Any], task_meta: None = None
196
+ ) -> ResourceResult: ...
197
+
198
+ @overload
199
+ async def _read(
200
+ self, uri: str, params: dict[str, Any], task_meta: TaskMeta
201
+ ) -> mcp.types.CreateTaskResult: ...
202
+
203
+ async def _read(
204
+ self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
205
+ ) -> ResourceResult | mcp.types.CreateTaskResult:
206
+ """Server entry point that handles task routing.
207
+
208
+ This allows ANY ResourceTemplate subclass to support background execution
209
+ by setting task_config.mode to "supported" or "required". The server calls
210
+ this method instead of create_resource()/read() directly.
211
+
212
+ Args:
213
+ uri: The concrete URI being read
214
+ params: Template parameters extracted from the URI
215
+ task_meta: If provided, execute as a background task and return
216
+ CreateTaskResult. If None (default), execute synchronously and
217
+ return ResourceResult.
218
+
219
+ Returns:
220
+ ResourceResult when task_meta is None.
221
+ CreateTaskResult when task_meta is provided.
222
+
223
+ Subclasses can override this to customize task routing behavior.
224
+ For example, FastMCPProviderResourceTemplate overrides to delegate to child
225
+ middleware without submitting to Docket.
226
+ """
227
+ from fastmcp.server.tasks.routing import check_background_task
228
+
229
+ task_result = await check_background_task(
230
+ component=self, task_type="template", arguments=params, task_meta=task_meta
231
+ )
232
+ if task_result:
233
+ return task_result
234
+
235
+ # Synchronous execution - create resource and read directly
236
+ # Call resource.read() not resource._read() to avoid task routing on ephemeral resource
237
+ resource = await self.create_resource(uri, params)
238
+ result = await resource.read()
239
+ return self.convert_result(result)
240
+
175
241
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
176
242
  """Create a resource from the template with the given parameters.
177
243
 
@@ -185,8 +251,6 @@ class ResourceTemplate(FastMCPComponent):
185
251
 
186
252
  def to_mcp_template(
187
253
  self,
188
- *,
189
- include_fastmcp_meta: bool | None = None,
190
254
  **overrides: Any,
191
255
  ) -> SDKResourceTemplate:
192
256
  """Convert the resource template to an SDKResourceTemplate."""
@@ -199,8 +263,8 @@ class ResourceTemplate(FastMCPComponent):
199
263
  title=overrides.get("title", self.title),
200
264
  icons=overrides.get("icons", self.icons),
201
265
  annotations=overrides.get("annotations", self.annotations),
202
- _meta=overrides.get(
203
- "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
266
+ _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
267
+ "_meta", self.get_meta()
204
268
  ),
205
269
  )
206
270
 
@@ -219,28 +283,96 @@ class ResourceTemplate(FastMCPComponent):
219
283
 
220
284
  @property
221
285
  def key(self) -> str:
286
+ """The globally unique lookup key for this template."""
287
+ base_key = self.make_key(self.uri_template)
288
+ return f"{base_key}@{self.version or ''}"
289
+
290
+ def register_with_docket(self, docket: Docket) -> None:
291
+ """Register this template with docket for background execution."""
292
+ if not self.task_config.supports_tasks():
293
+ return
294
+ docket.register(self.read, names=[self.key])
295
+
296
+ async def add_to_docket( # type: ignore[override]
297
+ self,
298
+ docket: Docket,
299
+ params: dict[str, Any],
300
+ *,
301
+ fn_key: str | None = None,
302
+ task_key: str | None = None,
303
+ **kwargs: Any,
304
+ ) -> Execution:
305
+ """Schedule this template for background execution via docket.
306
+
307
+ Args:
308
+ docket: The Docket instance
309
+ params: Template parameters
310
+ fn_key: Function lookup key in Docket registry (defaults to self.key)
311
+ task_key: Redis storage key for the result
312
+ **kwargs: Additional kwargs passed to docket.add()
222
313
  """
223
- The key of the component. This is used for internal bookkeeping
224
- and may reflect e.g. prefixes or other identifiers. You should not depend on
225
- keys having a certain value, as the same tool loaded from different
226
- hierarchies of servers may have different keys.
227
- """
228
- return self._key or self.uri_template
314
+ lookup_key = fn_key or self.key
315
+ if task_key:
316
+ kwargs["key"] = task_key
317
+ return await docket.add(lookup_key, **kwargs)(params)
318
+
319
+ def get_span_attributes(self) -> dict[str, Any]:
320
+ return super().get_span_attributes() | {
321
+ "fastmcp.component.type": "resource_template",
322
+ "fastmcp.provider.type": "LocalProvider",
323
+ }
229
324
 
230
325
 
231
326
  class FunctionResourceTemplate(ResourceTemplate):
232
327
  """A template for dynamically creating resources."""
233
328
 
234
329
  fn: Callable[..., Any]
235
- task_config: Annotated[
236
- TaskConfig,
237
- Field(description="Background task execution configuration (SEP-1686)."),
238
- ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
330
+
331
+ @overload
332
+ async def _read(
333
+ self, uri: str, params: dict[str, Any], task_meta: None = None
334
+ ) -> ResourceResult: ...
335
+
336
+ @overload
337
+ async def _read(
338
+ self, uri: str, params: dict[str, Any], task_meta: TaskMeta
339
+ ) -> mcp.types.CreateTaskResult: ...
340
+
341
+ async def _read(
342
+ self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
343
+ ) -> ResourceResult | mcp.types.CreateTaskResult:
344
+ """Optimized server entry point that skips ephemeral resource creation.
345
+
346
+ For FunctionResourceTemplate, we can call read() directly instead of
347
+ creating a temporary resource, which is more efficient.
348
+
349
+ Args:
350
+ uri: The concrete URI being read
351
+ params: Template parameters extracted from the URI
352
+ task_meta: If provided, execute as a background task and return
353
+ CreateTaskResult. If None (default), execute synchronously and
354
+ return ResourceResult.
355
+
356
+ Returns:
357
+ ResourceResult when task_meta is None.
358
+ CreateTaskResult when task_meta is provided.
359
+ """
360
+ from fastmcp.server.tasks.routing import check_background_task
361
+
362
+ task_result = await check_background_task(
363
+ component=self, task_type="template", arguments=params, task_meta=task_meta
364
+ )
365
+ if task_result:
366
+ return task_result
367
+
368
+ # Synchronous execution - call read() directly, skip resource creation
369
+ result = await self.read(arguments=params)
370
+ return self.convert_result(result)
239
371
 
240
372
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
241
373
  """Create a resource from the template with the given parameters."""
242
374
 
243
- async def resource_read_fn() -> str | bytes:
375
+ async def resource_read_fn() -> str | bytes | ResourceResult:
244
376
  # Call function and check if result is a coroutine
245
377
  result = await self.read(arguments=params)
246
378
  return result
@@ -252,11 +384,11 @@ class FunctionResourceTemplate(ResourceTemplate):
252
384
  description=self.description,
253
385
  mime_type=self.mime_type,
254
386
  tags=self.tags,
255
- enabled=self.enabled,
256
387
  task=self.task_config,
388
+ auth=self.auth,
257
389
  )
258
390
 
259
- async def read(self, arguments: dict[str, Any]) -> str | bytes:
391
+ async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
260
392
  """Read the resource content."""
261
393
  # Type coercion for query parameters (which arrive as strings)
262
394
  kwargs = arguments.copy()
@@ -287,21 +419,57 @@ class FunctionResourceTemplate(ResourceTemplate):
287
419
 
288
420
  return result
289
421
 
422
+ def register_with_docket(self, docket: Docket) -> None:
423
+ """Register this template with docket for background execution.
424
+
425
+ FunctionResourceTemplate registers the underlying function, which has the
426
+ user's Depends parameters for docket to resolve.
427
+ """
428
+ if not self.task_config.supports_tasks():
429
+ return
430
+ docket.register(self.fn, names=[self.key])
431
+
432
+ async def add_to_docket(
433
+ self,
434
+ docket: Docket,
435
+ params: dict[str, Any],
436
+ *,
437
+ fn_key: str | None = None,
438
+ task_key: str | None = None,
439
+ **kwargs: Any,
440
+ ) -> Execution:
441
+ """Schedule this template for background execution via docket.
442
+
443
+ FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
444
+
445
+ Args:
446
+ docket: The Docket instance
447
+ params: Template parameters
448
+ fn_key: Function lookup key in Docket registry (defaults to self.key)
449
+ task_key: Redis storage key for the result
450
+ **kwargs: Additional kwargs passed to docket.add()
451
+ """
452
+ lookup_key = fn_key or self.key
453
+ if task_key:
454
+ kwargs["key"] = task_key
455
+ return await docket.add(lookup_key, **kwargs)(**params)
456
+
290
457
  @classmethod
291
458
  def from_function(
292
459
  cls,
293
460
  fn: Callable[..., Any],
294
461
  uri_template: str,
295
462
  name: str | None = None,
463
+ version: str | int | None = None,
296
464
  title: str | None = None,
297
465
  description: str | None = None,
298
466
  icons: list[Icon] | None = None,
299
467
  mime_type: str | None = None,
300
468
  tags: set[str] | None = None,
301
- enabled: bool | None = None,
302
469
  annotations: Annotations | None = None,
303
470
  meta: dict[str, Any] | None = None,
304
471
  task: bool | TaskConfig | None = None,
472
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
305
473
  ) -> FunctionResourceTemplate:
306
474
  """Create a template from a function."""
307
475
 
@@ -388,6 +556,9 @@ class FunctionResourceTemplate(ResourceTemplate):
388
556
  if isinstance(fn, staticmethod):
389
557
  fn = fn.__func__
390
558
 
559
+ # Transform Context type annotations to Depends() for unified DI
560
+ fn = transform_context_annotations(fn)
561
+
391
562
  wrapper_fn = without_injected_parameters(fn)
392
563
  type_adapter = get_cached_typeadapter(wrapper_fn)
393
564
  parameters = type_adapter.json_schema()
@@ -399,6 +570,7 @@ class FunctionResourceTemplate(ResourceTemplate):
399
570
  return cls(
400
571
  uri_template=uri_template,
401
572
  name=func_name,
573
+ version=str(version) if version is not None else None,
402
574
  title=title,
403
575
  description=description,
404
576
  icons=icons,
@@ -406,8 +578,8 @@ class FunctionResourceTemplate(ResourceTemplate):
406
578
  fn=fn,
407
579
  parameters=parameters,
408
580
  tags=tags or set(),
409
- enabled=enabled if enabled is not None else True,
410
581
  annotations=annotations,
411
582
  meta=meta,
412
583
  task_config=task_config,
584
+ auth=auth,
413
585
  )
@@ -12,7 +12,7 @@ from pydantic import Field, ValidationInfo
12
12
  from typing_extensions import override
13
13
 
14
14
  from fastmcp.exceptions import ResourceError
15
- from fastmcp.resources.resource import Resource
15
+ from fastmcp.resources.resource import Resource, ResourceContent, ResourceResult
16
16
  from fastmcp.utilities.logging import get_logger
17
17
 
18
18
  logger = get_logger(__name__)
@@ -23,9 +23,11 @@ class TextResource(Resource):
23
23
 
24
24
  text: str = Field(description="Text content of the resource")
25
25
 
26
- async def read(self) -> str:
26
+ async def read(self) -> ResourceResult:
27
27
  """Read the text content."""
28
- return self.text
28
+ return ResourceResult(
29
+ contents=[ResourceContent(content=self.text, mime_type=self.mime_type)]
30
+ )
29
31
 
30
32
 
31
33
  class BinaryResource(Resource):
@@ -33,9 +35,11 @@ class BinaryResource(Resource):
33
35
 
34
36
  data: bytes = Field(description="Binary content of the resource")
35
37
 
36
- async def read(self) -> bytes:
38
+ async def read(self) -> ResourceResult:
37
39
  """Read the binary content."""
38
- return self.data
40
+ return ResourceResult(
41
+ contents=[ResourceContent(content=self.data, mime_type=self.mime_type)]
42
+ )
39
43
 
40
44
 
41
45
  class FileResource(Resource):
@@ -76,12 +80,16 @@ class FileResource(Resource):
76
80
  return not mime_type.startswith("text/")
77
81
 
78
82
  @override
79
- async def read(self) -> str | bytes:
83
+ async def read(self) -> ResourceResult:
80
84
  """Read the file content."""
81
85
  try:
82
86
  if self.is_binary:
83
- return await self._async_path.read_bytes()
84
- return await self._async_path.read_text()
87
+ content: str | bytes = await self._async_path.read_bytes()
88
+ else:
89
+ content = await self._async_path.read_text()
90
+ return ResourceResult(
91
+ contents=[ResourceContent(content=content, mime_type=self.mime_type)]
92
+ )
85
93
  except Exception as e:
86
94
  raise ResourceError(f"Error reading file {self.path}") from e
87
95
 
@@ -95,12 +103,16 @@ class HttpResource(Resource):
95
103
  )
96
104
 
97
105
  @override
98
- async def read(self) -> str | bytes:
106
+ async def read(self) -> ResourceResult:
99
107
  """Read the HTTP content."""
100
108
  async with httpx.AsyncClient() as client:
101
109
  response = await client.get(self.url)
102
110
  _ = response.raise_for_status()
103
- return response.text
111
+ return ResourceResult(
112
+ contents=[
113
+ ResourceContent(content=response.text, mime_type=self.mime_type)
114
+ ]
115
+ )
104
116
 
105
117
 
106
118
  class DirectoryResource(Resource):
@@ -145,13 +157,16 @@ class DirectoryResource(Resource):
145
157
  raise ResourceError(f"Error listing directory {self.path}") from e
146
158
 
147
159
  @override
148
- async def read(self) -> str: # Always returns JSON string
160
+ async def read(self) -> ResourceResult:
149
161
  """Read the directory listing."""
150
162
  try:
151
163
  files: list[Path] = await self.list_files()
152
164
 
153
165
  file_list = [str(f.relative_to(self.path)) for f in files]
154
166
 
155
- return json.dumps({"files": file_list}, indent=2)
167
+ content = json.dumps({"files": file_list}, indent=2)
168
+ return ResourceResult(
169
+ contents=[ResourceContent(content=content, mime_type=self.mime_type)]
170
+ )
156
171
  except Exception as e:
157
172
  raise ResourceError(f"Error reading directory {self.path}") from e
@@ -1,6 +1,6 @@
1
- from .server import FastMCP
2
1
  from .context import Context
2
+ from .server import FastMCP, create_proxy
3
3
  from . import dependencies
4
4
 
5
5
 
6
- __all__ = ["Context", "FastMCP"]
6
+ __all__ = ["Context", "FastMCP", "create_proxy"]
@@ -5,6 +5,14 @@ from .auth import (
5
5
  AccessToken,
6
6
  AuthProvider,
7
7
  )
8
+ from .authorization import (
9
+ AuthCheck,
10
+ AuthContext,
11
+ require_auth,
12
+ require_scopes,
13
+ restrict_tag,
14
+ run_auth_checks,
15
+ )
8
16
  from .providers.debug import DebugTokenVerifier
9
17
  from .providers.jwt import JWTVerifier, StaticTokenVerifier
10
18
  from .oauth_proxy import OAuthProxy
@@ -13,6 +21,8 @@ from .oidc_proxy import OIDCProxy
13
21
 
14
22
  __all__ = [
15
23
  "AccessToken",
24
+ "AuthCheck",
25
+ "AuthContext",
16
26
  "AuthProvider",
17
27
  "DebugTokenVerifier",
18
28
  "JWTVerifier",
@@ -22,4 +32,8 @@ __all__ = [
22
32
  "RemoteAuthProvider",
23
33
  "StaticTokenVerifier",
24
34
  "TokenVerifier",
35
+ "require_auth",
36
+ "require_scopes",
37
+ "restrict_tag",
38
+ "run_auth_checks",
25
39
  ]
@@ -47,19 +47,19 @@ class AccessToken(_SDKAccessToken):
47
47
 
48
48
 
49
49
  class TokenHandler(_SDKTokenHandler):
50
- """TokenHandler that returns OAuth 2.1 compliant error responses.
50
+ """TokenHandler that returns MCP-compliant error responses.
51
51
 
52
- The MCP SDK returns `unauthorized_client` for client authentication failures.
53
- However, per RFC 6749 Section 5.2, authentication failures should return
54
- `invalid_client` with HTTP 401, not `unauthorized_client`.
52
+ This handler addresses two SDK issues:
55
53
 
56
- This distinction matters: `unauthorized_client` means "client exists but
57
- can't do this", while `invalid_client` means "client doesn't exist or
58
- credentials are wrong". Claude's OAuth client uses this to decide whether
59
- to re-register.
54
+ 1. Error code: The SDK returns `unauthorized_client` for client authentication
55
+ failures, but RFC 6749 Section 5.2 requires `invalid_client` with HTTP 401.
56
+ This distinction matters for client re-registration behavior.
60
57
 
61
- This handler transforms 401 responses with `unauthorized_client` to use
62
- `invalid_client` instead, making the error semantics correct per OAuth spec.
58
+ 2. Status code: The SDK returns HTTP 400 for all token errors including
59
+ `invalid_grant` (expired/invalid tokens). However, the MCP spec requires:
60
+ "Invalid or expired tokens MUST receive a HTTP 401 response."
61
+
62
+ This handler transforms responses to be compliant with both OAuth 2.1 and MCP specs.
63
63
  """
64
64
 
65
65
  async def handle(self, request: Any):
@@ -85,6 +85,26 @@ class TokenHandler(_SDKTokenHandler):
85
85
  except (json.JSONDecodeError, AttributeError):
86
86
  pass # Not JSON or unexpected format, return as-is
87
87
 
88
+ # Transform 400 invalid_grant -> 401 for expired/invalid tokens
89
+ # Per MCP spec: "Invalid or expired tokens MUST receive a HTTP 401 response."
90
+ if response.status_code == 400:
91
+ try:
92
+ body = json.loads(response.body)
93
+ if body.get("error") == "invalid_grant":
94
+ return PydanticJSONResponse(
95
+ content=TokenErrorResponse(
96
+ error="invalid_grant",
97
+ error_description=body.get("error_description"),
98
+ ),
99
+ status_code=401,
100
+ headers={
101
+ "Cache-Control": "no-store",
102
+ "Pragma": "no-cache",
103
+ },
104
+ )
105
+ except (json.JSONDecodeError, AttributeError):
106
+ pass # Not JSON or unexpected format, return as-is
107
+
88
108
  return response
89
109
 
90
110