fastmcp 2.13.2__py3-none-any.whl → 2.14.0__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 (74) 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 +665 -129
  9. fastmcp/client/elicitation.py +11 -5
  10. fastmcp/client/messages.py +7 -5
  11. fastmcp/client/roots.py +2 -1
  12. fastmcp/client/tasks.py +614 -0
  13. fastmcp/client/transports.py +37 -5
  14. fastmcp/contrib/component_manager/component_service.py +4 -20
  15. fastmcp/dependencies.py +25 -0
  16. fastmcp/experimental/sampling/handlers/openai.py +1 -1
  17. fastmcp/experimental/server/openapi/__init__.py +15 -13
  18. fastmcp/experimental/utilities/openapi/__init__.py +12 -38
  19. fastmcp/prompts/prompt.py +33 -33
  20. fastmcp/resources/resource.py +29 -12
  21. fastmcp/resources/template.py +64 -54
  22. fastmcp/server/auth/__init__.py +0 -9
  23. fastmcp/server/auth/auth.py +127 -3
  24. fastmcp/server/auth/oauth_proxy.py +47 -97
  25. fastmcp/server/auth/oidc_proxy.py +7 -0
  26. fastmcp/server/auth/providers/in_memory.py +2 -2
  27. fastmcp/server/auth/providers/oci.py +2 -2
  28. fastmcp/server/context.py +66 -72
  29. fastmcp/server/dependencies.py +464 -6
  30. fastmcp/server/elicitation.py +285 -47
  31. fastmcp/server/event_store.py +177 -0
  32. fastmcp/server/http.py +15 -3
  33. fastmcp/server/low_level.py +56 -12
  34. fastmcp/server/middleware/middleware.py +2 -2
  35. fastmcp/server/openapi/__init__.py +35 -0
  36. fastmcp/{experimental/server → server}/openapi/components.py +4 -3
  37. fastmcp/{experimental/server → server}/openapi/routing.py +1 -1
  38. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  39. fastmcp/server/proxy.py +50 -37
  40. fastmcp/server/server.py +731 -532
  41. fastmcp/server/tasks/__init__.py +21 -0
  42. fastmcp/server/tasks/capabilities.py +22 -0
  43. fastmcp/server/tasks/config.py +89 -0
  44. fastmcp/server/tasks/converters.py +205 -0
  45. fastmcp/server/tasks/handlers.py +356 -0
  46. fastmcp/server/tasks/keys.py +93 -0
  47. fastmcp/server/tasks/protocol.py +355 -0
  48. fastmcp/server/tasks/subscriptions.py +205 -0
  49. fastmcp/settings.py +101 -103
  50. fastmcp/tools/tool.py +80 -44
  51. fastmcp/tools/tool_transform.py +1 -12
  52. fastmcp/utilities/components.py +3 -3
  53. fastmcp/utilities/json_schema_type.py +4 -4
  54. fastmcp/utilities/mcp_config.py +1 -2
  55. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +1 -1
  56. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  57. fastmcp/utilities/openapi/__init__.py +63 -0
  58. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  59. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +1 -1
  60. fastmcp/utilities/tests.py +11 -5
  61. fastmcp/utilities/types.py +8 -0
  62. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/METADATA +5 -4
  63. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/RECORD +71 -59
  64. fastmcp/server/auth/providers/bearer.py +0 -25
  65. fastmcp/server/openapi.py +0 -1087
  66. fastmcp/utilities/openapi.py +0 -1568
  67. /fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  68. /fastmcp/{experimental/utilities → utilities}/openapi/director.py +0 -0
  69. /fastmcp/{experimental/utilities → utilities}/openapi/models.py +0 -0
  70. /fastmcp/{experimental/utilities → utilities}/openapi/parser.py +0 -0
  71. /fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +0 -0
  72. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +0 -0
  73. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  74. {fastmcp-2.13.2.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ import os
6
6
  import shutil
7
7
  import sys
8
8
  import warnings
9
- from collections.abc import AsyncIterator
9
+ from collections.abc import AsyncIterator, Callable
10
10
  from pathlib import Path
11
11
  from typing import Any, Literal, TextIO, TypeVar, cast, overload
12
12
 
@@ -36,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
36
36
  from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
37
37
  from fastmcp.server.dependencies import get_http_headers
38
38
  from fastmcp.server.server import FastMCP
39
+ from fastmcp.server.tasks.capabilities import get_task_capabilities
39
40
  from fastmcp.utilities.logging import get_logger
40
41
  from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
41
42
 
@@ -254,6 +255,8 @@ class StreamableHttpTransport(ClientTransport):
254
255
  sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
255
256
  self.sse_read_timeout = sse_read_timeout
256
257
 
258
+ self._get_session_id_cb: Callable[[], str | None] | None = None
259
+
257
260
  def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
258
261
  if auth == "oauth":
259
262
  auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
@@ -287,12 +290,25 @@ class StreamableHttpTransport(ClientTransport):
287
290
  auth=self.auth,
288
291
  **client_kwargs,
289
292
  ) as transport:
290
- read_stream, write_stream, _ = transport
293
+ read_stream, write_stream, get_session_id = transport
294
+ self._get_session_id_cb = get_session_id
291
295
  async with ClientSession(
292
296
  read_stream, write_stream, **session_kwargs
293
297
  ) as session:
294
298
  yield session
295
299
 
300
+ def get_session_id(self) -> str | None:
301
+ if self._get_session_id_cb:
302
+ try:
303
+ return self._get_session_id_cb()
304
+ except Exception:
305
+ return None
306
+ return None
307
+
308
+ async def close(self):
309
+ # Reset the session id callback
310
+ self._get_session_id_cb = None
311
+
296
312
  def __repr__(self) -> str:
297
313
  return f"<StreamableHttpTransport(url='{self.url}')>"
298
314
 
@@ -375,7 +391,8 @@ class StdioTransport(ClientTransport):
375
391
  env=self.env,
376
392
  cwd=self.cwd,
377
393
  log_file=self.log_file,
378
- session_kwargs=session_kwargs,
394
+ # TODO(ty): remove when ty supports Unpack[TypedDict] inference
395
+ session_kwargs=session_kwargs, # type: ignore[arg-type]
379
396
  ready_event=self._ready_event,
380
397
  stop_event=self._stop_event,
381
398
  session_future=session_future,
@@ -849,16 +866,25 @@ class FastMCPTransport(ClientTransport):
849
866
  client_read, client_write = client_streams
850
867
  server_read, server_write = server_streams
851
868
 
852
- # Create a cancel scope for the server task
869
+ # Capture exceptions to re-raise after task group cleanup.
870
+ # anyio task groups can suppress exceptions when cancel_scope.cancel()
871
+ # is called during cleanup, so we capture and re-raise manually.
872
+ exception_to_raise: BaseException | None = None
873
+
853
874
  async with (
854
875
  anyio.create_task_group() as tg,
855
876
  _enter_server_lifespan(server=self.server),
856
877
  ):
878
+ # Build experimental capabilities
879
+ experimental_capabilities = get_task_capabilities()
880
+
857
881
  tg.start_soon(
858
882
  lambda: self.server._mcp_server.run(
859
883
  server_read,
860
884
  server_write,
861
- self.server._mcp_server.create_initialization_options(),
885
+ self.server._mcp_server.create_initialization_options(
886
+ experimental_capabilities=experimental_capabilities
887
+ ),
862
888
  raise_exceptions=self.raise_exceptions,
863
889
  )
864
890
  )
@@ -870,9 +896,15 @@ class FastMCPTransport(ClientTransport):
870
896
  **session_kwargs,
871
897
  ) as client_session:
872
898
  yield client_session
899
+ except BaseException as e:
900
+ exception_to_raise = e
873
901
  finally:
874
902
  tg.cancel_scope.cancel()
875
903
 
904
+ # Re-raise after task group has exited cleanly
905
+ if exception_to_raise is not None:
906
+ raise exception_to_raise
907
+
876
908
  def __repr__(self) -> str:
877
909
  return f"<FastMCPTransport(server='{self.server.name}')>"
878
910
 
@@ -105,16 +105,8 @@ class ComponentService:
105
105
  # 2. Check mounted servers using the filtered protocol path.
106
106
  for mounted in reversed(self._server._mounted_servers):
107
107
  if mounted.prefix:
108
- if has_resource_prefix(
109
- key,
110
- mounted.prefix,
111
- mounted.resource_prefix_format,
112
- ):
113
- key = remove_resource_prefix(
114
- key,
115
- mounted.prefix,
116
- mounted.resource_prefix_format,
117
- )
108
+ if has_resource_prefix(key, mounted.prefix):
109
+ key = remove_resource_prefix(key, mounted.prefix)
118
110
  mounted_service = ComponentService(mounted.server)
119
111
  mounted_resource: (
120
112
  Resource | ResourceTemplate
@@ -148,16 +140,8 @@ class ComponentService:
148
140
  # 2. Check mounted servers using the filtered protocol path.
149
141
  for mounted in reversed(self._server._mounted_servers):
150
142
  if mounted.prefix:
151
- if has_resource_prefix(
152
- key,
153
- mounted.prefix,
154
- mounted.resource_prefix_format,
155
- ):
156
- key = remove_resource_prefix(
157
- key,
158
- mounted.prefix,
159
- mounted.resource_prefix_format,
160
- )
143
+ if has_resource_prefix(key, mounted.prefix):
144
+ key = remove_resource_prefix(key, mounted.prefix)
161
145
  mounted_service = ComponentService(mounted.server)
162
146
  mounted_resource: (
163
147
  Resource | ResourceTemplate
@@ -0,0 +1,25 @@
1
+ """Dependency injection exports for FastMCP.
2
+
3
+ This module re-exports dependency injection symbols from Docket and FastMCP
4
+ to provide a clean, centralized import location for all dependency-related
5
+ functionality.
6
+ """
7
+
8
+ from docket import Depends
9
+
10
+ from fastmcp.server.dependencies import (
11
+ CurrentContext,
12
+ CurrentDocket,
13
+ CurrentFastMCP,
14
+ CurrentWorker,
15
+ Progress,
16
+ )
17
+
18
+ __all__ = [
19
+ "CurrentContext",
20
+ "CurrentDocket",
21
+ "CurrentFastMCP",
22
+ "CurrentWorker",
23
+ "Depends",
24
+ "Progress",
25
+ ]
@@ -164,7 +164,7 @@ class OpenAISamplingHandler(BaseLLMSamplingHandler):
164
164
  ) -> ChatModel:
165
165
  for model_option in self._iter_models_from_preferences(model_preferences):
166
166
  if model_option in get_args(ChatModel):
167
- chosen_model: ChatModel = model_option # pyright: ignore[reportAssignmentType]
167
+ chosen_model: ChatModel = model_option # type: ignore[assignment]
168
168
  return chosen_model
169
169
 
170
170
  return self.default_model
@@ -1,26 +1,28 @@
1
- """OpenAPI server implementation for FastMCP - refactored for better maintainability."""
1
+ """Deprecated: Import from fastmcp.server.openapi instead."""
2
2
 
3
- # Import from server
4
- from .server import FastMCPOpenAPI
3
+ import warnings
5
4
 
6
- # Import from routing
7
- from .routing import (
5
+ from fastmcp.server.openapi import (
6
+ ComponentFn,
7
+ DEFAULT_ROUTE_MAPPINGS,
8
+ FastMCPOpenAPI,
8
9
  MCPType,
10
+ OpenAPIResource,
11
+ OpenAPIResourceTemplate,
12
+ OpenAPITool,
9
13
  RouteMap,
10
14
  RouteMapFn,
11
- ComponentFn,
12
- DEFAULT_ROUTE_MAPPINGS,
13
15
  _determine_route_type,
14
16
  )
15
17
 
16
- # Import from components
17
- from .components import (
18
- OpenAPITool,
19
- OpenAPIResource,
20
- OpenAPIResourceTemplate,
18
+ # Deprecated in 2.14 when OpenAPI support was promoted out of experimental
19
+ warnings.warn(
20
+ "Importing from fastmcp.experimental.server.openapi is deprecated. "
21
+ "Import from fastmcp.server.openapi instead.",
22
+ DeprecationWarning,
23
+ stacklevel=2,
21
24
  )
22
25
 
23
- # Export public symbols - maintaining backward compatibility
24
26
  __all__ = [
25
27
  "DEFAULT_ROUTE_MAPPINGS",
26
28
  "ComponentFn",
@@ -1,63 +1,37 @@
1
- """OpenAPI utilities for FastMCP - refactored for better maintainability."""
1
+ """Deprecated: Import from fastmcp.utilities.openapi instead."""
2
2
 
3
- # Import from models
4
- from .models import (
3
+ import warnings
4
+
5
+ from fastmcp.utilities.openapi import (
5
6
  HTTPRoute,
6
7
  HttpMethod,
7
- JsonSchema,
8
8
  ParameterInfo,
9
9
  ParameterLocation,
10
10
  RequestBodyInfo,
11
11
  ResponseInfo,
12
- )
13
-
14
- # Import from parser
15
- from .parser import parse_openapi_to_http_routes
16
-
17
- # Import from formatters
18
- from .formatters import (
19
- format_array_parameter,
20
- format_deep_object_parameter,
21
- format_description_with_responses,
22
- format_json_for_description,
12
+ extract_output_schema_from_responses,
23
13
  format_simple_description,
24
- generate_example_from_schema,
25
- )
26
-
27
- # Import from schemas
28
- from .schemas import (
14
+ parse_openapi_to_http_routes,
29
15
  _combine_schemas,
30
- extract_output_schema_from_responses,
31
- clean_schema_for_display,
32
- _make_optional_parameter_nullable,
33
16
  )
34
17
 
35
- # Import from json_schema_converter
36
- from .json_schema_converter import (
37
- convert_openapi_schema_to_json_schema,
38
- convert_schema_definitions,
18
+ # Deprecated in 2.14 when OpenAPI support was promoted out of experimental
19
+ warnings.warn(
20
+ "Importing from fastmcp.experimental.utilities.openapi is deprecated. "
21
+ "Import from fastmcp.utilities.openapi instead.",
22
+ DeprecationWarning,
23
+ stacklevel=2,
39
24
  )
40
25
 
41
- # Export public symbols - maintaining backward compatibility
42
26
  __all__ = [
43
27
  "HTTPRoute",
44
28
  "HttpMethod",
45
- "JsonSchema",
46
29
  "ParameterInfo",
47
30
  "ParameterLocation",
48
31
  "RequestBodyInfo",
49
32
  "ResponseInfo",
50
33
  "_combine_schemas",
51
- "_make_optional_parameter_nullable",
52
- "clean_schema_for_display",
53
- "convert_openapi_schema_to_json_schema",
54
- "convert_schema_definitions",
55
34
  "extract_output_schema_from_responses",
56
- "format_array_parameter",
57
- "format_deep_object_parameter",
58
- "format_description_with_responses",
59
- "format_json_for_description",
60
35
  "format_simple_description",
61
- "generate_example_from_schema",
62
36
  "parse_openapi_to_http_routes",
63
37
  ]
fastmcp/prompts/prompt.py CHANGED
@@ -5,7 +5,7 @@ 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
@@ -14,13 +14,13 @@ from mcp.types import PromptArgument as MCPPromptArgument
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
 
@@ -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
@@ -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")
@@ -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