Skip to content

Commit c7fc284

Browse files
committed
Add initial basic docs
1 parent 4f790db commit c7fc284

File tree

5 files changed

+899
-0
lines changed

5 files changed

+899
-0
lines changed

docs/experimental/index.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Experimental Features
2+
3+
!!! warning "Experimental APIs"
4+
5+
The features in this section are experimental and may change without notice.
6+
They track the evolving MCP specification and are not yet stable.
7+
8+
This section documents experimental features in the MCP Python SDK. These features
9+
implement draft specifications that are still being refined.
10+
11+
## Available Experimental Features
12+
13+
### [Tasks](tasks.md)
14+
15+
Tasks enable asynchronous execution of MCP operations. Instead of waiting for a
16+
long-running operation to complete, the server returns a task reference immediately.
17+
Clients can then poll for status updates and retrieve results when ready.
18+
19+
Tasks are useful for:
20+
21+
- **Long-running computations** that would otherwise block
22+
- **Batch operations** that process many items
23+
- **Interactive workflows** that require user input (elicitation) or LLM assistance (sampling)
24+
25+
## Using Experimental APIs
26+
27+
Experimental features are accessed via the `.experimental` property:
28+
29+
```python
30+
# Server-side
31+
@server.experimental.get_task()
32+
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
33+
...
34+
35+
# Client-side
36+
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
37+
```
38+
39+
## Providing Feedback
40+
41+
Since these features are experimental, feedback is especially valuable. If you encounter
42+
issues or have suggestions, please open an issue on the
43+
[python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues).

docs/experimental/tasks-client.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# Client Task Usage
2+
3+
!!! warning "Experimental"
4+
5+
Tasks are an experimental feature. The API may change without notice.
6+
7+
This guide shows how to call task-augmented tools from an MCP client and retrieve
8+
their results.
9+
10+
## Prerequisites
11+
12+
You'll need:
13+
14+
- An MCP client session connected to a server that supports tasks
15+
- The `ClientSession` from `mcp.client.session`
16+
17+
## Step 1: Call a Tool as a Task
18+
19+
Use the `experimental.call_tool_as_task()` method to call a tool with task
20+
augmentation:
21+
22+
```python
23+
from mcp.client.session import ClientSession
24+
25+
async with ClientSession(read_stream, write_stream) as session:
26+
await session.initialize()
27+
28+
# Call the tool as a task
29+
result = await session.experimental.call_tool_as_task(
30+
"process_data",
31+
{"input": "hello world"},
32+
ttl=60000, # Keep result for 60 seconds
33+
)
34+
35+
# Get the task ID for polling
36+
task_id = result.task.taskId
37+
print(f"Task created: {task_id}")
38+
print(f"Initial status: {result.task.status}")
39+
```
40+
41+
The method returns a `CreateTaskResult` containing:
42+
43+
- `task.taskId` - Unique identifier for polling
44+
- `task.status` - Initial status (usually "working")
45+
- `task.pollInterval` - Suggested polling interval in milliseconds
46+
- `task.ttl` - Time-to-live for the task result
47+
48+
## Step 2: Poll for Status
49+
50+
Check the task status periodically until it completes:
51+
52+
```python
53+
import anyio
54+
55+
while True:
56+
status = await session.experimental.get_task(task_id)
57+
print(f"Status: {status.status}")
58+
59+
if status.statusMessage:
60+
print(f"Message: {status.statusMessage}")
61+
62+
if status.status in ("completed", "failed", "cancelled"):
63+
break
64+
65+
# Respect the suggested poll interval
66+
poll_interval = status.pollInterval or 500
67+
await anyio.sleep(poll_interval / 1000) # Convert ms to seconds
68+
```
69+
70+
The `GetTaskResult` contains:
71+
72+
- `taskId` - The task identifier
73+
- `status` - Current status: "working", "completed", "failed", "cancelled", or "input_required"
74+
- `statusMessage` - Optional progress message
75+
- `pollInterval` - Suggested interval before next poll (milliseconds)
76+
77+
## Step 3: Retrieve the Result
78+
79+
Once the task is complete, retrieve the actual result:
80+
81+
```python
82+
from mcp.types import CallToolResult
83+
84+
if status.status == "completed":
85+
# Get the actual tool result
86+
final_result = await session.experimental.get_task_result(
87+
task_id,
88+
CallToolResult, # The expected result type
89+
)
90+
91+
# Process the result
92+
for content in final_result.content:
93+
if hasattr(content, "text"):
94+
print(f"Result: {content.text}")
95+
96+
elif status.status == "failed":
97+
print(f"Task failed: {status.statusMessage}")
98+
```
99+
100+
The result type depends on the original request:
101+
102+
- `tools/call` tasks return `CallToolResult`
103+
- Other request types return their corresponding result type
104+
105+
## Complete Polling Example
106+
107+
Here's a complete client that calls a task and waits for the result:
108+
109+
```python
110+
import anyio
111+
112+
from mcp.client.session import ClientSession
113+
from mcp.client.stdio import stdio_client
114+
from mcp.types import CallToolResult
115+
116+
117+
async def main():
118+
async with stdio_client(
119+
command="python",
120+
args=["server.py"],
121+
) as (read, write):
122+
async with ClientSession(read, write) as session:
123+
await session.initialize()
124+
125+
# 1. Create the task
126+
print("Creating task...")
127+
result = await session.experimental.call_tool_as_task(
128+
"slow_echo",
129+
{"message": "Hello, Tasks!", "delay_seconds": 3},
130+
)
131+
task_id = result.task.taskId
132+
print(f"Task created: {task_id}")
133+
134+
# 2. Poll until complete
135+
print("Polling for completion...")
136+
while True:
137+
status = await session.experimental.get_task(task_id)
138+
print(f" Status: {status.status}", end="")
139+
if status.statusMessage:
140+
print(f" - {status.statusMessage}", end="")
141+
print()
142+
143+
if status.status in ("completed", "failed", "cancelled"):
144+
break
145+
146+
await anyio.sleep((status.pollInterval or 500) / 1000)
147+
148+
# 3. Get the result
149+
if status.status == "completed":
150+
print("Retrieving result...")
151+
final = await session.experimental.get_task_result(
152+
task_id,
153+
CallToolResult,
154+
)
155+
for content in final.content:
156+
if hasattr(content, "text"):
157+
print(f"Result: {content.text}")
158+
else:
159+
print(f"Task ended with status: {status.status}")
160+
161+
162+
if __name__ == "__main__":
163+
anyio.run(main)
164+
```
165+
166+
## Cancelling Tasks
167+
168+
If you need to cancel a running task:
169+
170+
```python
171+
cancel_result = await session.experimental.cancel_task(task_id)
172+
print(f"Task cancelled, final status: {cancel_result.status}")
173+
```
174+
175+
Note that cancellation is cooperative - the server must check for and handle
176+
cancellation requests. A cancelled task will transition to the "cancelled" state.
177+
178+
## Listing Tasks
179+
180+
To see all tasks on a server:
181+
182+
```python
183+
# Get the first page of tasks
184+
tasks_result = await session.experimental.list_tasks()
185+
186+
for task in tasks_result.tasks:
187+
print(f"Task {task.taskId}: {task.status}")
188+
189+
# Handle pagination if needed
190+
while tasks_result.nextCursor:
191+
tasks_result = await session.experimental.list_tasks(
192+
cursor=tasks_result.nextCursor
193+
)
194+
for task in tasks_result.tasks:
195+
print(f"Task {task.taskId}: {task.status}")
196+
```
197+
198+
## Low-Level API
199+
200+
If you need more control, you can use the low-level request API directly:
201+
202+
```python
203+
from mcp.types import (
204+
ClientRequest,
205+
CallToolRequest,
206+
CallToolRequestParams,
207+
TaskMetadata,
208+
CreateTaskResult,
209+
GetTaskRequest,
210+
GetTaskRequestParams,
211+
GetTaskResult,
212+
GetTaskPayloadRequest,
213+
GetTaskPayloadRequestParams,
214+
)
215+
216+
# Create task with full control over the request
217+
result = await session.send_request(
218+
ClientRequest(
219+
CallToolRequest(
220+
params=CallToolRequestParams(
221+
name="process_data",
222+
arguments={"input": "data"},
223+
task=TaskMetadata(ttl=60000),
224+
),
225+
)
226+
),
227+
CreateTaskResult,
228+
)
229+
230+
# Poll status
231+
status = await session.send_request(
232+
ClientRequest(
233+
GetTaskRequest(
234+
params=GetTaskRequestParams(taskId=result.task.taskId),
235+
)
236+
),
237+
GetTaskResult,
238+
)
239+
240+
# Get result
241+
final = await session.send_request(
242+
ClientRequest(
243+
GetTaskPayloadRequest(
244+
params=GetTaskPayloadRequestParams(taskId=result.task.taskId),
245+
)
246+
),
247+
CallToolResult,
248+
)
249+
```
250+
251+
## Error Handling
252+
253+
Tasks can fail for various reasons. Handle errors appropriately:
254+
255+
```python
256+
try:
257+
result = await session.experimental.call_tool_as_task("my_tool", args)
258+
task_id = result.task.taskId
259+
260+
while True:
261+
status = await session.experimental.get_task(task_id)
262+
263+
if status.status == "completed":
264+
final = await session.experimental.get_task_result(
265+
task_id, CallToolResult
266+
)
267+
# Process success...
268+
break
269+
270+
elif status.status == "failed":
271+
print(f"Task failed: {status.statusMessage}")
272+
break
273+
274+
elif status.status == "cancelled":
275+
print("Task was cancelled")
276+
break
277+
278+
await anyio.sleep(0.5)
279+
280+
except Exception as e:
281+
print(f"Error: {e}")
282+
```
283+
284+
## Next Steps
285+
286+
- [Server Implementation](tasks-server.md) - Learn how to build task-supporting servers
287+
- [Tasks Overview](tasks.md) - Review the task lifecycle and concepts

0 commit comments

Comments
 (0)