Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
2f23ceb
update types.py for tasks
maxisbey Nov 18, 2025
7105d9d
add mvp for server side tasks
maxisbey Nov 19, 2025
2588391
tasks additions
maxisbey Nov 20, 2025
109920e
client
maxisbey Nov 20, 2025
f75029f
taskhint gone
maxisbey Nov 21, 2025
aff2a8c
add task helpers to context
maxisbey Nov 21, 2025
d2968a9
fix utc datetime import, circular dependency, and add type hints
maxisbey Nov 24, 2025
61354eb
notifications and client side
maxisbey Nov 24, 2025
b709d6f
Add client-side task handler protocols and auto-capability building
maxisbey Nov 24, 2025
4c0385f
Refactor client task handlers into ExperimentalTaskHandlers dataclass
maxisbey Nov 24, 2025
78e177b
Add interactive task examples and move call_tool_as_task to experimental
maxisbey Nov 25, 2025
f138fcb
Rename TaskExecutionMode values to match updated spec
maxisbey Nov 25, 2025
0c7df11
Add initial basic docs
maxisbey Nov 25, 2025
ec08851
Add lastUpdatedAt field and related-task metadata for spec conformance
maxisbey Nov 25, 2025
71b7324
Add cancel_task helper and terminal status transition validation
maxisbey Nov 25, 2025
4dde3fc
Add model-immediate-response support to run_task helper
maxisbey Nov 25, 2025
6e2b727
Fix race condition in response routing and simplify handler registration
maxisbey Nov 26, 2025
3c4f262
Fix ElicitRequestParams usage in task_session.py
maxisbey Nov 26, 2025
34ad089
Refactor task architecture: separate pure state from server integration
maxisbey Nov 27, 2025
800b409
Use call_tool_as_task helper in simple-task-client example
maxisbey Nov 27, 2025
9c535a7
Simplify simple-task server example using new task API
maxisbey Nov 27, 2025
e200fbd
Replace magic strings with constants for task status and metadata
maxisbey Nov 27, 2025
8b31b61
Clean up task code: fix error codes, imports, type annotations, and a…
maxisbey Nov 27, 2025
76b3a26
Refactor test_handlers.py to use client_streams fixture
maxisbey Nov 27, 2025
499602e
Use typed request classes instead of hardcoded method strings in test…
maxisbey Nov 27, 2025
dadcccb
Add experimental warning to send_message docstring
maxisbey Nov 27, 2025
4eb5e45
Remove unnecessary request_params from RequestResponder
maxisbey Nov 27, 2025
2f3b792
Add test for task-augmented elicitation (covers client/session.py lin…
maxisbey Nov 27, 2025
b184785
Add coverage tests and fix gaps
maxisbey Nov 27, 2025
27303bc
Add TaskResultHandler unit tests
maxisbey Nov 27, 2025
5830e3c
Add coverage tests for experimental tasks code
maxisbey Nov 27, 2025
a118f98
Add session method coverage tests
maxisbey Nov 27, 2025
49543bb
Add coverage for remaining branch gaps
maxisbey Nov 27, 2025
b529e26
Achieve 100% coverage for experimental tasks
maxisbey Nov 27, 2025
98782fc
Add poll_task() method and update examples to use spec-compliant polling
maxisbey Nov 28, 2025
a9548e8
Revert unnecessary refactor of pkg_version function
maxisbey Nov 28, 2025
a28a650
Add explanatory comment for type narrowing guard in _handle_response
maxisbey Nov 28, 2025
1efe8b0
Add server→client task-augmented elicitation and sampling support
maxisbey Nov 28, 2025
8cd2765
Refactor tasks capability checking into isolated module
maxisbey Nov 28, 2025
b7d44fa
Add comprehensive capability tests and improve test coverage
maxisbey Nov 28, 2025
6eb1b3f
Unify sampling and elicitation code paths with shared validation
maxisbey Nov 28, 2025
e8c7c8a
Achieve 100% test coverage for tasks code
maxisbey Nov 28, 2025
e68f821
Refactor example servers to have simpler tool dispatch
maxisbey Nov 28, 2025
05e19de
Clean up test code patterns
maxisbey Nov 28, 2025
fde9dc8
Clean up test code: remove useless comments and replace sleeps with e…
maxisbey Nov 28, 2025
fc60097
Merge branch 'main' into maxisbey/SEP-1686_Tasks
felixweinberger Nov 28, 2025
4516515
Rewrite tasks documentation for new API
maxisbey Nov 28, 2025
757df38
Fix outdated references in example READMEs
maxisbey Nov 28, 2025
1688c6a
Simplify catch-all case in client session match
maxisbey Nov 28, 2025
31b5aa1
Mark unreachable catch-all case as excluded from coverage
maxisbey Nov 28, 2025
973641c
Re-enable disabled stdio cleanup tests
maxisbey Nov 28, 2025
14c8fb3
Align types.py with official MCP schema for tasks
maxisbey Nov 28, 2025
bbd0ed3
Address review feedback
maxisbey Nov 28, 2025
728c139
Use distinct variable names in interactive task client example
maxisbey Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/experimental/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Experimental Features

!!! warning "Experimental APIs"

The features in this section are experimental and may change without notice.
They track the evolving MCP specification and are not yet stable.

This section documents experimental features in the MCP Python SDK. These features
implement draft specifications that are still being refined.

## Available Experimental Features

### [Tasks](tasks.md)

Tasks enable asynchronous execution of MCP operations. Instead of waiting for a
long-running operation to complete, the server returns a task reference immediately.
Clients can then poll for status updates and retrieve results when ready.

Tasks are useful for:

- **Long-running computations** that would otherwise block
- **Batch operations** that process many items
- **Interactive workflows** that require user input (elicitation) or LLM assistance (sampling)

## Using Experimental APIs

Experimental features are accessed via the `.experimental` property:

```python
# Server-side
@server.experimental.get_task()
async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
...

# Client-side
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
```

## Providing Feedback

Since these features are experimental, feedback is especially valuable. If you encounter
issues or have suggestions, please open an issue on the
[python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues).
287 changes: 287 additions & 0 deletions docs/experimental/tasks-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# Client Task Usage

!!! warning "Experimental"

Tasks are an experimental feature. The API may change without notice.

This guide shows how to call task-augmented tools from an MCP client and retrieve
their results.

## Prerequisites

You'll need:

- An MCP client session connected to a server that supports tasks
- The `ClientSession` from `mcp.client.session`

## Step 1: Call a Tool as a Task

Use the `experimental.call_tool_as_task()` method to call a tool with task
augmentation:

```python
from mcp.client.session import ClientSession

async with ClientSession(read_stream, write_stream) as session:
await session.initialize()

# Call the tool as a task
result = await session.experimental.call_tool_as_task(
"process_data",
{"input": "hello world"},
ttl=60000, # Keep result for 60 seconds
)

# Get the task ID for polling
task_id = result.task.taskId
print(f"Task created: {task_id}")
print(f"Initial status: {result.task.status}")
```

The method returns a `CreateTaskResult` containing:

- `task.taskId` - Unique identifier for polling
- `task.status` - Initial status (usually "working")
- `task.pollInterval` - Suggested polling interval in milliseconds
- `task.ttl` - Time-to-live for the task result

## Step 2: Poll for Status

Check the task status periodically until it completes:

```python
import anyio

while True:
status = await session.experimental.get_task(task_id)
print(f"Status: {status.status}")

if status.statusMessage:
print(f"Message: {status.statusMessage}")

if status.status in ("completed", "failed", "cancelled"):
break

# Respect the suggested poll interval
poll_interval = status.pollInterval or 500
await anyio.sleep(poll_interval / 1000) # Convert ms to seconds
```

The `GetTaskResult` contains:

- `taskId` - The task identifier
- `status` - Current status: "working", "completed", "failed", "cancelled", or "input_required"
- `statusMessage` - Optional progress message
- `pollInterval` - Suggested interval before next poll (milliseconds)

## Step 3: Retrieve the Result

Once the task is complete, retrieve the actual result:

```python
from mcp.types import CallToolResult

if status.status == "completed":
# Get the actual tool result
final_result = await session.experimental.get_task_result(
task_id,
CallToolResult, # The expected result type
)

# Process the result
for content in final_result.content:
if hasattr(content, "text"):
print(f"Result: {content.text}")

elif status.status == "failed":
print(f"Task failed: {status.statusMessage}")
```

The result type depends on the original request:

- `tools/call` tasks return `CallToolResult`
- Other request types return their corresponding result type

## Complete Polling Example

Here's a complete client that calls a task and waits for the result:

```python
import anyio

from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client
from mcp.types import CallToolResult


async def main():
async with stdio_client(
command="python",
args=["server.py"],
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()

# 1. Create the task
print("Creating task...")
result = await session.experimental.call_tool_as_task(
"slow_echo",
{"message": "Hello, Tasks!", "delay_seconds": 3},
)
task_id = result.task.taskId
print(f"Task created: {task_id}")

# 2. Poll until complete
print("Polling for completion...")
while True:
status = await session.experimental.get_task(task_id)
print(f" Status: {status.status}", end="")
if status.statusMessage:
print(f" - {status.statusMessage}", end="")
print()

if status.status in ("completed", "failed", "cancelled"):
break

await anyio.sleep((status.pollInterval or 500) / 1000)

# 3. Get the result
if status.status == "completed":
print("Retrieving result...")
final = await session.experimental.get_task_result(
task_id,
CallToolResult,
)
for content in final.content:
if hasattr(content, "text"):
print(f"Result: {content.text}")
else:
print(f"Task ended with status: {status.status}")


if __name__ == "__main__":
anyio.run(main)
```

## Cancelling Tasks

If you need to cancel a running task:

```python
cancel_result = await session.experimental.cancel_task(task_id)
print(f"Task cancelled, final status: {cancel_result.status}")
```

Note that cancellation is cooperative - the server must check for and handle
cancellation requests. A cancelled task will transition to the "cancelled" state.

## Listing Tasks

To see all tasks on a server:

```python
# Get the first page of tasks
tasks_result = await session.experimental.list_tasks()

for task in tasks_result.tasks:
print(f"Task {task.taskId}: {task.status}")

# Handle pagination if needed
while tasks_result.nextCursor:
tasks_result = await session.experimental.list_tasks(
cursor=tasks_result.nextCursor
)
for task in tasks_result.tasks:
print(f"Task {task.taskId}: {task.status}")
```

## Low-Level API

If you need more control, you can use the low-level request API directly:

```python
from mcp.types import (
ClientRequest,
CallToolRequest,
CallToolRequestParams,
TaskMetadata,
CreateTaskResult,
GetTaskRequest,
GetTaskRequestParams,
GetTaskResult,
GetTaskPayloadRequest,
GetTaskPayloadRequestParams,
)

# Create task with full control over the request
result = await session.send_request(
ClientRequest(
CallToolRequest(
params=CallToolRequestParams(
name="process_data",
arguments={"input": "data"},
task=TaskMetadata(ttl=60000),
),
)
),
CreateTaskResult,
)

# Poll status
status = await session.send_request(
ClientRequest(
GetTaskRequest(
params=GetTaskRequestParams(taskId=result.task.taskId),
)
),
GetTaskResult,
)

# Get result
final = await session.send_request(
ClientRequest(
GetTaskPayloadRequest(
params=GetTaskPayloadRequestParams(taskId=result.task.taskId),
)
),
CallToolResult,
)
```

## Error Handling

Tasks can fail for various reasons. Handle errors appropriately:

```python
try:
result = await session.experimental.call_tool_as_task("my_tool", args)
task_id = result.task.taskId

while True:
status = await session.experimental.get_task(task_id)

if status.status == "completed":
final = await session.experimental.get_task_result(
task_id, CallToolResult
)
# Process success...
break

elif status.status == "failed":
print(f"Task failed: {status.statusMessage}")
break

elif status.status == "cancelled":
print("Task was cancelled")
break

await anyio.sleep(0.5)

except Exception as e:
print(f"Error: {e}")
```

## Next Steps

- [Server Implementation](tasks-server.md) - Learn how to build task-supporting servers
- [Tasks Overview](tasks.md) - Review the task lifecycle and concepts
Loading
Loading