Skip to content

Commit 10c605a

Browse files
committed
tasks additions
1 parent c508051 commit 10c605a

File tree

15 files changed

+1907
-3
lines changed

15 files changed

+1907
-3
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,13 @@ def call_tool(self, *, validate_input: bool = True):
497497
def decorator(
498498
func: Callable[
499499
...,
500-
Awaitable[UnstructuredContent | StructuredContent | CombinationContent | types.CallToolResult],
500+
Awaitable[
501+
UnstructuredContent
502+
| StructuredContent
503+
| CombinationContent
504+
| types.CallToolResult
505+
| types.CreateTaskResult
506+
],
501507
],
502508
):
503509
logger.debug("Registering handler for CallToolRequest")
@@ -523,6 +529,9 @@ async def handler(req: types.CallToolRequest):
523529
maybe_structured_content: StructuredContent | None
524530
if isinstance(results, types.CallToolResult):
525531
return types.ServerResult(results)
532+
elif isinstance(results, types.CreateTaskResult):
533+
# Task-augmented execution returns task info instead of result
534+
return types.ServerResult(results)
526535
elif isinstance(results, tuple) and len(results) == 2:
527536
# tool returned both structured and unstructured content
528537
unstructured_content, maybe_structured_content = cast(CombinationContent, results)

src/mcp/shared/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
22
from typing import Any, Generic
33

44
from typing_extensions import TypeVar
@@ -26,5 +26,5 @@ class RequestContext(Generic[SessionT, LifespanContextT, RequestT]):
2626
meta: RequestParams.Meta | None
2727
session: SessionT
2828
lifespan_context: LifespanContextT
29-
experimental: Experimental = Experimental()
29+
experimental: Experimental = field(default_factory=Experimental)
3030
request: RequestT | None = None
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Experimental MCP features.
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
"""
5+
6+
from mcp.shared.experimental import tasks
7+
8+
__all__ = ["tasks"]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Experimental task management for MCP.
3+
4+
This module provides:
5+
- TaskStore: Abstract interface for task state storage
6+
- TaskContext: Context object for task work to interact with state/notifications
7+
- InMemoryTaskStore: Reference implementation for testing/development
8+
- Helper functions: run_task, is_terminal, create_task_state, generate_task_id
9+
10+
Architecture:
11+
- TaskStore is pure storage - it doesn't know about execution
12+
- TaskContext wraps store + session, providing a clean API for task work
13+
- run_task is optional convenience for spawning in-process tasks
14+
15+
WARNING: These APIs are experimental and may change without notice.
16+
"""
17+
18+
from mcp.shared.experimental.tasks.context import TaskContext
19+
from mcp.shared.experimental.tasks.helpers import (
20+
create_task_state,
21+
generate_task_id,
22+
is_terminal,
23+
run_task,
24+
task_execution,
25+
)
26+
from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore
27+
from mcp.shared.experimental.tasks.store import TaskStore
28+
29+
__all__ = [
30+
"TaskStore",
31+
"TaskContext",
32+
"InMemoryTaskStore",
33+
"run_task",
34+
"task_execution",
35+
"is_terminal",
36+
"create_task_state",
37+
"generate_task_id",
38+
]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
TaskContext - Context for task work to interact with state and notifications.
3+
"""
4+
5+
from typing import TYPE_CHECKING
6+
7+
from mcp.shared.experimental.tasks.store import TaskStore
8+
from mcp.types import (
9+
Result,
10+
ServerNotification,
11+
Task,
12+
TaskStatusNotification,
13+
TaskStatusNotificationParams,
14+
)
15+
16+
if TYPE_CHECKING:
17+
from mcp.server.session import ServerSession
18+
19+
20+
class TaskContext:
21+
"""
22+
Context provided to task work for state management and notifications.
23+
24+
This wraps a TaskStore and optional session, providing a clean API
25+
for task work to update status, complete, fail, and send notifications.
26+
27+
Example:
28+
async def my_task_work(ctx: TaskContext) -> CallToolResult:
29+
await ctx.update_status("Starting processing...")
30+
31+
for i, item in enumerate(items):
32+
await ctx.update_status(f"Processing item {i+1}/{len(items)}")
33+
if ctx.is_cancelled:
34+
return CallToolResult(content=[TextContent(type="text", text="Cancelled")])
35+
process(item)
36+
37+
return CallToolResult(content=[TextContent(type="text", text="Done!")])
38+
"""
39+
40+
def __init__(
41+
self,
42+
task: Task,
43+
store: TaskStore,
44+
session: "ServerSession | None" = None,
45+
):
46+
self._task = task
47+
self._store = store
48+
self._session = session
49+
self._cancelled = False
50+
51+
@property
52+
def task_id(self) -> str:
53+
"""The task identifier."""
54+
return self._task.taskId
55+
56+
@property
57+
def task(self) -> Task:
58+
"""The current task state."""
59+
return self._task
60+
61+
@property
62+
def is_cancelled(self) -> bool:
63+
"""Whether cancellation has been requested."""
64+
return self._cancelled
65+
66+
def request_cancellation(self) -> None:
67+
"""
68+
Request cancellation of this task.
69+
70+
This sets is_cancelled=True. Task work should check this
71+
periodically and exit gracefully if set.
72+
"""
73+
self._cancelled = True
74+
75+
async def update_status(self, message: str, *, notify: bool = True) -> None:
76+
"""
77+
Update the task's status message.
78+
79+
Args:
80+
message: The new status message
81+
notify: Whether to send a notification to the client
82+
"""
83+
self._task = await self._store.update_task(
84+
self.task_id,
85+
status_message=message,
86+
)
87+
if notify:
88+
await self._send_notification()
89+
90+
async def complete(self, result: Result, *, notify: bool = True) -> None:
91+
"""
92+
Mark the task as completed with the given result.
93+
94+
Args:
95+
result: The task result
96+
notify: Whether to send a notification to the client
97+
"""
98+
await self._store.store_result(self.task_id, result)
99+
self._task = await self._store.update_task(
100+
self.task_id,
101+
status="completed",
102+
)
103+
if notify:
104+
await self._send_notification()
105+
106+
async def fail(self, error: str, *, notify: bool = True) -> None:
107+
"""
108+
Mark the task as failed with an error message.
109+
110+
Args:
111+
error: The error message
112+
notify: Whether to send a notification to the client
113+
"""
114+
self._task = await self._store.update_task(
115+
self.task_id,
116+
status="failed",
117+
status_message=error,
118+
)
119+
if notify:
120+
await self._send_notification()
121+
122+
async def _send_notification(self) -> None:
123+
"""Send a task status notification to the client."""
124+
if self._session is None:
125+
return
126+
127+
await self._session.send_notification(
128+
ServerNotification(
129+
TaskStatusNotification(
130+
params=TaskStatusNotificationParams(
131+
taskId=self._task.taskId,
132+
status=self._task.status,
133+
statusMessage=self._task.statusMessage,
134+
createdAt=self._task.createdAt,
135+
ttl=self._task.ttl,
136+
pollInterval=self._task.pollInterval,
137+
)
138+
)
139+
)
140+
)

0 commit comments

Comments
 (0)