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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {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
|
-
}
|
fastmcp/server/tasks/protocol.py
DELETED
|
@@ -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
|
-
)
|