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
@@ -1,206 +0,0 @@
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
- # PromptMessage is imported from mcp.types - use directly
127
- messages.append(msg)
128
- elif isinstance(msg, str):
129
- messages.append(
130
- mcp.types.PromptMessage(
131
- role="user",
132
- content=mcp.types.TextContent(type="text", text=msg),
133
- )
134
- )
135
- elif isinstance(msg, dict):
136
- messages.append(mcp.types.PromptMessage.model_validate(msg))
137
- else:
138
- raise ValueError(f"Invalid message type: {type(msg)}")
139
-
140
- return mcp.types.GetPromptResult(
141
- description=prompt.description or "",
142
- messages=messages,
143
- _meta={
144
- "modelcontextprotocol.io/related-task": {
145
- "taskId": client_task_id,
146
- }
147
- },
148
- )
149
-
150
-
151
- async def convert_resource_result(
152
- server: FastMCP, raw_value: Any, uri: str, client_task_id: str
153
- ) -> dict[str, Any]:
154
- """Convert raw resource return value to MCP resource contents dict.
155
-
156
- Args:
157
- server: FastMCP server instance
158
- raw_value: The raw return value from user's resource function (str or bytes)
159
- uri: Resource URI (for the contents response)
160
- client_task_id: Client task ID for related-task metadata
161
-
162
- Returns:
163
- Dict with 'contents' key containing list of resource contents
164
- """
165
- # Build related-task metadata
166
- related_task_meta = {
167
- "modelcontextprotocol.io/related-task": {
168
- "taskId": client_task_id,
169
- }
170
- }
171
-
172
- # Resources return str or bytes directly
173
- if isinstance(raw_value, str):
174
- return {
175
- "contents": [
176
- {
177
- "uri": uri,
178
- "text": raw_value,
179
- "mimeType": "text/plain",
180
- }
181
- ],
182
- "_meta": related_task_meta,
183
- }
184
- elif isinstance(raw_value, bytes):
185
- return {
186
- "contents": [
187
- {
188
- "uri": uri,
189
- "blob": base64.b64encode(raw_value).decode(),
190
- "mimeType": "application/octet-stream",
191
- }
192
- ],
193
- "_meta": related_task_meta,
194
- }
195
- else:
196
- # Fallback: convert to JSON string
197
- return {
198
- "contents": [
199
- {
200
- "uri": uri,
201
- "text": json.dumps(raw_value),
202
- "mimeType": "application/json",
203
- }
204
- ],
205
- "_meta": related_task_meta,
206
- }
@@ -1,359 +0,0 @@
1
- """SEP-1686 task protocol handlers.
2
-
3
- Implements MCP task protocol methods: tasks/get, tasks/result, tasks/list, tasks/cancel, tasks/delete.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from datetime import datetime, timedelta, timezone
9
- from typing import TYPE_CHECKING, Any
10
-
11
- import mcp.types
12
- from docket.execution import ExecutionState
13
- from mcp.shared.exceptions import McpError
14
- from mcp.types import (
15
- INTERNAL_ERROR,
16
- INVALID_PARAMS,
17
- CancelTaskResult,
18
- ErrorData,
19
- GetTaskResult,
20
- ListTasksResult,
21
- )
22
-
23
- from fastmcp.server.tasks.converters import (
24
- convert_prompt_result,
25
- convert_resource_result,
26
- convert_tool_result,
27
- )
28
- from fastmcp.server.tasks.keys import parse_task_key
29
-
30
- if TYPE_CHECKING:
31
- from fastmcp.server.server import FastMCP
32
-
33
- # Map Docket execution states to MCP task status strings
34
- # Per SEP-1686 final spec (line 381): tasks MUST begin in "working" status
35
- DOCKET_TO_MCP_STATE: dict[ExecutionState, str] = {
36
- ExecutionState.SCHEDULED: "working", # Initial state per spec
37
- ExecutionState.QUEUED: "working", # Initial state per spec
38
- ExecutionState.RUNNING: "working",
39
- ExecutionState.COMPLETED: "completed",
40
- ExecutionState.FAILED: "failed",
41
- ExecutionState.CANCELLED: "cancelled",
42
- }
43
-
44
-
45
- async def tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskResult:
46
- """Handle MCP 'tasks/get' request (SEP-1686).
47
-
48
- Args:
49
- server: FastMCP server instance
50
- params: Request params containing taskId
51
-
52
- Returns:
53
- GetTaskResult: Task status response with spec-compliant fields
54
- """
55
- import fastmcp.server.context
56
-
57
- async with fastmcp.server.context.Context(fastmcp=server) as ctx:
58
- client_task_id = params.get("taskId")
59
- if not client_task_id:
60
- raise McpError(
61
- ErrorData(
62
- code=INVALID_PARAMS, message="Missing required parameter: taskId"
63
- )
64
- )
65
-
66
- # Get session ID from Context
67
- session_id = ctx.session_id
68
-
69
- # Get execution from Docket (use instance attribute for cross-task access)
70
- docket = server._docket
71
- if docket is None:
72
- raise McpError(
73
- ErrorData(
74
- code=INTERNAL_ERROR,
75
- message="Background tasks require Docket",
76
- )
77
- )
78
-
79
- # Look up full task key and creation timestamp from Redis
80
- task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
81
- created_at_key = docket.key(
82
- f"fastmcp:task:{session_id}:{client_task_id}:created_at"
83
- )
84
- async with docket.redis() as redis:
85
- task_key_bytes = await redis.get(task_meta_key)
86
- created_at_bytes = await redis.get(created_at_key)
87
-
88
- task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
89
- created_at = (
90
- None if created_at_bytes is None else created_at_bytes.decode("utf-8")
91
- )
92
-
93
- if task_key is None:
94
- # Task not found - raise error per MCP protocol
95
- raise McpError(
96
- ErrorData(
97
- code=INVALID_PARAMS, message=f"Task {client_task_id} not found"
98
- )
99
- )
100
-
101
- execution = await docket.get_execution(task_key)
102
- if execution is None:
103
- # Task key exists but no execution - raise error
104
- raise McpError(
105
- ErrorData(
106
- code=INVALID_PARAMS,
107
- message=f"Task {client_task_id} execution not found",
108
- )
109
- )
110
-
111
- # Sync state from Redis
112
- await execution.sync()
113
-
114
- # Map Docket state to MCP state
115
- mcp_state = DOCKET_TO_MCP_STATE.get(execution.state, "failed")
116
-
117
- # Build response (use default ttl since we don't track per-task values)
118
- # createdAt is REQUIRED per SEP-1686 final spec (line 430)
119
- # Per spec lines 447-448: SHOULD NOT include related-task metadata in tasks/get
120
- error_message = None
121
- status_message = None
122
-
123
- if execution.state == ExecutionState.FAILED:
124
- try:
125
- await execution.get_result(timeout=timedelta(seconds=0))
126
- except Exception as error:
127
- error_message = str(error)
128
- status_message = f"Task failed: {error_message}"
129
- elif execution.progress and execution.progress.message:
130
- # Extract progress message from Docket if available (spec line 403)
131
- status_message = execution.progress.message
132
-
133
- return GetTaskResult(
134
- taskId=client_task_id,
135
- status=mcp_state, # type: ignore[arg-type]
136
- createdAt=created_at, # type: ignore[arg-type]
137
- lastUpdatedAt=datetime.now(timezone.utc),
138
- ttl=60000,
139
- pollInterval=1000,
140
- statusMessage=status_message,
141
- )
142
-
143
-
144
- async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
145
- """Handle MCP 'tasks/result' request (SEP-1686).
146
-
147
- Converts raw task return values to MCP types based on task type.
148
-
149
- Args:
150
- server: FastMCP server instance
151
- params: Request params containing taskId
152
-
153
- Returns:
154
- MCP result (CallToolResult, GetPromptResult, or ReadResourceResult)
155
- """
156
- import fastmcp.server.context
157
-
158
- async with fastmcp.server.context.Context(fastmcp=server) as ctx:
159
- client_task_id = params.get("taskId")
160
- if not client_task_id:
161
- raise McpError(
162
- ErrorData(
163
- code=INVALID_PARAMS, message="Missing required parameter: taskId"
164
- )
165
- )
166
-
167
- # Get session ID from Context
168
- session_id = ctx.session_id
169
-
170
- # Get execution from Docket (use instance attribute for cross-task access)
171
- docket = server._docket
172
- if docket is None:
173
- raise McpError(
174
- ErrorData(
175
- code=INTERNAL_ERROR,
176
- message="Background tasks require Docket",
177
- )
178
- )
179
-
180
- # Look up full task key from Redis
181
- task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
182
- async with docket.redis() as redis:
183
- task_key_bytes = await redis.get(task_meta_key)
184
-
185
- task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
186
-
187
- if task_key is None:
188
- raise McpError(
189
- ErrorData(
190
- code=INVALID_PARAMS,
191
- message=f"Invalid taskId: {client_task_id} not found",
192
- )
193
- )
194
-
195
- execution = await docket.get_execution(task_key)
196
- if execution is None:
197
- raise McpError(
198
- ErrorData(
199
- code=INVALID_PARAMS,
200
- message=f"Invalid taskId: {client_task_id} not found",
201
- )
202
- )
203
-
204
- # Sync state from Redis
205
- await execution.sync()
206
-
207
- # Check if completed
208
- if execution.state not in (ExecutionState.COMPLETED, ExecutionState.FAILED):
209
- mcp_state = DOCKET_TO_MCP_STATE.get(execution.state, "failed")
210
- raise McpError(
211
- ErrorData(
212
- code=INVALID_PARAMS,
213
- message=f"Task not completed yet (current state: {mcp_state})",
214
- )
215
- )
216
-
217
- # Get result from Docket
218
- try:
219
- raw_value = await execution.get_result(timeout=timedelta(seconds=0))
220
- except Exception as error:
221
- # Task failed - return error result
222
- return mcp.types.CallToolResult(
223
- content=[mcp.types.TextContent(type="text", text=str(error))],
224
- isError=True,
225
- _meta={
226
- "modelcontextprotocol.io/related-task": {
227
- "taskId": client_task_id,
228
- }
229
- },
230
- )
231
-
232
- # Parse task key to get type and component info
233
- key_parts = parse_task_key(task_key)
234
- task_type = key_parts["task_type"]
235
-
236
- # Convert based on task type (pass client_task_id for metadata)
237
- if task_type == "tool":
238
- return await convert_tool_result(
239
- server, raw_value, key_parts["component_identifier"], client_task_id
240
- )
241
- elif task_type == "prompt":
242
- return await convert_prompt_result(
243
- server, raw_value, key_parts["component_identifier"], client_task_id
244
- )
245
- elif task_type == "resource":
246
- return await convert_resource_result(
247
- server, raw_value, key_parts["component_identifier"], client_task_id
248
- )
249
- else:
250
- raise McpError(
251
- ErrorData(
252
- code=INTERNAL_ERROR,
253
- message=f"Internal error: Unknown task type: {task_type}",
254
- )
255
- )
256
-
257
-
258
- async def tasks_list_handler(
259
- server: FastMCP, params: dict[str, Any]
260
- ) -> ListTasksResult:
261
- """Handle MCP 'tasks/list' request (SEP-1686).
262
-
263
- Note: With client-side tracking, this returns minimal info.
264
-
265
- Args:
266
- server: FastMCP server instance
267
- params: Request params (cursor, limit)
268
-
269
- Returns:
270
- ListTasksResult: Response with tasks list and pagination
271
- """
272
- # Return empty list - client tracks tasks locally
273
- return ListTasksResult(tasks=[], nextCursor=None)
274
-
275
-
276
- async def tasks_cancel_handler(
277
- server: FastMCP, params: dict[str, Any]
278
- ) -> CancelTaskResult:
279
- """Handle MCP 'tasks/cancel' request (SEP-1686).
280
-
281
- Cancels a running task, transitioning it to cancelled state.
282
-
283
- Args:
284
- server: FastMCP server instance
285
- params: Request params containing taskId
286
-
287
- Returns:
288
- CancelTaskResult: Task status response showing cancelled state
289
- """
290
- import fastmcp.server.context
291
-
292
- async with fastmcp.server.context.Context(fastmcp=server) as ctx:
293
- client_task_id = params.get("taskId")
294
- if not client_task_id:
295
- raise McpError(
296
- ErrorData(
297
- code=INVALID_PARAMS, message="Missing required parameter: taskId"
298
- )
299
- )
300
-
301
- # Get session ID from Context
302
- session_id = ctx.session_id
303
-
304
- docket = server._docket
305
- if docket is None:
306
- raise McpError(
307
- ErrorData(
308
- code=INTERNAL_ERROR,
309
- message="Background tasks require Docket",
310
- )
311
- )
312
-
313
- # Look up full task key and creation timestamp from Redis
314
- task_meta_key = docket.key(f"fastmcp:task:{session_id}:{client_task_id}")
315
- created_at_key = docket.key(
316
- f"fastmcp:task:{session_id}:{client_task_id}:created_at"
317
- )
318
- async with docket.redis() as redis:
319
- task_key_bytes = await redis.get(task_meta_key)
320
- created_at_bytes = await redis.get(created_at_key)
321
-
322
- task_key = None if task_key_bytes is None else task_key_bytes.decode("utf-8")
323
- created_at = (
324
- None if created_at_bytes is None else created_at_bytes.decode("utf-8")
325
- )
326
-
327
- if task_key is None:
328
- raise McpError(
329
- ErrorData(
330
- code=INVALID_PARAMS,
331
- message=f"Invalid taskId: {client_task_id} not found",
332
- )
333
- )
334
-
335
- # Check if task exists
336
- execution = await docket.get_execution(task_key)
337
- if execution is None:
338
- raise McpError(
339
- ErrorData(
340
- code=INVALID_PARAMS,
341
- message=f"Invalid taskId: {client_task_id} not found",
342
- )
343
- )
344
-
345
- # Cancel via Docket (now sets CANCELLED state natively)
346
- await docket.cancel(task_key)
347
-
348
- # Return task status with cancelled state
349
- # createdAt is REQUIRED per SEP-1686 final spec (line 430)
350
- # Per spec lines 447-448: SHOULD NOT include related-task metadata in tasks/cancel
351
- return CancelTaskResult(
352
- taskId=client_task_id,
353
- status="cancelled",
354
- createdAt=created_at or datetime.now(timezone.utc).isoformat(),
355
- lastUpdatedAt=datetime.now(timezone.utc),
356
- ttl=60_000,
357
- pollInterval=1000,
358
- statusMessage="Task cancelled",
359
- )