fastmcp 2.12.5__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 (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,21 @@
1
+ """MCP SEP-1686 background tasks support.
2
+
3
+ This module implements protocol-level background task execution for MCP servers.
4
+ """
5
+
6
+ from fastmcp.server.tasks.capabilities import get_task_capabilities
7
+ from fastmcp.server.tasks.config import TaskConfig, TaskMode
8
+ from fastmcp.server.tasks.keys import (
9
+ build_task_key,
10
+ get_client_task_id_from_key,
11
+ parse_task_key,
12
+ )
13
+
14
+ __all__ = [
15
+ "TaskConfig",
16
+ "TaskMode",
17
+ "build_task_key",
18
+ "get_client_task_id_from_key",
19
+ "get_task_capabilities",
20
+ "parse_task_key",
21
+ ]
@@ -0,0 +1,22 @@
1
+ """SEP-1686 task capabilities declaration."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def get_task_capabilities() -> dict[str, Any]:
7
+ """Return the SEP-1686 task capabilities structure.
8
+
9
+ This is the standard capabilities map advertised to clients,
10
+ declaring support for list, cancel, and request operations.
11
+ """
12
+ return {
13
+ "tasks": {
14
+ "list": {},
15
+ "cancel": {},
16
+ "requests": {
17
+ "tools": {"call": {}},
18
+ "prompts": {"get": {}},
19
+ "resources": {"read": {}},
20
+ },
21
+ }
22
+ }
@@ -0,0 +1,89 @@
1
+ """TaskConfig for MCP SEP-1686 background task execution modes.
2
+
3
+ This module defines the configuration for how tools, resources, and prompts
4
+ handle task-augmented execution as specified in SEP-1686.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass
12
+ from typing import Any, Literal
13
+
14
+ # Task execution modes per SEP-1686 / MCP ToolExecution.taskSupport
15
+ TaskMode = Literal["forbidden", "optional", "required"]
16
+
17
+
18
+ @dataclass
19
+ class TaskConfig:
20
+ """Configuration for MCP background task execution (SEP-1686).
21
+
22
+ Controls how a component handles task-augmented requests:
23
+
24
+ - "forbidden": Component does not support task execution. Clients must not
25
+ request task augmentation; server returns -32601 if they do.
26
+ - "optional": Component supports both synchronous and task execution.
27
+ Client may request task augmentation or call normally.
28
+ - "required": Component requires task execution. Clients must request task
29
+ augmentation; server returns -32601 if they don't.
30
+
31
+ Example:
32
+ ```python
33
+ from fastmcp import FastMCP
34
+ from fastmcp.server.tasks import TaskConfig
35
+
36
+ mcp = FastMCP("MyServer")
37
+
38
+ # Background execution required
39
+ @mcp.tool(task=TaskConfig(mode="required"))
40
+ async def long_running_task(): ...
41
+
42
+ # Supports both modes (default when task=True)
43
+ @mcp.tool(task=TaskConfig(mode="optional"))
44
+ async def flexible_task(): ...
45
+ ```
46
+ """
47
+
48
+ mode: TaskMode = "optional"
49
+
50
+ @classmethod
51
+ def from_bool(cls, value: bool) -> TaskConfig:
52
+ """Convert boolean task flag to TaskConfig.
53
+
54
+ Args:
55
+ value: True for "optional" mode, False for "forbidden" mode.
56
+
57
+ Returns:
58
+ TaskConfig with appropriate mode.
59
+ """
60
+ return cls(mode="optional" if value else "forbidden")
61
+
62
+ def validate_function(self, fn: Callable[..., Any], name: str) -> None:
63
+ """Validate that function is compatible with this task config.
64
+
65
+ Task execution requires async functions. Raises ValueError if mode
66
+ is "optional" or "required" but function is synchronous.
67
+
68
+ Args:
69
+ fn: The function to validate (handles callable classes and staticmethods).
70
+ name: Name for error messages.
71
+
72
+ Raises:
73
+ ValueError: If task execution is enabled but function is sync.
74
+ """
75
+ if self.mode == "forbidden":
76
+ return
77
+
78
+ # Unwrap callable classes and staticmethods
79
+ fn_to_check = fn
80
+ if not inspect.isroutine(fn) and callable(fn):
81
+ fn_to_check = fn.__call__
82
+ if isinstance(fn_to_check, staticmethod):
83
+ fn_to_check = fn_to_check.__func__
84
+
85
+ if not inspect.iscoroutinefunction(fn_to_check):
86
+ raise ValueError(
87
+ f"'{name}' uses a sync function but has task execution enabled. "
88
+ "Background tasks require async functions."
89
+ )
@@ -0,0 +1,205 @@
1
+ """SEP-1686 task result converters.
2
+
3
+ Converts raw task return values to MCP result types.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ import json
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import mcp.types
13
+ import pydantic_core
14
+
15
+ if TYPE_CHECKING:
16
+ from fastmcp.server.server import FastMCP
17
+
18
+
19
+ async def convert_tool_result(
20
+ server: FastMCP, raw_value: Any, tool_name: str, client_task_id: str
21
+ ) -> mcp.types.CallToolResult:
22
+ """Convert raw tool return value to MCP CallToolResult.
23
+
24
+ Replicates the serialization logic from tool.run() to properly handle
25
+ output_schema, structured content, etc.
26
+
27
+ Args:
28
+ server: FastMCP server instance
29
+ raw_value: The raw return value from user's tool function
30
+ tool_name: Name of the tool (to get output_schema and serializer)
31
+ client_task_id: Client task ID for related-task metadata
32
+
33
+ Returns:
34
+ CallToolResult with properly formatted content and structured content
35
+ """
36
+ # Import here to avoid circular import:
37
+ # tools/tool.py -> tasks/config.py -> tasks/__init__.py -> converters.py -> tools/tool.py
38
+ from fastmcp.tools.tool import ToolResult, _convert_to_content
39
+
40
+ # Get the tool to access its configuration
41
+ tool = await server.get_tool(tool_name)
42
+
43
+ # Build related-task metadata
44
+ related_task_meta = {
45
+ "modelcontextprotocol.io/related-task": {
46
+ "taskId": client_task_id,
47
+ }
48
+ }
49
+
50
+ # If raw value is already ToolResult, use it directly
51
+ if isinstance(raw_value, ToolResult):
52
+ mcp_result = raw_value.to_mcp_result()
53
+ if isinstance(mcp_result, mcp.types.CallToolResult):
54
+ # Add metadata
55
+ mcp_result._meta = related_task_meta
56
+ return mcp_result
57
+ elif isinstance(mcp_result, tuple):
58
+ content, structured_content = mcp_result
59
+ return mcp.types.CallToolResult(
60
+ content=content,
61
+ structuredContent=structured_content,
62
+ _meta=related_task_meta,
63
+ )
64
+ else:
65
+ return mcp.types.CallToolResult(content=mcp_result, _meta=related_task_meta)
66
+
67
+ # Convert raw value to content blocks
68
+ unstructured_result = _convert_to_content(raw_value, serializer=tool.serializer)
69
+
70
+ # Handle structured content creation (same logic as tool.run())
71
+ structured_content = None
72
+
73
+ if tool.output_schema is None:
74
+ # Try to serialize as dict for structured content
75
+ try:
76
+ sc = pydantic_core.to_jsonable_python(raw_value)
77
+ if isinstance(sc, dict):
78
+ structured_content = sc
79
+ except pydantic_core.PydanticSerializationError:
80
+ pass
81
+ else:
82
+ # Has output_schema - convert to JSON-able types
83
+ jsonable_value = pydantic_core.to_jsonable_python(raw_value)
84
+ wrap_result = tool.output_schema.get("x-fastmcp-wrap-result")
85
+ structured_content = (
86
+ {"result": jsonable_value} if wrap_result else jsonable_value
87
+ )
88
+
89
+ return mcp.types.CallToolResult(
90
+ content=unstructured_result,
91
+ structuredContent=structured_content,
92
+ _meta=related_task_meta,
93
+ )
94
+
95
+
96
+ async def convert_prompt_result(
97
+ server: FastMCP, raw_value: Any, prompt_name: str, client_task_id: str
98
+ ) -> mcp.types.GetPromptResult:
99
+ """Convert raw prompt return value to MCP GetPromptResult.
100
+
101
+ The user function returns raw values (strings, dicts, lists) that need
102
+ to be converted to PromptMessage objects.
103
+
104
+ Args:
105
+ server: FastMCP server instance
106
+ raw_value: The raw return value from user's prompt function
107
+ prompt_name: Name of the prompt
108
+ client_task_id: Client task ID for related-task metadata
109
+
110
+ Returns:
111
+ GetPromptResult with properly formatted messages
112
+ """
113
+ from fastmcp.prompts.prompt import PromptMessage
114
+
115
+ # Get the prompt for metadata
116
+ prompt = await server.get_prompt(prompt_name)
117
+
118
+ # Normalize to list
119
+ if not isinstance(raw_value, list | tuple):
120
+ raw_value = [raw_value]
121
+
122
+ # Convert to PromptMessages
123
+ messages: list[mcp.types.PromptMessage] = []
124
+ for msg in raw_value:
125
+ if isinstance(msg, PromptMessage):
126
+ messages.append(msg.to_mcp())
127
+ elif isinstance(msg, str):
128
+ messages.append(
129
+ mcp.types.PromptMessage(
130
+ role="user",
131
+ content=mcp.types.TextContent(type="text", text=msg),
132
+ )
133
+ )
134
+ elif isinstance(msg, dict):
135
+ messages.append(mcp.types.PromptMessage.model_validate(msg))
136
+ else:
137
+ raise ValueError(f"Invalid message type: {type(msg)}")
138
+
139
+ return mcp.types.GetPromptResult(
140
+ description=prompt.description or "",
141
+ messages=messages,
142
+ _meta={
143
+ "modelcontextprotocol.io/related-task": {
144
+ "taskId": client_task_id,
145
+ }
146
+ },
147
+ )
148
+
149
+
150
+ async def convert_resource_result(
151
+ server: FastMCP, raw_value: Any, uri: str, client_task_id: str
152
+ ) -> dict[str, Any]:
153
+ """Convert raw resource return value to MCP resource contents dict.
154
+
155
+ Args:
156
+ server: FastMCP server instance
157
+ raw_value: The raw return value from user's resource function (str or bytes)
158
+ uri: Resource URI (for the contents response)
159
+ client_task_id: Client task ID for related-task metadata
160
+
161
+ Returns:
162
+ Dict with 'contents' key containing list of resource contents
163
+ """
164
+ # Build related-task metadata
165
+ related_task_meta = {
166
+ "modelcontextprotocol.io/related-task": {
167
+ "taskId": client_task_id,
168
+ }
169
+ }
170
+
171
+ # Resources return str or bytes directly
172
+ if isinstance(raw_value, str):
173
+ return {
174
+ "contents": [
175
+ {
176
+ "uri": uri,
177
+ "text": raw_value,
178
+ "mimeType": "text/plain",
179
+ }
180
+ ],
181
+ "_meta": related_task_meta,
182
+ }
183
+ elif isinstance(raw_value, bytes):
184
+ return {
185
+ "contents": [
186
+ {
187
+ "uri": uri,
188
+ "blob": base64.b64encode(raw_value).decode(),
189
+ "mimeType": "application/octet-stream",
190
+ }
191
+ ],
192
+ "_meta": related_task_meta,
193
+ }
194
+ else:
195
+ # Fallback: convert to JSON string
196
+ return {
197
+ "contents": [
198
+ {
199
+ "uri": uri,
200
+ "text": json.dumps(raw_value),
201
+ "mimeType": "application/json",
202
+ }
203
+ ],
204
+ "_meta": related_task_meta,
205
+ }
@@ -0,0 +1,356 @@
1
+ """SEP-1686 task execution handlers.
2
+
3
+ Handles queuing tool/prompt/resource executions to Docket as background tasks.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import uuid
9
+ from contextlib import suppress
10
+ from datetime import datetime, timezone
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ import mcp.types
14
+ from mcp.shared.exceptions import McpError
15
+ from mcp.types import INTERNAL_ERROR, ErrorData
16
+
17
+ from fastmcp.server.dependencies import _current_docket, get_context
18
+ from fastmcp.server.tasks.keys import build_task_key
19
+
20
+ if TYPE_CHECKING:
21
+ from fastmcp.server.server import FastMCP
22
+
23
+ # Redis mapping TTL buffer: Add 15 minutes to Docket's execution_ttl
24
+ TASK_MAPPING_TTL_BUFFER_SECONDS = 15 * 60
25
+
26
+
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).
34
+
35
+ Queues the user's actual function to Docket (preserving signature for DI),
36
+ stores raw return values, converts to MCP types on retrieval.
37
+
38
+ 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)
43
+
44
+ Returns:
45
+ CallToolResult: Task stub with task metadata in _meta
46
+ """
47
+ # Generate server-side task ID per SEP-1686 final spec (line 375-377)
48
+ # Server MUST generate task IDs, clients no longer provide them
49
+ server_task_id = str(uuid.uuid4())
50
+
51
+ # 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()
54
+
55
+ # Get session ID and Docket
56
+ ctx = get_context()
57
+ session_id = ctx.session_id
58
+
59
+ docket = _current_docket.get()
60
+ if docket is None:
61
+ raise McpError(
62
+ ErrorData(
63
+ code=INTERNAL_ERROR,
64
+ message="Background tasks require a running FastMCP server context",
65
+ )
66
+ )
67
+
68
+ # Build full task key with embedded metadata
69
+ task_key = build_task_key(session_id, server_task_id, "tool", tool_name)
70
+
71
+ # Get the tool to access user's function
72
+ tool = await server.get_tool(tool_name)
73
+
74
+ # Store task key mapping and creation timestamp in Redis for protocol handlers
75
+ redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
76
+ created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
77
+ ttl_seconds = int(
78
+ docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
79
+ )
80
+ async with docket.redis() as redis:
81
+ await redis.set(redis_key, task_key, ex=ttl_seconds)
82
+ await redis.set(created_at_key, created_at, ex=ttl_seconds)
83
+
84
+ # Send notifications/tasks/created per SEP-1686 (mandatory)
85
+ # Send BEFORE queuing to avoid race where task completes before notification
86
+ notification = mcp.types.JSONRPCNotification(
87
+ jsonrpc="2.0",
88
+ method="notifications/tasks/created",
89
+ params={}, # Empty params per spec
90
+ _meta={ # taskId in _meta per spec
91
+ "modelcontextprotocol.io/related-task": {
92
+ "taskId": server_task_id,
93
+ }
94
+ },
95
+ )
96
+
97
+ ctx = get_context()
98
+ with suppress(Exception):
99
+ # Don't let notification failures break task creation
100
+ await ctx.session.send_notification(notification) # type: ignore[arg-type]
101
+
102
+ # Queue function to Docket by name (result storage via execution_ttl)
103
+ # Use tool.key which matches what was registered - prefixed for mounted tools
104
+ await docket.add(
105
+ tool.key,
106
+ key=task_key,
107
+ )(**arguments)
108
+
109
+ # Spawn subscription task to send status notifications (SEP-1686 optional feature)
110
+ from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
111
+
112
+ # Start subscription in session's task group (persists for connection lifetime)
113
+ if hasattr(ctx.session, "_subscription_task_group"):
114
+ tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
115
+ if tg:
116
+ tg.start_soon( # type: ignore[union-attr]
117
+ subscribe_to_task_updates,
118
+ server_task_id,
119
+ task_key,
120
+ ctx.session,
121
+ docket,
122
+ )
123
+
124
+ # Return task stub
125
+ # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
126
+ return mcp.types.CallToolResult(
127
+ content=[],
128
+ _meta={
129
+ "modelcontextprotocol.io/task": {
130
+ "taskId": server_task_id,
131
+ "status": "working",
132
+ }
133
+ },
134
+ )
135
+
136
+
137
+ async def handle_prompt_as_task(
138
+ server: FastMCP,
139
+ prompt_name: str,
140
+ arguments: dict[str, Any] | None,
141
+ task_meta: dict[str, Any],
142
+ ) -> mcp.types.GetPromptResult:
143
+ """Handle prompt execution as background task (SEP-1686).
144
+
145
+ Queues the user's actual function to Docket (preserving signature for DI).
146
+
147
+ Args:
148
+ server: FastMCP server instance
149
+ prompt_name: Name of the prompt to execute
150
+ arguments: Prompt arguments
151
+ task_meta: Task metadata from request (contains ttl)
152
+
153
+ Returns:
154
+ GetPromptResult: Task stub with task metadata in _meta
155
+ """
156
+ # Generate server-side task ID per SEP-1686 final spec (line 375-377)
157
+ # Server MUST generate task IDs, clients no longer provide them
158
+ server_task_id = str(uuid.uuid4())
159
+
160
+ # Record creation timestamp per SEP-1686 final spec (line 430)
161
+ # Format as ISO 8601 / RFC 3339 timestamp
162
+ created_at = datetime.now(timezone.utc).isoformat()
163
+
164
+ # Get session ID and Docket
165
+ ctx = get_context()
166
+ session_id = ctx.session_id
167
+
168
+ docket = _current_docket.get()
169
+ if docket is None:
170
+ raise McpError(
171
+ ErrorData(
172
+ code=INTERNAL_ERROR,
173
+ message="Background tasks require a running FastMCP server context",
174
+ )
175
+ )
176
+
177
+ # Build full task key with embedded metadata
178
+ task_key = build_task_key(session_id, server_task_id, "prompt", prompt_name)
179
+
180
+ # Get the prompt
181
+ prompt = await server.get_prompt(prompt_name)
182
+
183
+ # Store task key mapping and creation timestamp in Redis for protocol handlers
184
+ redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
185
+ created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
186
+ ttl_seconds = int(
187
+ docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
188
+ )
189
+ async with docket.redis() as redis:
190
+ await redis.set(redis_key, task_key, ex=ttl_seconds)
191
+ await redis.set(created_at_key, created_at, ex=ttl_seconds)
192
+
193
+ # Send notifications/tasks/created per SEP-1686 (mandatory)
194
+ # Send BEFORE queuing to avoid race where task completes before notification
195
+ notification = mcp.types.JSONRPCNotification(
196
+ jsonrpc="2.0",
197
+ method="notifications/tasks/created",
198
+ params={},
199
+ _meta={
200
+ "modelcontextprotocol.io/related-task": {
201
+ "taskId": server_task_id,
202
+ }
203
+ },
204
+ )
205
+ with suppress(Exception):
206
+ await ctx.session.send_notification(notification) # type: ignore[arg-type]
207
+
208
+ # Queue function to Docket by name (result storage via execution_ttl)
209
+ # Use prompt.key which matches what was registered - prefixed for mounted prompts
210
+ await docket.add(
211
+ prompt.key,
212
+ key=task_key,
213
+ )(**(arguments or {}))
214
+
215
+ # Spawn subscription task to send status notifications (SEP-1686 optional feature)
216
+ from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
217
+
218
+ # Start subscription in session's task group (persists for connection lifetime)
219
+ if hasattr(ctx.session, "_subscription_task_group"):
220
+ tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
221
+ if tg:
222
+ tg.start_soon( # type: ignore[union-attr]
223
+ subscribe_to_task_updates,
224
+ server_task_id,
225
+ task_key,
226
+ ctx.session,
227
+ docket,
228
+ )
229
+
230
+ # Return task stub
231
+ # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
232
+ return mcp.types.GetPromptResult(
233
+ description="",
234
+ messages=[],
235
+ _meta={
236
+ "modelcontextprotocol.io/task": {
237
+ "taskId": server_task_id,
238
+ "status": "working",
239
+ }
240
+ },
241
+ )
242
+
243
+
244
+ async def handle_resource_as_task(
245
+ server: FastMCP,
246
+ uri: str,
247
+ resource, # Resource or ResourceTemplate
248
+ task_meta: dict[str, Any],
249
+ ) -> mcp.types.ServerResult:
250
+ """Handle resource read as background task (SEP-1686).
251
+
252
+ Queues the user's actual function to Docket.
253
+
254
+ Args:
255
+ server: FastMCP server instance
256
+ uri: Resource URI
257
+ resource: Resource or ResourceTemplate object
258
+ task_meta: Task metadata from request (contains ttl)
259
+
260
+ Returns:
261
+ ServerResult with ReadResourceResult stub
262
+ """
263
+ # Generate server-side task ID per SEP-1686 final spec (line 375-377)
264
+ # Server MUST generate task IDs, clients no longer provide them
265
+ server_task_id = str(uuid.uuid4())
266
+
267
+ # Record creation timestamp per SEP-1686 final spec (line 430)
268
+ # Format as ISO 8601 / RFC 3339 timestamp
269
+ created_at = datetime.now(timezone.utc).isoformat()
270
+
271
+ # Get session ID and Docket
272
+ ctx = get_context()
273
+ session_id = ctx.session_id
274
+
275
+ docket = _current_docket.get()
276
+ if docket is None:
277
+ raise McpError(
278
+ ErrorData(
279
+ code=INTERNAL_ERROR,
280
+ message="Background tasks require Docket",
281
+ )
282
+ )
283
+
284
+ # Build full task key with embedded metadata (use original URI)
285
+ task_key = build_task_key(session_id, server_task_id, "resource", str(uri))
286
+
287
+ # Store task key mapping and creation timestamp in Redis for protocol handlers
288
+ redis_key = f"fastmcp:task:{session_id}:{server_task_id}"
289
+ created_at_key = f"fastmcp:task:{session_id}:{server_task_id}:created_at"
290
+ ttl_seconds = int(
291
+ docket.execution_ttl.total_seconds() + TASK_MAPPING_TTL_BUFFER_SECONDS
292
+ )
293
+ async with docket.redis() as redis:
294
+ await redis.set(redis_key, task_key, ex=ttl_seconds)
295
+ await redis.set(created_at_key, created_at, ex=ttl_seconds)
296
+
297
+ # Send notifications/tasks/created per SEP-1686 (mandatory)
298
+ # Send BEFORE queuing to avoid race where task completes before notification
299
+ notification = mcp.types.JSONRPCNotification(
300
+ jsonrpc="2.0",
301
+ method="notifications/tasks/created",
302
+ params={},
303
+ _meta={
304
+ "modelcontextprotocol.io/related-task": {
305
+ "taskId": server_task_id,
306
+ }
307
+ },
308
+ )
309
+ with suppress(Exception):
310
+ await ctx.session.send_notification(notification) # type: ignore[arg-type]
311
+
312
+ # Queue function to Docket by name (result storage via execution_ttl)
313
+ # Use resource.name which matches what was registered - prefixed for mounted resources
314
+ # For templates, extract URI params and pass them to the function
315
+ from fastmcp.resources.template import FunctionResourceTemplate, match_uri_template
316
+
317
+ if isinstance(resource, FunctionResourceTemplate):
318
+ params = match_uri_template(uri, resource.uri_template) or {}
319
+ await docket.add(
320
+ resource.name,
321
+ key=task_key,
322
+ )(**params)
323
+ else:
324
+ await docket.add(
325
+ resource.name,
326
+ key=task_key,
327
+ )()
328
+
329
+ # Spawn subscription task to send status notifications (SEP-1686 optional feature)
330
+ from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
331
+
332
+ # Start subscription in session's task group (persists for connection lifetime)
333
+ if hasattr(ctx.session, "_subscription_task_group"):
334
+ tg = ctx.session._subscription_task_group # type: ignore[attr-defined]
335
+ if tg:
336
+ tg.start_soon( # type: ignore[union-attr]
337
+ subscribe_to_task_updates,
338
+ server_task_id,
339
+ task_key,
340
+ ctx.session,
341
+ docket,
342
+ )
343
+
344
+ # Return task stub
345
+ # Tasks MUST begin in "working" status per SEP-1686 final spec (line 381)
346
+ return mcp.types.ServerResult(
347
+ mcp.types.ReadResourceResult(
348
+ contents=[],
349
+ _meta={
350
+ "modelcontextprotocol.io/task": {
351
+ "taskId": server_task_id,
352
+ "status": "working",
353
+ }
354
+ },
355
+ )
356
+ )