fastmcp 2.13.3__py3-none-any.whl → 2.14.1__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 (85) hide show
  1. fastmcp/__init__.py +0 -21
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +8 -22
  5. fastmcp/cli/install/shared.py +0 -15
  6. fastmcp/cli/tasks.py +110 -0
  7. fastmcp/client/auth/oauth.py +9 -9
  8. fastmcp/client/client.py +739 -136
  9. fastmcp/client/elicitation.py +11 -5
  10. fastmcp/client/messages.py +7 -5
  11. fastmcp/client/roots.py +2 -1
  12. fastmcp/client/sampling/__init__.py +69 -0
  13. fastmcp/client/sampling/handlers/__init__.py +0 -0
  14. fastmcp/client/sampling/handlers/anthropic.py +387 -0
  15. fastmcp/client/sampling/handlers/openai.py +399 -0
  16. fastmcp/client/tasks.py +551 -0
  17. fastmcp/client/transports.py +72 -21
  18. fastmcp/contrib/component_manager/component_service.py +4 -20
  19. fastmcp/dependencies.py +25 -0
  20. fastmcp/experimental/sampling/handlers/__init__.py +5 -0
  21. fastmcp/experimental/sampling/handlers/openai.py +4 -169
  22. fastmcp/experimental/server/openapi/__init__.py +15 -13
  23. fastmcp/experimental/utilities/openapi/__init__.py +12 -38
  24. fastmcp/prompts/prompt.py +38 -38
  25. fastmcp/resources/resource.py +33 -16
  26. fastmcp/resources/template.py +69 -59
  27. fastmcp/server/auth/__init__.py +0 -9
  28. fastmcp/server/auth/auth.py +127 -3
  29. fastmcp/server/auth/oauth_proxy.py +47 -97
  30. fastmcp/server/auth/oidc_proxy.py +7 -0
  31. fastmcp/server/auth/providers/in_memory.py +2 -2
  32. fastmcp/server/auth/providers/oci.py +2 -2
  33. fastmcp/server/context.py +509 -180
  34. fastmcp/server/dependencies.py +464 -6
  35. fastmcp/server/elicitation.py +285 -47
  36. fastmcp/server/event_store.py +177 -0
  37. fastmcp/server/http.py +15 -3
  38. fastmcp/server/low_level.py +56 -12
  39. fastmcp/server/middleware/middleware.py +2 -2
  40. fastmcp/server/openapi/__init__.py +35 -0
  41. fastmcp/{experimental/server → server}/openapi/components.py +4 -3
  42. fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
  43. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  44. fastmcp/server/proxy.py +53 -40
  45. fastmcp/server/sampling/__init__.py +10 -0
  46. fastmcp/server/sampling/run.py +301 -0
  47. fastmcp/server/sampling/sampling_tool.py +108 -0
  48. fastmcp/server/server.py +793 -552
  49. fastmcp/server/tasks/__init__.py +21 -0
  50. fastmcp/server/tasks/capabilities.py +22 -0
  51. fastmcp/server/tasks/config.py +89 -0
  52. fastmcp/server/tasks/converters.py +206 -0
  53. fastmcp/server/tasks/handlers.py +356 -0
  54. fastmcp/server/tasks/keys.py +93 -0
  55. fastmcp/server/tasks/protocol.py +355 -0
  56. fastmcp/server/tasks/subscriptions.py +205 -0
  57. fastmcp/settings.py +101 -103
  58. fastmcp/tools/tool.py +83 -49
  59. fastmcp/tools/tool_transform.py +1 -12
  60. fastmcp/utilities/components.py +3 -3
  61. fastmcp/utilities/json_schema_type.py +4 -4
  62. fastmcp/utilities/mcp_config.py +1 -2
  63. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  64. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  65. fastmcp/utilities/openapi/__init__.py +63 -0
  66. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  67. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
  68. fastmcp/utilities/tests.py +11 -5
  69. fastmcp/utilities/types.py +8 -0
  70. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/METADATA +7 -4
  71. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/RECORD +79 -63
  72. fastmcp/client/sampling.py +0 -56
  73. fastmcp/experimental/sampling/handlers/base.py +0 -21
  74. fastmcp/server/auth/providers/bearer.py +0 -25
  75. fastmcp/server/openapi.py +0 -1087
  76. fastmcp/server/sampling/handler.py +0 -19
  77. fastmcp/utilities/openapi.py +0 -1568
  78. /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  79. /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
  80. /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
  81. /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
  82. /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
  83. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/WHEEL +0 -0
  84. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/entry_points.txt +0 -0
  85. {fastmcp-2.13.3.dist-info → fastmcp-2.14.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py CHANGED
@@ -5,22 +5,22 @@ from __future__ import annotations as _annotations
5
5
  import inspect
6
6
  import json
7
7
  from collections.abc import Awaitable, Callable, Sequence
8
- from typing import Any
8
+ from typing import Annotated, Any
9
9
 
10
10
  import pydantic_core
11
11
  from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
12
- from mcp.types import Prompt as MCPPrompt
13
- from mcp.types import PromptArgument as MCPPromptArgument
12
+ from mcp.types import Prompt as SDKPrompt
13
+ from mcp.types import PromptArgument as SDKPromptArgument
14
14
  from pydantic import Field, TypeAdapter
15
15
 
16
16
  from fastmcp.exceptions import PromptError
17
- from fastmcp.server.dependencies import get_context
17
+ from fastmcp.server.dependencies import get_context, without_injected_parameters
18
+ from fastmcp.server.tasks.config import TaskConfig
18
19
  from fastmcp.utilities.components import FastMCPComponent
19
20
  from fastmcp.utilities.json_schema import compress_schema
20
21
  from fastmcp.utilities.logging import get_logger
21
22
  from fastmcp.utilities.types import (
22
23
  FastMCPBaseModel,
23
- find_kwarg_by_type,
24
24
  get_cached_typeadapter,
25
25
  )
26
26
 
@@ -89,10 +89,10 @@ class Prompt(FastMCPComponent):
89
89
  *,
90
90
  include_fastmcp_meta: bool | None = None,
91
91
  **overrides: Any,
92
- ) -> MCPPrompt:
92
+ ) -> SDKPrompt:
93
93
  """Convert the prompt to an MCP prompt."""
94
94
  arguments = [
95
- MCPPromptArgument(
95
+ SDKPromptArgument(
96
96
  name=arg.name,
97
97
  description=arg.description,
98
98
  required=arg.required,
@@ -100,7 +100,7 @@ class Prompt(FastMCPComponent):
100
100
  for arg in self.arguments or []
101
101
  ]
102
102
 
103
- return MCPPrompt(
103
+ return SDKPrompt(
104
104
  name=overrides.get("name", self.name),
105
105
  description=overrides.get("description", self.description),
106
106
  arguments=arguments,
@@ -121,6 +121,7 @@ class Prompt(FastMCPComponent):
121
121
  tags: set[str] | None = None,
122
122
  enabled: bool | None = None,
123
123
  meta: dict[str, Any] | None = None,
124
+ task: bool | TaskConfig | None = None,
124
125
  ) -> FunctionPrompt:
125
126
  """Create a Prompt from a function.
126
127
 
@@ -139,6 +140,7 @@ class Prompt(FastMCPComponent):
139
140
  tags=tags,
140
141
  enabled=enabled,
141
142
  meta=meta,
143
+ task=task,
142
144
  )
143
145
 
144
146
  async def render(
@@ -157,6 +159,10 @@ class FunctionPrompt(Prompt):
157
159
  """A prompt that is a function."""
158
160
 
159
161
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
162
+ task_config: Annotated[
163
+ TaskConfig,
164
+ Field(description="Background task execution configuration (SEP-1686)."),
165
+ ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
160
166
 
161
167
  @classmethod
162
168
  def from_function(
@@ -169,6 +175,7 @@ class FunctionPrompt(Prompt):
169
175
  tags: set[str] | None = None,
170
176
  enabled: bool | None = None,
171
177
  meta: dict[str, Any] | None = None,
178
+ task: bool | TaskConfig | None = None,
172
179
  ) -> FunctionPrompt:
173
180
  """Create a Prompt from a function.
174
181
 
@@ -178,7 +185,6 @@ class FunctionPrompt(Prompt):
178
185
  - A dict (converted to a message)
179
186
  - A sequence of any of the above
180
187
  """
181
- from fastmcp.server.context import Context
182
188
 
183
189
  func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
184
190
 
@@ -194,22 +200,27 @@ class FunctionPrompt(Prompt):
194
200
 
195
201
  description = description or inspect.getdoc(fn)
196
202
 
203
+ # Normalize task to TaskConfig and validate
204
+ if task is None:
205
+ task_config = TaskConfig(mode="forbidden")
206
+ elif isinstance(task, bool):
207
+ task_config = TaskConfig.from_bool(task)
208
+ else:
209
+ task_config = task
210
+ task_config.validate_function(fn, func_name)
211
+
197
212
  # if the fn is a callable class, we need to get the __call__ method from here out
198
213
  if not inspect.isroutine(fn):
199
214
  fn = fn.__call__
200
215
  # if the fn is a staticmethod, we need to work with the underlying function
201
216
  if isinstance(fn, staticmethod):
202
- fn = fn.__func__
217
+ fn = fn.__func__ # type: ignore[assignment]
203
218
 
204
- type_adapter = get_cached_typeadapter(fn)
219
+ # Wrap fn to handle dependency resolution internally
220
+ wrapped_fn = without_injected_parameters(fn)
221
+ type_adapter = get_cached_typeadapter(wrapped_fn)
205
222
  parameters = type_adapter.json_schema()
206
-
207
- # Auto-detect context parameter if not provided
208
-
209
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
210
- prune_params = [context_kwarg] if context_kwarg else None
211
-
212
- parameters = compress_schema(parameters, prune_params=prune_params)
223
+ parameters = compress_schema(parameters, prune_titles=True)
213
224
 
214
225
  # Convert parameters to PromptArguments
215
226
  arguments: list[PromptArgument] = []
@@ -224,7 +235,6 @@ class FunctionPrompt(Prompt):
224
235
  if (
225
236
  sig_param.annotation != inspect.Parameter.empty
226
237
  and sig_param.annotation is not str
227
- and param_name != context_kwarg
228
238
  ):
229
239
  # Get the JSON schema for this specific parameter type
230
240
  try:
@@ -260,29 +270,23 @@ class FunctionPrompt(Prompt):
260
270
  arguments=arguments,
261
271
  tags=tags or set(),
262
272
  enabled=enabled if enabled is not None else True,
263
- fn=fn,
273
+ fn=wrapped_fn,
264
274
  meta=meta,
275
+ task_config=task_config,
265
276
  )
266
277
 
267
278
  def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
268
279
  """Convert string arguments to expected types based on function signature."""
269
- from fastmcp.server.context import Context
280
+ from fastmcp.server.dependencies import without_injected_parameters
270
281
 
271
- sig = inspect.signature(self.fn)
282
+ wrapper_fn = without_injected_parameters(self.fn)
283
+ sig = inspect.signature(wrapper_fn)
272
284
  converted_kwargs = {}
273
285
 
274
- # Find context parameter name if any
275
- context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context)
276
-
277
286
  for param_name, param_value in kwargs.items():
278
287
  if param_name in sig.parameters:
279
288
  param = sig.parameters[param_name]
280
289
 
281
- # Skip Context parameters - they're handled separately
282
- if param_name == context_param_name:
283
- converted_kwargs[param_name] = param_value
284
- continue
285
-
286
290
  # If parameter has no annotation or annotation is str, pass as-is
287
291
  if (
288
292
  param.annotation == inspect.Parameter.empty
@@ -320,8 +324,6 @@ class FunctionPrompt(Prompt):
320
324
  arguments: dict[str, Any] | None = None,
321
325
  ) -> list[PromptMessage]:
322
326
  """Render the prompt with arguments."""
323
- from fastmcp.server.context import Context
324
-
325
327
  # Validate required arguments
326
328
  if self.arguments:
327
329
  required = {arg.name for arg in self.arguments if arg.required}
@@ -331,16 +333,14 @@ class FunctionPrompt(Prompt):
331
333
  raise ValueError(f"Missing required arguments: {missing}")
332
334
 
333
335
  try:
334
- # Prepare arguments with context
336
+ # Prepare arguments
335
337
  kwargs = arguments.copy() if arguments else {}
336
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
337
- if context_kwarg and context_kwarg not in kwargs:
338
- kwargs[context_kwarg] = get_context()
339
338
 
340
- # Convert string arguments to expected types when needed
339
+ # Convert string arguments to expected types BEFORE validation
341
340
  kwargs = self._convert_string_arguments(kwargs)
342
341
 
343
- # Call function and check if result is a coroutine
342
+ # self.fn is wrapped by without_injected_parameters which handles
343
+ # dependency resolution internally
344
344
  result = self.fn(**kwargs)
345
345
  if inspect.isawaitable(result):
346
346
  result = await result
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Annotated, Any
8
8
 
9
9
  import pydantic_core
10
10
  from mcp.types import Annotations, Icon
11
- from mcp.types import Resource as MCPResource
11
+ from mcp.types import Resource as SDKResource
12
12
  from pydantic import (
13
13
  AnyUrl,
14
14
  ConfigDict,
@@ -19,10 +19,10 @@ from pydantic import (
19
19
  )
20
20
  from typing_extensions import Self
21
21
 
22
- from fastmcp.server.dependencies import get_context
22
+ from fastmcp.server.dependencies import get_context, without_injected_parameters
23
+ from fastmcp.server.tasks.config import TaskConfig
23
24
  from fastmcp.utilities.components import FastMCPComponent
24
25
  from fastmcp.utilities.types import (
25
- find_kwarg_by_type,
26
26
  get_fn_name,
27
27
  )
28
28
 
@@ -42,7 +42,6 @@ class Resource(FastMCPComponent):
42
42
  mime_type: str = Field(
43
43
  default="text/plain",
44
44
  description="MIME type of the resource content",
45
- pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
46
45
  )
47
46
  annotations: Annotated[
48
47
  Annotations | None,
@@ -78,6 +77,7 @@ class Resource(FastMCPComponent):
78
77
  enabled: bool | None = None,
79
78
  annotations: Annotations | None = None,
80
79
  meta: dict[str, Any] | None = None,
80
+ task: bool | TaskConfig | None = None,
81
81
  ) -> FunctionResource:
82
82
  return FunctionResource.from_function(
83
83
  fn=fn,
@@ -91,6 +91,7 @@ class Resource(FastMCPComponent):
91
91
  enabled=enabled,
92
92
  annotations=annotations,
93
93
  meta=meta,
94
+ task=task,
94
95
  )
95
96
 
96
97
  @field_validator("mime_type", mode="before")
@@ -125,10 +126,10 @@ class Resource(FastMCPComponent):
125
126
  *,
126
127
  include_fastmcp_meta: bool | None = None,
127
128
  **overrides: Any,
128
- ) -> MCPResource:
129
- """Convert the resource to an MCPResource."""
129
+ ) -> SDKResource:
130
+ """Convert the resource to an SDKResource."""
130
131
 
131
- return MCPResource(
132
+ return SDKResource(
132
133
  name=overrides.get("name", self.name),
133
134
  uri=overrides.get("uri", self.uri),
134
135
  description=overrides.get("description", self.description),
@@ -169,6 +170,10 @@ class FunctionResource(Resource):
169
170
  """
170
171
 
171
172
  fn: Callable[..., Any]
173
+ task_config: Annotated[
174
+ TaskConfig,
175
+ Field(description="Background task execution configuration (SEP-1686)."),
176
+ ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
172
177
 
173
178
  @classmethod
174
179
  def from_function(
@@ -184,12 +189,28 @@ class FunctionResource(Resource):
184
189
  enabled: bool | None = None,
185
190
  annotations: Annotations | None = None,
186
191
  meta: dict[str, Any] | None = None,
192
+ task: bool | TaskConfig | None = None,
187
193
  ) -> FunctionResource:
188
194
  """Create a FunctionResource from a function."""
189
195
  if isinstance(uri, str):
190
196
  uri = AnyUrl(uri)
197
+
198
+ func_name = name or get_fn_name(fn)
199
+
200
+ # Normalize task to TaskConfig and validate
201
+ if task is None:
202
+ task_config = TaskConfig(mode="forbidden")
203
+ elif isinstance(task, bool):
204
+ task_config = TaskConfig.from_bool(task)
205
+ else:
206
+ task_config = task
207
+ task_config.validate_function(fn, func_name)
208
+
209
+ # Wrap fn to handle dependency resolution internally
210
+ wrapped_fn = without_injected_parameters(fn)
211
+
191
212
  return cls(
192
- fn=fn,
213
+ fn=wrapped_fn,
193
214
  uri=uri,
194
215
  name=name or get_fn_name(fn),
195
216
  title=title,
@@ -200,18 +221,14 @@ class FunctionResource(Resource):
200
221
  enabled=enabled if enabled is not None else True,
201
222
  annotations=annotations,
202
223
  meta=meta,
224
+ task_config=task_config,
203
225
  )
204
226
 
205
227
  async def read(self) -> str | bytes:
206
228
  """Read the resource by calling the wrapped function."""
207
- from fastmcp.server.context import Context
208
-
209
- kwargs = {}
210
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
211
- if context_kwarg is not None:
212
- kwargs[context_kwarg] = get_context()
213
-
214
- result = self.fn(**kwargs)
229
+ # self.fn is wrapped by without_injected_parameters which handles
230
+ # dependency resolution internally
231
+ result = self.fn()
215
232
  if inspect.isawaitable(result):
216
233
  result = await result
217
234
 
@@ -5,11 +5,11 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
- from typing import Any
8
+ from typing import Annotated, Any
9
9
  from urllib.parse import parse_qs, unquote
10
10
 
11
11
  from mcp.types import Annotations, Icon
12
- from mcp.types import ResourceTemplate as MCPResourceTemplate
12
+ from mcp.types import ResourceTemplate as SDKResourceTemplate
13
13
  from pydantic import (
14
14
  Field,
15
15
  field_validator,
@@ -17,13 +17,11 @@ from pydantic import (
17
17
  )
18
18
 
19
19
  from fastmcp.resources.resource import Resource
20
- from fastmcp.server.dependencies import get_context
20
+ from fastmcp.server.dependencies import get_context, without_injected_parameters
21
+ from fastmcp.server.tasks.config import TaskConfig
21
22
  from fastmcp.utilities.components import FastMCPComponent
22
23
  from fastmcp.utilities.json_schema import compress_schema
23
- from fastmcp.utilities.types import (
24
- find_kwarg_by_type,
25
- get_cached_typeadapter,
26
- )
24
+ from fastmcp.utilities.types import get_cached_typeadapter
27
25
 
28
26
 
29
27
  def extract_query_params(uri_template: str) -> set[str]:
@@ -139,6 +137,7 @@ class ResourceTemplate(FastMCPComponent):
139
137
  enabled: bool | None = None,
140
138
  annotations: Annotations | None = None,
141
139
  meta: dict[str, Any] | None = None,
140
+ task: bool | TaskConfig | None = None,
142
141
  ) -> FunctionResourceTemplate:
143
142
  return FunctionResourceTemplate.from_function(
144
143
  fn=fn,
@@ -152,6 +151,7 @@ class ResourceTemplate(FastMCPComponent):
152
151
  enabled=enabled,
153
152
  annotations=annotations,
154
153
  meta=meta,
154
+ task=task,
155
155
  )
156
156
 
157
157
  @field_validator("mime_type", mode="before")
@@ -173,21 +173,14 @@ class ResourceTemplate(FastMCPComponent):
173
173
  )
174
174
 
175
175
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
176
- """Create a resource from the template with the given parameters."""
177
-
178
- async def resource_read_fn() -> str | bytes:
179
- # Call function and check if result is a coroutine
180
- result = await self.read(arguments=params)
181
- return result
176
+ """Create a resource from the template with the given parameters.
182
177
 
183
- return Resource.from_function(
184
- fn=resource_read_fn,
185
- uri=uri,
186
- name=self.name,
187
- description=self.description,
188
- mime_type=self.mime_type,
189
- tags=self.tags,
190
- enabled=self.enabled,
178
+ The base implementation does not support background tasks.
179
+ Use FunctionResourceTemplate for task support.
180
+ """
181
+ raise NotImplementedError(
182
+ "Subclasses must implement create_resource(). "
183
+ "Use FunctionResourceTemplate for task support."
191
184
  )
192
185
 
193
186
  def to_mcp_template(
@@ -195,10 +188,10 @@ class ResourceTemplate(FastMCPComponent):
195
188
  *,
196
189
  include_fastmcp_meta: bool | None = None,
197
190
  **overrides: Any,
198
- ) -> MCPResourceTemplate:
199
- """Convert the resource template to an MCPResourceTemplate."""
191
+ ) -> SDKResourceTemplate:
192
+ """Convert the resource template to an SDKResourceTemplate."""
200
193
 
201
- return MCPResourceTemplate(
194
+ return SDKResourceTemplate(
202
195
  name=overrides.get("name", self.name),
203
196
  uriTemplate=overrides.get("uriTemplate", self.uri_template),
204
197
  description=overrides.get("description", self.description),
@@ -212,7 +205,7 @@ class ResourceTemplate(FastMCPComponent):
212
205
  )
213
206
 
214
207
  @classmethod
215
- def from_mcp_template(cls, mcp_template: MCPResourceTemplate) -> ResourceTemplate:
208
+ def from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate:
216
209
  """Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object."""
217
210
  # Note: This creates a simple ResourceTemplate instance. For function-based templates,
218
211
  # the original function is lost, which is expected for remote templates.
@@ -239,45 +232,59 @@ class FunctionResourceTemplate(ResourceTemplate):
239
232
  """A template for dynamically creating resources."""
240
233
 
241
234
  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"))
242
239
 
243
- async def read(self, arguments: dict[str, Any]) -> str | bytes:
244
- """Read the resource content."""
245
- from fastmcp.server.context import Context
240
+ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
241
+ """Create a resource from the template with the given parameters."""
246
242
 
247
- # Add context to parameters if needed
248
- kwargs = arguments.copy()
249
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
250
- if context_kwarg and context_kwarg not in kwargs:
251
- kwargs[context_kwarg] = get_context()
243
+ async def resource_read_fn() -> str | bytes:
244
+ # Call function and check if result is a coroutine
245
+ result = await self.read(arguments=params)
246
+ return result
247
+
248
+ return Resource.from_function(
249
+ fn=resource_read_fn,
250
+ uri=uri,
251
+ name=self.name,
252
+ description=self.description,
253
+ mime_type=self.mime_type,
254
+ tags=self.tags,
255
+ enabled=self.enabled,
256
+ task=self.task_config,
257
+ )
252
258
 
259
+ async def read(self, arguments: dict[str, Any]) -> str | bytes:
260
+ """Read the resource content."""
253
261
  # Type coercion for query parameters (which arrive as strings)
254
- # Get function signature for type hints
262
+ kwargs = arguments.copy()
255
263
  sig = inspect.signature(self.fn)
256
264
  for param_name, param_value in list(kwargs.items()):
257
265
  if param_name in sig.parameters and isinstance(param_value, str):
258
266
  param = sig.parameters[param_name]
259
267
  annotation = param.annotation
260
268
 
261
- # Skip if no annotation or annotation is str
262
269
  if annotation is inspect.Parameter.empty or annotation is str:
263
270
  continue
264
271
 
265
- # Handle common type coercions
266
272
  try:
267
273
  if annotation is int:
268
274
  kwargs[param_name] = int(param_value)
269
275
  elif annotation is float:
270
276
  kwargs[param_name] = float(param_value)
271
277
  elif annotation is bool:
272
- # Handle boolean strings
273
278
  kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
274
279
  except (ValueError, AttributeError):
275
- # Let validate_call handle the error
276
280
  pass
277
281
 
282
+ # self.fn is wrapped by without_injected_parameters which handles
283
+ # dependency resolution internally, so we call it directly
278
284
  result = self.fn(**kwargs)
279
285
  if inspect.isawaitable(result):
280
286
  result = await result
287
+
281
288
  return result
282
289
 
283
290
  @classmethod
@@ -294,9 +301,9 @@ class FunctionResourceTemplate(ResourceTemplate):
294
301
  enabled: bool | None = None,
295
302
  annotations: Annotations | None = None,
296
303
  meta: dict[str, Any] | None = None,
304
+ task: bool | TaskConfig | None = None,
297
305
  ) -> FunctionResourceTemplate:
298
306
  """Create a template from a function."""
299
- from fastmcp.server.context import Context
300
307
 
301
308
  func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
302
309
  if func_name == "<lambda>":
@@ -311,10 +318,6 @@ class FunctionResourceTemplate(ResourceTemplate):
311
318
  "Functions with *args are not supported as resource templates"
312
319
  )
313
320
 
314
- # Auto-detect context parameter if not provided
315
-
316
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
317
-
318
321
  # Extract path and query parameters from URI template
319
322
  path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
320
323
  query_params = extract_query_params(uri_template)
@@ -323,24 +326,23 @@ class FunctionResourceTemplate(ResourceTemplate):
323
326
  if not all_uri_params:
324
327
  raise ValueError("URI template must contain at least one parameter")
325
328
 
326
- func_params = set(sig.parameters.keys())
327
- if context_kwarg:
328
- func_params.discard(context_kwarg)
329
+ # Use wrapper to get user-facing parameters (excludes injected params)
330
+ wrapper_fn = without_injected_parameters(fn)
331
+ user_sig = inspect.signature(wrapper_fn)
332
+ func_params = set(user_sig.parameters.keys())
329
333
 
330
334
  # Get required and optional function parameters
331
335
  required_params = {
332
336
  p
333
337
  for p in func_params
334
- if sig.parameters[p].default is inspect.Parameter.empty
335
- and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
336
- and p != context_kwarg
338
+ if user_sig.parameters[p].default is inspect.Parameter.empty
339
+ and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
337
340
  }
338
341
  optional_params = {
339
342
  p
340
343
  for p in func_params
341
- if sig.parameters[p].default is not inspect.Parameter.empty
342
- and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
343
- and p != context_kwarg
344
+ if user_sig.parameters[p].default is not inspect.Parameter.empty
345
+ and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
344
346
  }
345
347
 
346
348
  # Validate RFC 6570 query parameters
@@ -370,6 +372,15 @@ class FunctionResourceTemplate(ResourceTemplate):
370
372
 
371
373
  description = description or inspect.getdoc(fn)
372
374
 
375
+ # Normalize task to TaskConfig and validate
376
+ if task is None:
377
+ task_config = TaskConfig(mode="forbidden")
378
+ elif isinstance(task, bool):
379
+ task_config = TaskConfig.from_bool(task)
380
+ else:
381
+ task_config = task
382
+ task_config.validate_function(fn, func_name)
383
+
373
384
  # if the fn is a callable class, we need to get the __call__ method from here out
374
385
  if not inspect.isroutine(fn):
375
386
  fn = fn.__call__
@@ -377,15 +388,13 @@ class FunctionResourceTemplate(ResourceTemplate):
377
388
  if isinstance(fn, staticmethod):
378
389
  fn = fn.__func__
379
390
 
380
- type_adapter = get_cached_typeadapter(fn)
391
+ wrapper_fn = without_injected_parameters(fn)
392
+ type_adapter = get_cached_typeadapter(wrapper_fn)
381
393
  parameters = type_adapter.json_schema()
394
+ parameters = compress_schema(parameters, prune_titles=True)
382
395
 
383
- # compress the schema
384
- prune_params = [context_kwarg] if context_kwarg else None
385
- parameters = compress_schema(parameters, prune_params=prune_params)
386
-
387
- # ensure the arguments are properly cast
388
- fn = validate_call(fn)
396
+ # Use validate_call on wrapper for runtime type coercion
397
+ fn = validate_call(wrapper_fn)
389
398
 
390
399
  return cls(
391
400
  uri_template=uri_template,
@@ -400,4 +409,5 @@ class FunctionResourceTemplate(ResourceTemplate):
400
409
  enabled=enabled if enabled is not None else True,
401
410
  annotations=annotations,
402
411
  meta=meta,
412
+ task_config=task_config,
403
413
  )
@@ -23,12 +23,3 @@ __all__ = [
23
23
  "StaticTokenVerifier",
24
24
  "TokenVerifier",
25
25
  ]
26
-
27
-
28
- def __getattr__(name: str):
29
- # Defer import because it raises a deprecation warning
30
- if name == "BearerAuthProvider":
31
- from .providers.bearer import BearerAuthProvider
32
-
33
- return BearerAuthProvider
34
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")