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
@@ -4,7 +4,7 @@ This module implements protocol-level background task execution for MCP servers.
4
4
  """
5
5
 
6
6
  from fastmcp.server.tasks.capabilities import get_task_capabilities
7
- from fastmcp.server.tasks.config import TaskConfig, TaskMode
7
+ from fastmcp.server.tasks.config import TaskConfig, TaskMeta, TaskMode
8
8
  from fastmcp.server.tasks.keys import (
9
9
  build_task_key,
10
10
  get_client_task_id_from_key,
@@ -13,6 +13,7 @@ from fastmcp.server.tasks.keys import (
13
13
 
14
14
  __all__ = [
15
15
  "TaskConfig",
16
+ "TaskMeta",
16
17
  "TaskMode",
17
18
  "build_task_key",
18
19
  "get_client_task_id_from_key",
@@ -1,5 +1,7 @@
1
1
  """SEP-1686 task capabilities declaration."""
2
2
 
3
+ from importlib.util import find_spec
4
+
3
5
  from mcp.types import (
4
6
  ServerTasksCapability,
5
7
  ServerTasksRequestsCapability,
@@ -10,15 +12,25 @@ from mcp.types import (
10
12
  )
11
13
 
12
14
 
13
- def get_task_capabilities() -> ServerTasksCapability:
15
+ def _is_docket_available() -> bool:
16
+ """Check if pydocket is installed (local to avoid circular import)."""
17
+ return find_spec("docket") is not None
18
+
19
+
20
+ def get_task_capabilities() -> ServerTasksCapability | None:
14
21
  """Return the SEP-1686 task capabilities.
15
22
 
16
23
  Returns task capabilities as a first-class ServerCapabilities field,
17
24
  declaring support for list, cancel, and request operations per SEP-1686.
18
25
 
26
+ Returns None if pydocket is not installed (no task support).
27
+
19
28
  Note: prompts/resources are passed via extra_data since the SDK types
20
29
  don't include them yet (FastMCP supports them ahead of the spec).
21
30
  """
31
+ if not _is_docket_available():
32
+ return None
33
+
22
34
  return ServerTasksCapability(
23
35
  list=TasksListCapability(),
24
36
  cancel=TasksCancelCapability(),
@@ -7,13 +7,36 @@ handle task-augmented execution as specified in SEP-1686.
7
7
  from __future__ import annotations
8
8
 
9
9
  import inspect
10
+ import warnings
10
11
  from collections.abc import Callable
11
12
  from dataclasses import dataclass
13
+ from datetime import timedelta
12
14
  from typing import Any, Literal
13
15
 
14
16
  # Task execution modes per SEP-1686 / MCP ToolExecution.taskSupport
15
17
  TaskMode = Literal["forbidden", "optional", "required"]
16
18
 
19
+ # Default values for task metadata (single source of truth)
20
+ DEFAULT_POLL_INTERVAL = timedelta(seconds=5) # Default poll interval
21
+ DEFAULT_POLL_INTERVAL_MS = int(DEFAULT_POLL_INTERVAL.total_seconds() * 1000)
22
+ DEFAULT_TTL_MS = 60_000 # Default TTL in milliseconds
23
+
24
+
25
+ @dataclass
26
+ class TaskMeta:
27
+ """Metadata for task-augmented execution requests.
28
+
29
+ When passed to call_tool/read_resource/get_prompt, signals that
30
+ the operation should be submitted as a background task.
31
+
32
+ Attributes:
33
+ ttl: Client-requested TTL in milliseconds. If None, uses server default.
34
+ fn_key: Docket routing key. Auto-derived from component name if None.
35
+ """
36
+
37
+ ttl: int | None = None
38
+ fn_key: str | None = None
39
+
17
40
 
18
41
  @dataclass
19
42
  class TaskConfig:
@@ -28,6 +51,11 @@ class TaskConfig:
28
51
  - "required": Component requires task execution. Clients must request task
29
52
  augmentation; server returns -32601 if they don't.
30
53
 
54
+ Important:
55
+ Task-enabled components must be available at server startup to be
56
+ registered with all Docket workers. Components added dynamically after
57
+ startup will not be registered for background execution.
58
+
31
59
  Example:
32
60
  ```python
33
61
  from fastmcp import FastMCP
@@ -46,6 +74,7 @@ class TaskConfig:
46
74
  """
47
75
 
48
76
  mode: TaskMode = "optional"
77
+ poll_interval: timedelta = DEFAULT_POLL_INTERVAL
49
78
 
50
79
  @classmethod
51
80
  def from_bool(cls, value: bool) -> TaskConfig:
@@ -59,22 +88,41 @@ class TaskConfig:
59
88
  """
60
89
  return cls(mode="optional" if value else "forbidden")
61
90
 
91
+ def supports_tasks(self) -> bool:
92
+ """Check if this component supports task execution.
93
+
94
+ Returns:
95
+ True if mode is "optional" or "required", False if "forbidden".
96
+ """
97
+ return self.mode != "forbidden"
98
+
62
99
  def validate_function(self, fn: Callable[..., Any], name: str) -> None:
63
100
  """Validate that function is compatible with this task config.
64
101
 
65
- Task execution requires async functions. Raises ValueError if mode
66
- is "optional" or "required" but function is synchronous.
102
+ Task execution requires:
103
+ 1. fastmcp[tasks] to be installed (pydocket)
104
+ 2. Async functions
105
+
106
+ Raises ImportError if mode is "optional" or "required" but pydocket
107
+ is not installed. Raises ValueError if function is synchronous.
67
108
 
68
109
  Args:
69
110
  fn: The function to validate (handles callable classes and staticmethods).
70
111
  name: Name for error messages.
71
112
 
72
113
  Raises:
114
+ ImportError: If task execution is enabled but pydocket not installed.
73
115
  ValueError: If task execution is enabled but function is sync.
74
116
  """
75
- if self.mode == "forbidden":
117
+ if not self.supports_tasks():
76
118
  return
77
119
 
120
+ # Check that docket is available for task execution
121
+ # Lazy import to avoid circular: dependencies.py → http.py → tasks/__init__.py → config.py
122
+ from fastmcp.server.dependencies import require_docket
123
+
124
+ require_docket(f"`task=True` on function '{name}'")
125
+
78
126
  # Unwrap callable classes and staticmethods
79
127
  fn_to_check = fn
80
128
  if not inspect.isroutine(fn) and callable(fn):
@@ -87,3 +135,18 @@ class TaskConfig:
87
135
  f"'{name}' uses a sync function but has task execution enabled. "
88
136
  "Background tasks require async functions."
89
137
  )
138
+
139
+ # Warn if function uses Context - it won't be available in workers
140
+ from fastmcp.server.context import Context
141
+ from fastmcp.utilities.types import find_kwarg_by_type
142
+
143
+ context_kwarg = find_kwarg_by_type(fn_to_check, Context)
144
+ if context_kwarg:
145
+ warnings.warn(
146
+ f"'{name}' uses Context but has task execution enabled. "
147
+ "Context is not available in background task workers because "
148
+ "there is no active MCP session. Consider using Docket dependencies "
149
+ "like Progress() instead for worker-compatible functionality.",
150
+ UserWarning,
151
+ stacklevel=4,
152
+ )
@@ -8,55 +8,66 @@ from __future__ import annotations
8
8
  import uuid
9
9
  from contextlib import suppress
10
10
  from datetime import datetime, timezone
11
- from typing import TYPE_CHECKING, Any
11
+ from typing import TYPE_CHECKING, Any, Literal
12
12
 
13
13
  import mcp.types
14
14
  from mcp.shared.exceptions import McpError
15
15
  from mcp.types import INTERNAL_ERROR, ErrorData
16
16
 
17
17
  from fastmcp.server.dependencies import _current_docket, get_context
18
+ from fastmcp.server.tasks.config import TaskMeta
18
19
  from fastmcp.server.tasks.keys import build_task_key
19
20
 
20
21
  if TYPE_CHECKING:
21
- from fastmcp.server.server import FastMCP
22
+ from fastmcp.prompts.prompt import Prompt
23
+ from fastmcp.resources.resource import Resource
24
+ from fastmcp.resources.template import ResourceTemplate
25
+ from fastmcp.tools.tool import Tool
22
26
 
23
27
  # Redis mapping TTL buffer: Add 15 minutes to Docket's execution_ttl
24
28
  TASK_MAPPING_TTL_BUFFER_SECONDS = 15 * 60
25
29
 
26
30
 
27
- async def handle_tool_as_task(
28
- server: FastMCP,
29
- tool_name: str,
30
- arguments: dict[str, Any],
31
- task_meta: dict[str, Any],
32
- ) -> mcp.types.CallToolResult:
33
- """Handle tool execution as background task (SEP-1686).
31
+ async def submit_to_docket(
32
+ task_type: Literal["tool", "resource", "template", "prompt"],
33
+ key: str,
34
+ component: Tool | Resource | ResourceTemplate | Prompt,
35
+ arguments: dict[str, Any] | None = None,
36
+ task_meta: TaskMeta | None = None,
37
+ ) -> mcp.types.CreateTaskResult:
38
+ """Submit any component to Docket for background execution (SEP-1686).
34
39
 
35
- Queues the user's actual function to Docket (preserving signature for DI),
36
- stores raw return values, converts to MCP types on retrieval.
40
+ Unified handler for all component types. Called by component's internal
41
+ methods (_run, _read, _render) when task metadata is present and mode allows.
42
+
43
+ Queues the component's method to Docket, stores raw return values,
44
+ and converts to MCP types on retrieval.
37
45
 
38
46
  Args:
39
- server: FastMCP server instance
40
- tool_name: Name of the tool to execute
41
- arguments: Tool arguments
42
- task_meta: Task metadata from request (contains ttl)
47
+ task_type: Component type for task key construction
48
+ key: The component key as seen by MCP layer (with namespace prefix)
49
+ component: The component instance (Tool, Resource, ResourceTemplate, Prompt)
50
+ arguments: Arguments/params (None for Resource which has no args)
51
+ task_meta: Task execution metadata. If task_meta.ttl is provided, it
52
+ overrides the server default (docket.execution_ttl).
43
53
 
44
54
  Returns:
45
- CallToolResult: Task stub with task metadata in _meta
55
+ CreateTaskResult: Task stub with proper Task object
46
56
  """
47
57
  # Generate server-side task ID per SEP-1686 final spec (line 375-377)
48
58
  # Server MUST generate task IDs, clients no longer provide them
49
59
  server_task_id = str(uuid.uuid4())
50
60
 
51
61
  # Record creation timestamp per SEP-1686 final spec (line 430)
52
- # Format as ISO 8601 / RFC 3339 timestamp
53
- created_at = datetime.now(timezone.utc).isoformat()
62
+ created_at = datetime.now(timezone.utc)
54
63
 
55
- # Get session ID and Docket
64
+ # Get session ID - use "internal" for programmatic calls without MCP session
56
65
  ctx = get_context()
57
- session_id = ctx.session_id
66
+ try:
67
+ session_id = ctx.session_id
68
+ except RuntimeError:
69
+ session_id = "internal"
58
70
 
59
- # Get Docket from ContextVar (set by Context.__aenter__ at request time)
60
71
  docket = _current_docket.get()
61
72
  if docket is None:
62
73
  raise McpError(
@@ -67,22 +78,28 @@ async def handle_tool_as_task(
67
78
  )
68
79
 
69
80
  # Build full task key with embedded metadata
70
- task_key = build_task_key(session_id, server_task_id, "tool", tool_name)
81
+ task_key = build_task_key(session_id, server_task_id, task_type, key)
71
82
 
72
- # Get the tool to access user's function
73
- tool = await server.get_tool(tool_name)
83
+ # Determine TTL: use task_meta.ttl if provided, else docket default
84
+ if task_meta is not None and task_meta.ttl is not None:
85
+ ttl_ms = task_meta.ttl
86
+ else:
87
+ ttl_ms = int(docket.execution_ttl.total_seconds() * 1000)
88
+ ttl_seconds = int(ttl_ms / 1000) + TASK_MAPPING_TTL_BUFFER_SECONDS
74
89
 
75
- # Store task key mapping and creation timestamp in Redis for protocol handlers
90
+ # Store task metadata in Redis for protocol handlers
76
91
  task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
77
92
  created_at_key = docket.key(
78
93
  f"fastmcp:task:{session_id}:{server_task_id}:created_at"
79
94
  )
80
- ttl_seconds = int(
81
- docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
95
+ poll_interval_key = docket.key(
96
+ f"fastmcp:task:{session_id}:{server_task_id}:poll_interval"
82
97
  )
98
+ poll_interval_ms = int(component.task_config.poll_interval.total_seconds() * 1000)
83
99
  async with docket.redis() as redis:
84
100
  await redis.set(task_meta_key, task_key, ex=ttl_seconds)
85
- await redis.set(created_at_key, created_at, ex=ttl_seconds)
101
+ await redis.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)
102
+ await redis.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)
86
103
 
87
104
  # Send notifications/tasks/created per SEP-1686 (mandatory)
88
105
  # Send BEFORE queuing to avoid race where task completes before notification
@@ -90,257 +107,32 @@ async def handle_tool_as_task(
90
107
  jsonrpc="2.0",
91
108
  method="notifications/tasks/created",
92
109
  params={}, # Empty params per spec
93
- _meta={ # taskId in _meta per spec
110
+ _meta={ # type: ignore[call-arg] # _meta is Pydantic alias for meta field
94
111
  "modelcontextprotocol.io/related-task": {
95
112
  "taskId": server_task_id,
96
113
  }
97
114
  },
98
115
  )
99
-
100
- ctx = get_context()
101
116
  with suppress(Exception):
102
117
  # Don't let notification failures break task creation
103
118
  await ctx.session.send_notification(notification) # type: ignore[arg-type]
104
119
 
105
- # Queue function to Docket by name (result storage via execution_ttl)
106
- # Use tool.key which matches what was registered - prefixed for mounted tools
107
- await docket.add(
108
- tool.key,
109
- key=task_key,
110
- )(**arguments)
111
-
112
- # Spawn subscription task to send status notifications (SEP-1686 optional feature)
113
- from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
114
-
115
- # Start subscription in session's task group (persists for connection lifetime)
116
- if hasattr(ctx.session, "_subscription_task_group"):
117
- tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
118
- if tg:
119
- tg.start_soon( # type: ignore[union-attr]
120
- subscribe_to_task_updates,
121
- server_task_id,
122
- task_key,
123
- ctx.session,
124
- docket,
125
- )
126
-
127
- # Return task stub
128
- # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
129
- return mcp.types.CallToolResult(
130
- content=[],
131
- _meta={
132
- "modelcontextprotocol.io/task": {
133
- "taskId": server_task_id,
134
- "status": "working",
135
- }
136
- },
137
- )
138
-
139
-
140
- async def handle_prompt_as_task(
141
- server: FastMCP,
142
- prompt_name: str,
143
- arguments: dict[str, Any] | None,
144
- task_meta: dict[str, Any],
145
- ) -> mcp.types.GetPromptResult:
146
- """Handle prompt execution as background task (SEP-1686).
147
-
148
- Queues the user's actual function to Docket (preserving signature for DI).
149
-
150
- Args:
151
- server: FastMCP server instance
152
- prompt_name: Name of the prompt to execute
153
- arguments: Prompt arguments
154
- task_meta: Task metadata from request (contains ttl)
155
-
156
- Returns:
157
- GetPromptResult: Task stub with task metadata in _meta
158
- """
159
- # Generate server-side task ID per SEP-1686 final spec (line 375-377)
160
- # Server MUST generate task IDs, clients no longer provide them
161
- server_task_id = str(uuid.uuid4())
162
-
163
- # Record creation timestamp per SEP-1686 final spec (line 430)
164
- # Format as ISO 8601 / RFC 3339 timestamp
165
- created_at = datetime.now(timezone.utc).isoformat()
166
-
167
- # Get session ID and Docket
168
- ctx = get_context()
169
- session_id = ctx.session_id
170
-
171
- # Get Docket from ContextVar (set by Context.__aenter__ at request time)
172
- docket = _current_docket.get()
173
- if docket is None:
174
- raise McpError(
175
- ErrorData(
176
- code=INTERNAL_ERROR,
177
- message="Background tasks require a running FastMCP server context",
178
- )
179
- )
180
-
181
- # Build full task key with embedded metadata
182
- task_key = build_task_key(session_id, server_task_id, "prompt", prompt_name)
183
-
184
- # Get the prompt
185
- prompt = await server.get_prompt(prompt_name)
186
-
187
- # Store task key mapping and creation timestamp in Redis for protocol handlers
188
- task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
189
- created_at_key = docket.key(
190
- f"fastmcp:task:{session_id}:{server_task_id}:created_at"
191
- )
192
- ttl_seconds = int(
193
- docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
194
- )
195
- async with docket.redis() as redis:
196
- await redis.set(task_meta_key, task_key, ex=ttl_seconds)
197
- await redis.set(created_at_key, created_at, ex=ttl_seconds)
198
-
199
- # Send notifications/tasks/created per SEP-1686 (mandatory)
200
- # Send BEFORE queuing to avoid race where task completes before notification
201
- notification = mcp.types.JSONRPCNotification(
202
- jsonrpc="2.0",
203
- method="notifications/tasks/created",
204
- params={},
205
- _meta={
206
- "modelcontextprotocol.io/related-task": {
207
- "taskId": server_task_id,
208
- }
209
- },
210
- )
211
- with suppress(Exception):
212
- await ctx.session.send_notification(notification) # type: ignore[arg-type]
213
-
214
- # Queue function to Docket by name (result storage via execution_ttl)
215
- # Use prompt.key which matches what was registered - prefixed for mounted prompts
216
- await docket.add(
217
- prompt.key,
218
- key=task_key,
219
- )(**(arguments or {}))
220
-
221
- # Spawn subscription task to send status notifications (SEP-1686 optional feature)
222
- from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
223
-
224
- # Start subscription in session's task group (persists for connection lifetime)
225
- if hasattr(ctx.session, "_subscription_task_group"):
226
- tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
227
- if tg:
228
- tg.start_soon( # type: ignore[union-attr]
229
- subscribe_to_task_updates,
230
- server_task_id,
231
- task_key,
232
- ctx.session,
233
- docket,
234
- )
235
-
236
- # Return task stub
237
- # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
238
- return mcp.types.GetPromptResult(
239
- description="",
240
- messages=[],
241
- _meta={
242
- "modelcontextprotocol.io/task": {
243
- "taskId": server_task_id,
244
- "status": "working",
245
- }
246
- },
247
- )
248
-
249
-
250
- async def handle_resource_as_task(
251
- server: FastMCP,
252
- uri: str,
253
- resource, # Resource or ResourceTemplate
254
- task_meta: dict[str, Any],
255
- ) -> mcp.types.ServerResult:
256
- """Handle resource read as background task (SEP-1686).
257
-
258
- Queues the user's actual function to Docket.
259
-
260
- Args:
261
- server: FastMCP server instance
262
- uri: Resource URI
263
- resource: Resource or ResourceTemplate object
264
- task_meta: Task metadata from request (contains ttl)
265
-
266
- Returns:
267
- ServerResult with ReadResourceResult stub
268
- """
269
- # Generate server-side task ID per SEP-1686 final spec (line 375-377)
270
- # Server MUST generate task IDs, clients no longer provide them
271
- server_task_id = str(uuid.uuid4())
272
-
273
- # Record creation timestamp per SEP-1686 final spec (line 430)
274
- # Format as ISO 8601 / RFC 3339 timestamp
275
- created_at = datetime.now(timezone.utc).isoformat()
276
-
277
- # Get session ID and Docket
278
- ctx = get_context()
279
- session_id = ctx.session_id
280
-
281
- # Get Docket from ContextVar (set by Context.__aenter__ at request time)
282
- docket = _current_docket.get()
283
- if docket is None:
284
- raise McpError(
285
- ErrorData(
286
- code=INTERNAL_ERROR,
287
- message="Background tasks require a running FastMCP server context",
288
- )
289
- )
290
-
291
- # Build full task key with embedded metadata (use original URI)
292
- task_key = build_task_key(session_id, server_task_id, "resource", str(uri))
293
-
294
- # Store task key mapping and creation timestamp in Redis for protocol handlers
295
- task_meta_key = docket.key(f"fastmcp:task:{session_id}:{server_task_id}")
296
- created_at_key = docket.key(
297
- f"fastmcp:task:{session_id}:{server_task_id}:created_at"
298
- )
299
- ttl_seconds = int(
300
- docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
301
- )
302
- async with docket.redis() as redis:
303
- await redis.set(task_meta_key, task_key, ex=ttl_seconds)
304
- await redis.set(created_at_key, created_at, ex=ttl_seconds)
305
-
306
- # Send notifications/tasks/created per SEP-1686 (mandatory)
307
- # Send BEFORE queuing to avoid race where task completes before notification
308
- notification = mcp.types.JSONRPCNotification(
309
- jsonrpc="2.0",
310
- method="notifications/tasks/created",
311
- params={},
312
- _meta={
313
- "modelcontextprotocol.io/related-task": {
314
- "taskId": server_task_id,
315
- }
316
- },
317
- )
318
- with suppress(Exception):
319
- await ctx.session.send_notification(notification) # type: ignore[arg-type]
320
-
321
- # Queue function to Docket by name (result storage via execution_ttl)
322
- # Use resource.name which matches what was registered - prefixed for mounted resources
323
- # For templates, extract URI params and pass them to the function
324
- from fastmcp.resources.template import FunctionResourceTemplate, match_uri_template
325
-
326
- if isinstance(resource, FunctionResourceTemplate):
327
- params = match_uri_template(uri, resource.uri_template) or {}
328
- await docket.add(
329
- resource.name,
330
- key=task_key,
331
- )(**params)
120
+ # Queue function to Docket by key (result storage via execution_ttl)
121
+ # Use component.add_to_docket() which handles calling conventions
122
+ # `fn_key` is the function lookup key (e.g., "child_multiply")
123
+ # `task_key` is the task result key (e.g., "fastmcp:task:{session}:{task_id}:tool:child_multiply")
124
+ # Resources don't take arguments; tools/prompts/templates always pass arguments (even if None/empty)
125
+ if task_type == "resource":
126
+ await component.add_to_docket(docket, fn_key=key, task_key=task_key) # type: ignore[call-arg]
332
127
  else:
333
- await docket.add(
334
- resource.name,
335
- key=task_key,
336
- )()
128
+ await component.add_to_docket(docket, arguments, fn_key=key, task_key=task_key) # type: ignore[call-arg]
337
129
 
338
130
  # Spawn subscription task to send status notifications (SEP-1686 optional feature)
339
131
  from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
340
132
 
341
133
  # Start subscription in session's task group (persists for connection lifetime)
342
134
  if hasattr(ctx.session, "_subscription_task_group"):
343
- tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
135
+ tg = ctx.session._subscription_task_group
344
136
  if tg:
345
137
  tg.start_soon( # type: ignore[union-attr]
346
138
  subscribe_to_task_updates,
@@ -348,18 +140,18 @@ async def handle_resource_as_task(
348
140
  task_key,
349
141
  ctx.session,
350
142
  docket,
143
+ poll_interval_ms,
351
144
  )
352
145
 
353
- # Return task stub
146
+ # Return CreateTaskResult with proper Task object
354
147
  # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
355
- return mcp.types.ServerResult(
356
- mcp.types.ReadResourceResult(
357
- contents=[],
358
- _meta={
359
- "modelcontextprotocol.io/task": {
360
- "taskId": server_task_id,
361
- "status": "working",
362
- }
363
- },
148
+ return mcp.types.CreateTaskResult(
149
+ task=mcp.types.Task(
150
+ taskId=server_task_id,
151
+ status="working",
152
+ createdAt=created_at,
153
+ lastUpdatedAt=created_at,
154
+ ttl=ttl_ms,
155
+ pollInterval=poll_interval_ms,
364
156
  )
365
157
  )
@@ -1,7 +1,7 @@
1
1
  """Task key management for SEP-1686 background tasks.
2
2
 
3
3
  Task keys encode security scoping and metadata in the Docket key format:
4
- {session_id}:{client_task_id}:{task_type}:{component_identifier}
4
+ `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
5
5
 
6
6
  This format provides:
7
7
  - Session-based security scoping (prevents cross-session access)
@@ -20,7 +20,7 @@ def build_task_key(
20
20
  ) -> str:
21
21
  """Build Docket task key with embedded metadata.
22
22
 
23
- Format: {session_id}:{client_task_id}:{task_type}:{component_identifier}
23
+ Format: `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
24
24
 
25
25
  The component_identifier is URI-encoded to handle special characters (colons, slashes, etc.).
26
26
 
@@ -55,12 +55,10 @@ def parse_task_key(task_key: str) -> dict[str, str]:
55
55
 
56
56
  Examples:
57
57
  >>> parse_task_key("session123:task456:tool:my_tool")
58
- {'session_id': 'session123', 'client_task_id': 'task456',
59
- 'task_type': 'tool', 'component_identifier': 'my_tool'}
58
+ `{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`
60
59
 
61
60
  >>> parse_task_key("session123:task456:resource:file%3A%2F%2Fdata.txt")
62
- {'session_id': 'session123', 'client_task_id': 'task456',
63
- 'task_type': 'resource', 'component_identifier': 'file://data.txt'}
61
+ `{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'resource', 'component_identifier': 'file://data.txt'}`
64
62
  """
65
63
  parts = task_key.split(":", 3)
66
64
  if len(parts) != 4: