Remove Tasks (SEP-1686) from the SDK (#2714)

This commit is contained in:
Max 2026-06-02 17:27:05 +01:00 • committed by GitHub
commit 8cc187fac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 63 additions and 12963 deletions

View file

@ -2489,7 +2489,6 @@ MCP servers declare capabilities during initialization:
## Documentation
- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/)
- [Experimental Features (Tasks)](https://modelcontextprotocol.github.io/python-sdk/experimental/tasks/)
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
- [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest)
- [Officially supported servers](https://github.com/modelcontextprotocol/servers)

View file

@ -1,42 +0,0 @@
# 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: enable task support (auto-registers default handlers)
server = Server(name="my-server")
server.experimental.enable_tasks()
# 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).

View file

@ -1,361 +0,0 @@
# Client Task Usage
!!! warning "Experimental"
Tasks are an experimental feature. The API may change without notice.
This guide covers calling task-augmented tools from clients, handling the `input_required` status, and advanced patterns like receiving task requests from servers.
## Quick Start
Call a tool as a task and poll for the result:
```python
from mcp.client.session import ClientSession
from mcp.types import CallToolResult
async with ClientSession(read, write) as session:
await session.initialize()
# Call tool as task
result = await session.experimental.call_tool_as_task(
"process_data",
{"input": "hello"},
ttl=60000,
)
task_id = result.task.taskId
# Poll until complete
async for status in session.experimental.poll_task(task_id):
print(f"Status: {status.status} - {status.statusMessage or ''}")
# Get result
final = await session.experimental.get_task_result(task_id, CallToolResult)
print(f"Result: {final.content[0].text}")
```
## Calling Tools as Tasks
Use `call_tool_as_task()` to invoke a tool with task augmentation:
```python
result = await session.experimental.call_tool_as_task(
"my_tool", # Tool name
{"arg": "value"}, # Arguments
ttl=60000, # Time-to-live in milliseconds
meta={"key": "val"}, # Optional metadata
)
task_id = result.task.taskId
print(f"Task: {task_id}, Status: {result.task.status}")
```
The response is a `CreateTaskResult` containing:
- `task.taskId` - Unique identifier for polling
- `task.status` - Initial status (usually `"working"`)
- `task.pollInterval` - Suggested polling interval (milliseconds)
- `task.ttl` - Time-to-live for results
- `task.createdAt` - Creation timestamp
## Polling with poll_task
The `poll_task()` async iterator polls until the task reaches a terminal state:
```python
async for status in session.experimental.poll_task(task_id):
print(f"Status: {status.status}")
if status.statusMessage:
print(f"Progress: {status.statusMessage}")
```
It automatically:
- Respects the server's suggested `pollInterval`
- Stops when status is `completed`, `failed`, or `cancelled`
- Yields each status for progress display
### Handling input_required
When a task needs user input (elicitation), it transitions to `input_required`. You must call `get_task_result()` to receive and respond to the elicitation:
```python
async for status in session.experimental.poll_task(task_id):
print(f"Status: {status.status}")
if status.status == "input_required":
# This delivers the elicitation and waits for completion
final = await session.experimental.get_task_result(task_id, CallToolResult)
break
```
The elicitation callback (set during session creation) handles the actual user interaction.
## Elicitation Callbacks
To handle elicitation requests from the server, provide a callback when creating the session:
```python
from mcp.types import ElicitRequestParams, ElicitResult
async def handle_elicitation(context, params: ElicitRequestParams) -> ElicitResult:
# Display the message to the user
print(f"Server asks: {params.message}")
# Collect user input (this is a simplified example)
response = input("Your response (y/n): ")
confirmed = response.lower() == "y"
return ElicitResult(
action="accept",
content={"confirm": confirmed},
)
async with ClientSession(
read,
write,
elicitation_callback=handle_elicitation,
) as session:
await session.initialize()
# ... call tasks that may require elicitation
```
## Sampling Callbacks
Similarly, handle sampling requests with a callback:
```python
from mcp.types import CreateMessageRequestParams, CreateMessageResult, TextContent
async def handle_sampling(context, params: CreateMessageRequestParams) -> CreateMessageResult:
# In a real implementation, call your LLM here
prompt = params.messages[-1].content.text if params.messages else ""
# Return a mock response
return CreateMessageResult(
role="assistant",
content=TextContent(type="text", text=f"Response to: {prompt}"),
model="my-model",
)
async with ClientSession(
read,
write,
sampling_callback=handle_sampling,
) as session:
# ...
```
## Retrieving Results
Once a task completes, retrieve the result:
```python
if status.status == "completed":
result = await session.experimental.get_task_result(task_id, CallToolResult)
for content in result.content:
if hasattr(content, "text"):
print(content.text)
elif status.status == "failed":
print(f"Task failed: {status.statusMessage}")
elif status.status == "cancelled":
print("Task was cancelled")
```
The result type matches the original request:
- `tools/call` → `CallToolResult`
- `sampling/createMessage` → `CreateMessageResult`
- `elicitation/create` → `ElicitResult`
## Cancellation
Cancel a running task:
```python
cancel_result = await session.experimental.cancel_task(task_id)
print(f"Cancelled, status: {cancel_result.status}")
```
Note: Cancellation is cooperative—the server must check for and handle cancellation.
## Listing Tasks
View all tasks on the server:
```python
result = await session.experimental.list_tasks()
for task in result.tasks:
print(f"{task.taskId}: {task.status}")
# Handle pagination
while result.nextCursor:
result = await session.experimental.list_tasks(cursor=result.nextCursor)
for task in result.tasks:
print(f"{task.taskId}: {task.status}")
```
## Advanced: Client as Task Receiver
Servers can send task-augmented requests to clients. This is useful when the server needs the client to perform async work (like complex sampling or user interaction).
### Declaring Client Capabilities
Register task handlers to declare what task-augmented requests your client accepts:
```python
from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers
from mcp.types import (
CreateTaskResult, GetTaskResult, GetTaskPayloadResult,
TaskMetadata, ElicitRequestParams,
)
from mcp.shared.experimental.tasks import InMemoryTaskStore
# Client-side task store
client_store = InMemoryTaskStore()
async def handle_augmented_elicitation(context, params: ElicitRequestParams, task_metadata: TaskMetadata):
"""Handle task-augmented elicitation from server."""
# Create a task for this elicitation
task = await client_store.create_task(task_metadata)
# Start async work (e.g., show UI, wait for user)
async def complete_elicitation():
# ... do async work ...
result = ElicitResult(action="accept", content={"confirm": True})
await client_store.store_result(task.taskId, result)
await client_store.update_task(task.taskId, status="completed")
context.session._task_group.start_soon(complete_elicitation)
# Return task reference immediately
return CreateTaskResult(task=task)
async def handle_get_task(context, params):
"""Handle tasks/get from server."""
task = await client_store.get_task(params.taskId)
return GetTaskResult(
taskId=task.taskId,
status=task.status,
statusMessage=task.statusMessage,
createdAt=task.createdAt,
lastUpdatedAt=task.lastUpdatedAt,
ttl=task.ttl,
pollInterval=100,
)
async def handle_get_task_result(context, params):
"""Handle tasks/result from server."""
result = await client_store.get_result(params.taskId)
return GetTaskPayloadResult.model_validate(result.model_dump())
task_handlers = ExperimentalTaskHandlers(
augmented_elicitation=handle_augmented_elicitation,
get_task=handle_get_task,
get_task_result=handle_get_task_result,
)
async with ClientSession(
read,
write,
experimental_task_handlers=task_handlers,
) as session:
# Client now accepts task-augmented elicitation from server
await session.initialize()
```
This enables flows where:
1. Client calls a task-augmented tool
2. Server's tool work calls `task.elicit_as_task()`
3. Client receives task-augmented elicitation
4. Client creates its own task, does async work
5. Server polls client's task
6. Eventually both tasks complete
## Complete Example
A client that handles all task scenarios:
```python
import anyio
from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client
from mcp.types import CallToolResult, ElicitRequestParams, ElicitResult
async def elicitation_callback(context, params: ElicitRequestParams) -> ElicitResult:
print(f"\n[Elicitation] {params.message}")
response = input("Confirm? (y/n): ")
return ElicitResult(action="accept", content={"confirm": response.lower() == "y"})
async def main():
async with stdio_client(command="python", args=["server.py"]) as (read, write):
async with ClientSession(
read,
write,
elicitation_callback=elicitation_callback,
) as session:
await session.initialize()
# List available tools
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
# Call a task-augmented tool
print("\nCalling task tool...")
result = await session.experimental.call_tool_as_task(
"confirm_action",
{"action": "delete files"},
)
task_id = result.task.taskId
print(f"Task created: {task_id}")
# Poll and handle input_required
async for status in session.experimental.poll_task(task_id):
print(f"Status: {status.status}")
if status.status == "input_required":
final = await session.experimental.get_task_result(task_id, CallToolResult)
print(f"Result: {final.content[0].text}")
break
if status.status == "completed":
final = await session.experimental.get_task_result(task_id, CallToolResult)
print(f"Result: {final.content[0].text}")
if __name__ == "__main__":
anyio.run(main)
```
## Error Handling
Handle task errors gracefully:
```python
from mcp.shared.exceptions import MCPError
try:
result = await session.experimental.call_tool_as_task("my_tool", args)
task_id = result.task.taskId
async for status in session.experimental.poll_task(task_id):
if status.status == "failed":
raise RuntimeError(f"Task failed: {status.statusMessage}")
final = await session.experimental.get_task_result(task_id, CallToolResult)
except MCPError as e:
print(f"MCP error: {e.message}")
except Exception as e:
print(f"Error: {e}")
```
## Next Steps
- [Server Implementation](tasks-server.md) - Build task-supporting servers
- [Tasks Overview](tasks.md) - Review lifecycle and concepts

View file

@ -1,577 +0,0 @@
# Server Task Implementation
!!! warning "Experimental"
Tasks are an experimental feature. The API may change without notice.
This guide covers implementing task support in MCP servers, from basic setup to advanced patterns like elicitation and sampling within tasks.
## Quick Start
The simplest way to add task support:
```python
from mcp.server import Server
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.types import CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED
server = Server("my-server")
server.experimental.enable_tasks() # Registers all task handlers automatically
@server.list_tools()
async def list_tools():
return [
Tool(
name="process_data",
description="Process data asynchronously",
inputSchema={"type": "object", "properties": {"input": {"type": "string"}}},
execution=ToolExecution(taskSupport=TASK_REQUIRED),
)
]
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTaskResult:
if name == "process_data":
return await handle_process_data(arguments)
return CallToolResult(content=[TextContent(type="text", text=f"Unknown: {name}")], isError=True)
async def handle_process_data(arguments: dict) -> CreateTaskResult:
ctx = server.request_context
ctx.experimental.validate_task_mode(TASK_REQUIRED)
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Processing...")
result = arguments.get("input", "").upper()
return CallToolResult(content=[TextContent(type="text", text=result)])
return await ctx.experimental.run_task(work)
```
That's it. `enable_tasks()` automatically:
- Creates an in-memory task store
- Registers handlers for `tasks/get`, `tasks/result`, `tasks/list`, `tasks/cancel`
- Updates server capabilities
## Tool Declaration
Tools declare task support via the `execution.taskSupport` field:
```python
from mcp.types import Tool, ToolExecution, TASK_REQUIRED, TASK_OPTIONAL, TASK_FORBIDDEN
Tool(
name="my_tool",
inputSchema={"type": "object"},
execution=ToolExecution(taskSupport=TASK_REQUIRED), # or TASK_OPTIONAL, TASK_FORBIDDEN
)
```
| Value | Meaning |
|-------|---------|
| `TASK_REQUIRED` | Tool **must** be called as a task |
| `TASK_OPTIONAL` | Tool supports both sync and task execution |
| `TASK_FORBIDDEN` | Tool **cannot** be called as a task (default) |
Validate the request matches your tool's requirements:
```python
@server.call_tool()
async def handle_tool(name: str, arguments: dict):
ctx = server.request_context
if name == "required_task_tool":
ctx.experimental.validate_task_mode(TASK_REQUIRED) # Raises if not task mode
return await handle_as_task(arguments)
elif name == "optional_task_tool":
if ctx.experimental.is_task:
return await handle_as_task(arguments)
else:
return handle_sync(arguments)
```
## The run_task Pattern
`run_task()` is the recommended way to execute task work:
```python
async def handle_my_tool(arguments: dict) -> CreateTaskResult:
ctx = server.request_context
ctx.experimental.validate_task_mode(TASK_REQUIRED)
async def work(task: ServerTaskContext) -> CallToolResult:
# Your work here
return CallToolResult(content=[TextContent(type="text", text="Done")])
return await ctx.experimental.run_task(work)
```
**What `run_task()` does:**
1. Creates a task in the store
2. Spawns your work function in the background
3. Returns `CreateTaskResult` immediately
4. Auto-completes the task when your function returns
5. Auto-fails the task if your function raises
**The `ServerTaskContext` provides:**
- `task.task_id` - The task identifier
- `task.update_status(message)` - Update progress
- `task.complete(result)` - Explicitly complete (usually automatic)
- `task.fail(error)` - Explicitly fail
- `task.is_cancelled` - Check if cancellation requested
## Status Updates
Keep clients informed of progress:
```python
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Starting...")
for i, item in enumerate(items):
await task.update_status(f"Processing {i+1}/{len(items)}")
await process_item(item)
await task.update_status("Finalizing...")
return CallToolResult(content=[TextContent(type="text", text="Complete")])
```
Status messages appear in `tasks/get` responses, letting clients show progress to users.
## Elicitation Within Tasks
Tasks can request user input via elicitation. This transitions the task to `input_required` status.
### Form Elicitation
Collect structured data from the user:
```python
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Waiting for confirmation...")
result = await task.elicit(
message="Delete these files?",
requestedSchema={
"type": "object",
"properties": {
"confirm": {"type": "boolean"},
"reason": {"type": "string"},
},
"required": ["confirm"],
},
)
if result.action == "accept" and result.content.get("confirm"):
# User confirmed
return CallToolResult(content=[TextContent(type="text", text="Files deleted")])
else:
# User declined or cancelled
return CallToolResult(content=[TextContent(type="text", text="Cancelled")])
```
### URL Elicitation
Direct users to external URLs for OAuth, payments, or other out-of-band flows:
```python
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Waiting for OAuth...")
result = await task.elicit_url(
message="Please authorize with GitHub",
url="https://github.com/login/oauth/authorize?client_id=...",
elicitation_id="oauth-github-123",
)
if result.action == "accept":
# User completed OAuth flow
return CallToolResult(content=[TextContent(type="text", text="Connected to GitHub")])
else:
return CallToolResult(content=[TextContent(type="text", text="OAuth cancelled")])
```
## Sampling Within Tasks
Tasks can request LLM completions from the client:
```python
from mcp.types import SamplingMessage, TextContent
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Generating response...")
result = await task.create_message(
messages=[
SamplingMessage(
role="user",
content=TextContent(type="text", text="Write a haiku about coding"),
)
],
max_tokens=100,
)
haiku = result.content.text if isinstance(result.content, TextContent) else "Error"
return CallToolResult(content=[TextContent(type="text", text=haiku)])
```
Sampling supports additional parameters:
```python
result = await task.create_message(
messages=[...],
max_tokens=500,
system_prompt="You are a helpful assistant",
temperature=0.7,
stop_sequences=["\n\n"],
model_preferences=ModelPreferences(hints=[ModelHint(name="claude-3")]),
)
```
## Cancellation Support
Check for cancellation in long-running work:
```python
async def work(task: ServerTaskContext) -> CallToolResult:
for i in range(1000):
if task.is_cancelled:
# Clean up and exit
return CallToolResult(content=[TextContent(type="text", text="Cancelled")])
await task.update_status(f"Step {i}/1000")
await process_step(i)
return CallToolResult(content=[TextContent(type="text", text="Complete")])
```
The SDK's default cancel handler updates the task status. Your work function should check `is_cancelled` periodically.
## Custom Task Store
For production, implement `TaskStore` with persistent storage:
```python
from mcp.shared.experimental.tasks.store import TaskStore
from mcp.types import Task, TaskMetadata, Result
class RedisTaskStore(TaskStore):
def __init__(self, redis_client):
self.redis = redis_client
async def create_task(self, metadata: TaskMetadata, task_id: str | None = None) -> Task:
# Create and persist task
...
async def get_task(self, task_id: str) -> Task | None:
# Retrieve task from Redis
...
async def update_task(self, task_id: str, status: str | None = None, ...) -> Task:
# Update and persist
...
async def store_result(self, task_id: str, result: Result) -> None:
# Store result in Redis
...
async def get_result(self, task_id: str) -> Result | None:
# Retrieve result
...
# ... implement remaining methods
```
Use your custom store:
```python
store = RedisTaskStore(redis_client)
server.experimental.enable_tasks(store=store)
```
## Complete Example
A server with multiple task-supporting tools:
```python
from mcp.server import Server
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.types import (
CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution,
SamplingMessage, TASK_REQUIRED,
)
server = Server("task-demo")
server.experimental.enable_tasks()
@server.list_tools()
async def list_tools():
return [
Tool(
name="confirm_action",
description="Requires user confirmation",
inputSchema={"type": "object", "properties": {"action": {"type": "string"}}},
execution=ToolExecution(taskSupport=TASK_REQUIRED),
),
Tool(
name="generate_text",
description="Generate text via LLM",
inputSchema={"type": "object", "properties": {"prompt": {"type": "string"}}},
execution=ToolExecution(taskSupport=TASK_REQUIRED),
),
]
async def handle_confirm_action(arguments: dict) -> CreateTaskResult:
ctx = server.request_context
ctx.experimental.validate_task_mode(TASK_REQUIRED)
action = arguments.get("action", "unknown action")
async def work(task: ServerTaskContext) -> CallToolResult:
result = await task.elicit(
message=f"Confirm: {action}?",
requestedSchema={
"type": "object",
"properties": {"confirm": {"type": "boolean"}},
"required": ["confirm"],
},
)
if result.action == "accept" and result.content.get("confirm"):
return CallToolResult(content=[TextContent(type="text", text=f"Executed: {action}")])
return CallToolResult(content=[TextContent(type="text", text="Cancelled")])
return await ctx.experimental.run_task(work)
async def handle_generate_text(arguments: dict) -> CreateTaskResult:
ctx = server.request_context
ctx.experimental.validate_task_mode(TASK_REQUIRED)
prompt = arguments.get("prompt", "Hello")
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Generating...")
result = await task.create_message(
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=200,
)
text = result.content.text if isinstance(result.content, TextContent) else "Error"
return CallToolResult(content=[TextContent(type="text", text=text)])
return await ctx.experimental.run_task(work)
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTaskResult:
if name == "confirm_action":
return await handle_confirm_action(arguments)
elif name == "generate_text":
return await handle_generate_text(arguments)
return CallToolResult(content=[TextContent(type="text", text=f"Unknown: {name}")], isError=True)
```
## Error Handling in Tasks
Tasks handle errors automatically, but you can also fail explicitly:
```python
async def work(task: ServerTaskContext) -> CallToolResult:
try:
result = await risky_operation()
return CallToolResult(content=[TextContent(type="text", text=result)])
except PermissionError:
await task.fail("Access denied - insufficient permissions")
raise
except TimeoutError:
await task.fail("Operation timed out after 30 seconds")
raise
```
When `run_task()` catches an exception, it automatically:
1. Marks the task as `failed`
2. Sets `statusMessage` to the exception message
3. Propagates the exception (which is caught by the task group)
For custom error messages, call `task.fail()` before raising.
## HTTP Transport Example
For web applications, use the Streamable HTTP transport:
```python
import uvicorn
from mcp.server import Server
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.types import (
CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED,
)
server = Server("http-task-server")
server.experimental.enable_tasks()
@server.list_tools()
async def list_tools():
return [
Tool(
name="long_operation",
description="A long-running operation",
inputSchema={"type": "object", "properties": {"duration": {"type": "number"}}},
execution=ToolExecution(taskSupport=TASK_REQUIRED),
)
]
async def handle_long_operation(arguments: dict) -> CreateTaskResult:
ctx = server.request_context
ctx.experimental.validate_task_mode(TASK_REQUIRED)
duration = arguments.get("duration", 5)
async def work(task: ServerTaskContext) -> CallToolResult:
import anyio
for i in range(int(duration)):
await task.update_status(f"Step {i+1}/{int(duration)}")
await anyio.sleep(1)
return CallToolResult(content=[TextContent(type="text", text=f"Completed after {duration}s")])
return await ctx.experimental.run_task(work)
@server.call_tool()
async def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTaskResult:
if name == "long_operation":
return await handle_long_operation(arguments)
return CallToolResult(content=[TextContent(type="text", text=f"Unknown: {name}")], isError=True)
if __name__ == "__main__":
uvicorn.run(server.streamable_http_app(), host="127.0.0.1", port=8000)
```
## Testing Task Servers
Test task functionality with the SDK's testing utilities:
```python
import pytest
import anyio
from mcp.client.session import ClientSession
from mcp.types import CallToolResult
@pytest.mark.anyio
async def test_task_tool():
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream(10)
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream(10)
async def run_server():
await server.run(
client_to_server_receive,
server_to_client_send,
server.create_initialization_options(),
)
async def run_client():
async with ClientSession(server_to_client_receive, client_to_server_send) as session:
await session.initialize()
# Call the tool as a task
result = await session.experimental.call_tool_as_task("my_tool", {"arg": "value"})
task_id = result.task.taskId
assert result.task.status == "working"
# Poll until complete
async for status in session.experimental.poll_task(task_id):
if status.status in ("completed", "failed"):
break
# Get result
final = await session.experimental.get_task_result(task_id, CallToolResult)
assert len(final.content) > 0
async with anyio.create_task_group() as tg:
tg.start_soon(run_server)
tg.start_soon(run_client)
```
## Best Practices
### Keep Work Functions Focused
```python
# Good: focused work function
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Validating...")
validate_input(arguments)
await task.update_status("Processing...")
result = await process_data(arguments)
return CallToolResult(content=[TextContent(type="text", text=result)])
```
### Check Cancellation in Loops
```python
async def work(task: ServerTaskContext) -> CallToolResult:
results = []
for item in large_dataset:
if task.is_cancelled:
return CallToolResult(content=[TextContent(type="text", text="Cancelled")])
results.append(await process(item))
return CallToolResult(content=[TextContent(type="text", text=str(results))])
```
### Use Meaningful Status Messages
```python
async def work(task: ServerTaskContext) -> CallToolResult:
await task.update_status("Connecting to database...")
db = await connect()
await task.update_status("Fetching records (0/1000)...")
for i, record in enumerate(records):
if i % 100 == 0:
await task.update_status(f"Processing records ({i}/1000)...")
await process(record)
await task.update_status("Finalizing results...")
return CallToolResult(content=[TextContent(type="text", text="Done")])
```
### Handle Elicitation Responses
```python
async def work(task: ServerTaskContext) -> CallToolResult:
result = await task.elicit(message="Continue?", requestedSchema={...})
match result.action:
case "accept":
# User accepted, process content
return await process_accepted(result.content)
case "decline":
# User explicitly declined
return CallToolResult(content=[TextContent(type="text", text="User declined")])
case "cancel":
# User cancelled the elicitation
return CallToolResult(content=[TextContent(type="text", text="Cancelled")])
```
## Next Steps
- [Client Usage](tasks-client.md) - Learn how clients interact with task servers
- [Tasks Overview](tasks.md) - Review lifecycle and concepts

View file

@ -1,188 +0,0 @@
# Tasks
!!! warning "Experimental"
Tasks are an experimental feature tracking the draft MCP specification.
The API may change without notice.
Tasks enable asynchronous request handling in MCP. Instead of blocking until an operation completes, the receiver creates a task, returns immediately, and the requestor polls for the result.
## When to Use Tasks
Tasks are designed for operations that:
- Take significant time (seconds to minutes)
- Need progress updates during execution
- Require user input mid-execution (elicitation, sampling)
- Should run without blocking the requestor
Common use cases:
- Long-running data processing
- Multi-step workflows with user confirmation
- LLM-powered operations requiring sampling
- OAuth flows requiring user browser interaction
## Task Lifecycle
```text
┌─────────────┐
│ working │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌───────────┐ ┌───────────┐
│ completed │ │ failed │ │ cancelled │
└────────────┘ └───────────┘ └───────────┘
▲
│
┌────────┴────────┐
│ input_required │◄──────┐
└────────┬────────┘ │
│ │
└────────────────┘
```
| Status | Description |
|--------|-------------|
| `working` | Task is being processed |
| `input_required` | Receiver needs input from requestor (elicitation/sampling) |
| `completed` | Task finished successfully |
| `failed` | Task encountered an error |
| `cancelled` | Task was cancelled by requestor |
Terminal states (`completed`, `failed`, `cancelled`) are final—tasks cannot transition out of them.
## Bidirectional Flow
Tasks work in both directions:
**Client → Server** (most common):
```text
Client Server
│ │
│── tools/call (task) ──────────────>│ Creates task
│<── CreateTaskResult ───────────────│
│ │
│── tasks/get ──────────────────────>│
│<── status: working ────────────────│
│ │ ... work continues ...
│── tasks/get ──────────────────────>│
│<── status: completed ──────────────│
│ │
│── tasks/result ───────────────────>│
│<── CallToolResult ─────────────────│
```
**Server → Client** (for elicitation/sampling):
```text
Server Client
│ │
│── elicitation/create (task) ──────>│ Creates task
│<── CreateTaskResult ───────────────│
│ │
│── tasks/get ──────────────────────>│
│<── status: working ────────────────│
│ │ ... user interaction ...
│── tasks/get ──────────────────────>│
│<── status: completed ──────────────│
│ │
│── tasks/result ───────────────────>│
│<── ElicitResult ───────────────────│
```
## Key Concepts
### Task Metadata
When augmenting a request with task execution, include `TaskMetadata`:
```python
from mcp.types import TaskMetadata
task = TaskMetadata(ttl=60000) # TTL in milliseconds
```
The `ttl` (time-to-live) specifies how long the task and result are retained after completion.
### Task Store
Servers persist task state in a `TaskStore`. The SDK provides `InMemoryTaskStore` for development:
```python
from mcp.shared.experimental.tasks import InMemoryTaskStore
store = InMemoryTaskStore()
```
For production, implement `TaskStore` with a database or distributed cache.
### Capabilities
Both servers and clients declare task support through capabilities:
**Server capabilities:**
- `tasks.requests.tools.call` - Server accepts task-augmented tool calls
**Client capabilities:**
- `tasks.requests.sampling.createMessage` - Client accepts task-augmented sampling
- `tasks.requests.elicitation.create` - Client accepts task-augmented elicitation
The SDK manages these automatically when you enable task support.
## Quick Example
**Server** (simplified API):
```python
from mcp.server import Server
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.types import CallToolResult, TextContent, TASK_REQUIRED
server = Server("my-server")
server.experimental.enable_tasks() # One-line setup
@server.call_tool()
async def handle_tool(name: str, arguments: dict):
ctx = server.request_context
ctx.experimental.validate_task_mode(TASK_REQUIRED)
async def work(task: ServerTaskContext):
await task.update_status("Processing...")
# ... do work ...
return CallToolResult(content=[TextContent(type="text", text="Done!")])
return await ctx.experimental.run_task(work)
```
**Client:**
```python
from mcp.client.session import ClientSession
from mcp.types import CallToolResult
async with ClientSession(read, write) as session:
await session.initialize()
# Call tool as task
result = await session.experimental.call_tool_as_task("my_tool", {"arg": "value"})
task_id = result.task.taskId
# Poll until done
async for status in session.experimental.poll_task(task_id):
print(f"Status: {status.status}")
# Get result
final = await session.experimental.get_task_result(task_id, CallToolResult)
```
## Next Steps
- [Server Implementation](tasks-server.md) - Build task-supporting servers
- [Client Usage](tasks-client.md) - Call and poll tasks from clients

View file

@ -595,7 +595,7 @@ The `RequestContext` class has been split to separate shared fields from server-
**`RequestContext` changes:**
- Type parameters reduced from `RequestContext[SessionT, LifespanContextT, RequestT]` to `RequestContext[SessionT]`
- Server-specific fields (`lifespan_context`, `experimental`, `request`, `close_sse_stream`, `close_standalone_sse_stream`) moved to new `ServerRequestContext` class in `mcp.server.context`
- Server-specific fields (`lifespan_context`, `request`, `close_sse_stream`, `close_standalone_sse_stream`) moved to new `ServerRequestContext` class in `mcp.server.context`
**Before (v1):**
@ -861,7 +861,7 @@ server = Server("my-server", on_list_tools=handle_list_tools, on_call_tool=handl
**Key differences:**
- Handlers receive `(ctx, params)` instead of the full request object or unpacked arguments. `ctx` is a `ServerRequestContext` with `session`, `lifespan_context`, and `experimental` fields (plus `request_id`, `meta`, etc. for request handlers). `params` is the typed request params object.
- Handlers receive `(ctx, params)` instead of the full request object or unpacked arguments. `ctx` is a `ServerRequestContext` with `session` and `lifespan_context` fields (plus `request_id`, `meta`, etc. for request handlers). `params` is the typed request params object.
- Handlers return the full result type (e.g. `ListToolsResult`) rather than unwrapped values (e.g. `list[Tool]`).
- The automatic `jsonschema` input/output validation that the old `call_tool()` decorator performed has been removed. There is no built-in replacement — if you relied on schema validation in the lowlevel server, you will need to validate inputs yourself in your handler.
@ -872,7 +872,7 @@ All handlers receive `ctx: ServerRequestContext` as the first argument. The seco
| v1 decorator | v2 constructor kwarg | `params` type | return type |
|---|---|---|---|
| `@server.list_tools()` | `on_list_tools` | `PaginatedRequestParams \| None` | `ListToolsResult` |
| `@server.call_tool()` | `on_call_tool` | `CallToolRequestParams` | `CallToolResult \| CreateTaskResult` |
| `@server.call_tool()` | `on_call_tool` | `CallToolRequestParams` | `CallToolResult` |
| `@server.list_resources()` | `on_list_resources` | `PaginatedRequestParams \| None` | `ListResourcesResult` |
| `@server.list_resource_templates()` | `on_list_resource_templates` | `PaginatedRequestParams \| None` | `ListResourceTemplatesResult` |
| `@server.read_resource()` | `on_read_resource` | `ReadResourceRequestParams` | `ReadResourceResult` |
@ -1039,37 +1039,11 @@ from mcp.server import ServerRequestContext
# but None in notification handlers
```
### Experimental: task handler decorators removed
### Experimental Tasks support removed
The experimental decorator methods on `ExperimentalHandlers` (`@server.experimental.list_tasks()`, `@server.experimental.get_task()`, etc.) have been removed.
Tasks (SEP-1686) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with all `Task*` types, the `tasks` capability fields, `Tool.execution`, and the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`.
Default task handlers are still registered automatically via `server.experimental.enable_tasks()`. Custom handlers can be passed as `on_*` kwargs to override specific defaults.
**Before (v1):**
```python
server = Server("my-server")
server.experimental.enable_tasks()
@server.experimental.get_task()
async def custom_get_task(request: GetTaskRequest) -> GetTaskResult:
...
```
**After (v2):**
```python
from mcp.server import Server, ServerRequestContext
from mcp.types import GetTaskRequestParams, GetTaskResult
async def custom_get_task(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult:
...
server = Server("my-server")
server.experimental.enable_tasks(on_get_task=custom_get_task)
```
Tasks are expected to return as a separate MCP extension in a future release.
## Deprecations

View file

@ -1,43 +0,0 @@
# Simple Task Client
A minimal MCP client demonstrating polling for task results over streamable HTTP.
## Running
First, start the simple-task server in another terminal:
```bash
cd examples/servers/simple-task
uv run mcp-simple-task
```
Then run the client:
```bash
cd examples/clients/simple-task-client
uv run mcp-simple-task-client
```
Use `--url` to connect to a different server.
## What it does
1. Connects to the server via streamable HTTP
2. Calls the `long_running_task` tool as a task
3. Polls the task status until completion
4. Retrieves and prints the result
## Expected output
```text
Available tools: ['long_running_task']
Calling tool as a task...
Task created: <task-id>
Status: working - Starting work...
Status: working - Processing step 1...
Status: working - Processing step 2...
Status: completed -
Result: Task completed!
```

View file

@ -1,5 +0,0 @@
import sys
from .main import main
sys.exit(main()) # type: ignore[call-arg]

View file

@ -1,56 +0,0 @@
"""Simple task client demonstrating MCP tasks polling over streamable HTTP."""
import asyncio
import click
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
from mcp.types import CallToolResult, TextContent
async def run(url: str) -> None:
async with streamable_http_client(url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# List tools
tools = await session.list_tools()
print(f"Available tools: {[t.name for t in tools.tools]}")
# Call the tool as a task
print("\nCalling tool as a task...")
result = await session.experimental.call_tool_as_task(
"long_running_task",
arguments={},
ttl=60000,
)
task_id = result.task.task_id
print(f"Task created: {task_id}")
status = None
# Poll until done (respects server's pollInterval hint)
async for status in session.experimental.poll_task(task_id):
print(f" Status: {status.status} - {status.status_message or ''}")
# Check final status
if status and status.status != "completed":
print(f"Task ended with status: {status.status}")
return
# Get the result
task_result = await session.experimental.get_task_result(task_id, CallToolResult)
content = task_result.content[0]
if isinstance(content, TextContent):
print(f"\nResult: {content.text}")
@click.command()
@click.option("--url", default="http://localhost:8000/mcp", help="Server URL")
def main(url: str) -> int:
asyncio.run(run(url))
return 0
if __name__ == "__main__":
main()

View file

@ -1,43 +0,0 @@
[project]
name = "mcp-simple-task-client"
version = "0.1.0"
description = "A simple MCP client demonstrating task polling"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "tasks", "client"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["click>=8.0", "mcp"]
[project.scripts]
mcp-simple-task-client = "mcp_simple_task_client.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_task_client"]
[tool.pyright]
include = ["mcp_simple_task_client"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "ruff>=0.6.9"]

View file

@ -1,87 +0,0 @@
# Simple Interactive Task Client
A minimal MCP client demonstrating responses to interactive tasks (elicitation and sampling).
## Running
First, start the interactive task server in another terminal:
```bash
cd examples/servers/simple-task-interactive
uv run mcp-simple-task-interactive
```
Then run the client:
```bash
cd examples/clients/simple-task-interactive-client
uv run mcp-simple-task-interactive-client
```
Use `--url` to connect to a different server.
## What it does
1. Connects to the server via streamable HTTP
2. Calls `confirm_delete` - server asks for confirmation, client responds via terminal
3. Calls `write_haiku` - server requests LLM completion, client returns a hardcoded haiku
## Key concepts
### Elicitation callback
```python
async def elicitation_callback(context, params) -> ElicitResult:
# Handle user input request from server
return ElicitResult(action="accept", content={"confirm": True})
```
### Sampling callback
```python
async def sampling_callback(context, params) -> CreateMessageResult:
# Handle LLM completion request from server
return CreateMessageResult(model="...", role="assistant", content=...)
```
### Using call_tool_as_task
```python
# Call a tool as a task (returns immediately with task reference)
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
task_id = result.task.task_id
# Get result - this delivers elicitation/sampling requests and blocks until complete
final = await session.experimental.get_task_result(task_id, CallToolResult)
```
**Important**: The `get_task_result()` call is what triggers the delivery of elicitation
and sampling requests to your callbacks. It blocks until the task completes and returns
the final result.
## Expected output
```text
Available tools: ['confirm_delete', 'write_haiku']
--- Demo 1: Elicitation ---
Calling confirm_delete tool...
Task created: <task-id>
[Elicitation] Server asks: Are you sure you want to delete 'important.txt'?
Your response (y/n): y
[Elicitation] Responding with: confirm=True
Result: Deleted 'important.txt'
--- Demo 2: Sampling ---
Calling write_haiku tool...
Task created: <task-id>
[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves
[Sampling] Responding with haiku
Result:
Haiku:
Cherry blossoms fall
Softly on the quiet pond
Spring whispers goodbye
```

View file

@ -1,5 +0,0 @@
import sys
from .main import main
sys.exit(main()) # type: ignore[call-arg]

View file

@ -1,137 +0,0 @@
"""Simple interactive task client demonstrating elicitation and sampling responses.
This example demonstrates the spec-compliant polling pattern:
1. Poll tasks/get watching for status changes
2. On input_required, call tasks/result to receive elicitation/sampling requests
3. Continue until terminal status, then retrieve final result
"""
import asyncio
import click
from mcp import ClientSession
from mcp.client.context import ClientRequestContext
from mcp.client.streamable_http import streamable_http_client
from mcp.types import (
CallToolResult,
CreateMessageRequestParams,
CreateMessageResult,
ElicitRequestParams,
ElicitResult,
TextContent,
)
async def elicitation_callback(
context: ClientRequestContext,
params: ElicitRequestParams,
) -> ElicitResult:
"""Handle elicitation requests from the server."""
print(f"\n[Elicitation] Server asks: {params.message}")
# Simple terminal prompt
response = input("Your response (y/n): ").strip().lower()
confirmed = response in ("y", "yes", "true", "1")
print(f"[Elicitation] Responding with: confirm={confirmed}")
return ElicitResult(action="accept", content={"confirm": confirmed})
async def sampling_callback(
context: ClientRequestContext,
params: CreateMessageRequestParams,
) -> CreateMessageResult:
"""Handle sampling requests from the server."""
# Get the prompt from the first message
prompt = "unknown"
if params.messages:
content = params.messages[0].content
if isinstance(content, TextContent):
prompt = content.text
print(f"\n[Sampling] Server requests LLM completion for: {prompt}")
# Return a hardcoded haiku (in real use, call your LLM here)
haiku = """Cherry blossoms fall
Softly on the quiet pond
Spring whispers goodbye"""
print("[Sampling] Responding with haiku")
return CreateMessageResult(
model="mock-haiku-model",
role="assistant",
content=TextContent(type="text", text=haiku),
)
def get_text(result: CallToolResult) -> str:
"""Extract text from a CallToolResult."""
if result.content and isinstance(result.content[0], TextContent):
return result.content[0].text
return "(no text)"
async def run(url: str) -> None:
async with streamable_http_client(url) as (read, write):
async with ClientSession(
read,
write,
elicitation_callback=elicitation_callback,
sampling_callback=sampling_callback,
) as session:
await session.initialize()
# List tools
tools = await session.list_tools()
print(f"Available tools: {[t.name for t in tools.tools]}")
# Demo 1: Elicitation (confirm_delete)
print("\n--- Demo 1: Elicitation ---")
print("Calling confirm_delete tool...")
elicit_task = await session.experimental.call_tool_as_task("confirm_delete", {"filename": "important.txt"})
elicit_task_id = elicit_task.task.task_id
print(f"Task created: {elicit_task_id}")
# Poll until terminal, calling tasks/result on input_required
async for status in session.experimental.poll_task(elicit_task_id):
print(f"[Poll] Status: {status.status}")
if status.status == "input_required":
# Server needs input - tasks/result delivers the elicitation request
elicit_result = await session.experimental.get_task_result(elicit_task_id, CallToolResult)
break
else:
# poll_task exited due to terminal status
elicit_result = await session.experimental.get_task_result(elicit_task_id, CallToolResult)
print(f"Result: {get_text(elicit_result)}")
# Demo 2: Sampling (write_haiku)
print("\n--- Demo 2: Sampling ---")
print("Calling write_haiku tool...")
sampling_task = await session.experimental.call_tool_as_task("write_haiku", {"topic": "autumn leaves"})
sampling_task_id = sampling_task.task.task_id
print(f"Task created: {sampling_task_id}")
# Poll until terminal, calling tasks/result on input_required
async for status in session.experimental.poll_task(sampling_task_id):
print(f"[Poll] Status: {status.status}")
if status.status == "input_required":
sampling_result = await session.experimental.get_task_result(sampling_task_id, CallToolResult)
break
else:
sampling_result = await session.experimental.get_task_result(sampling_task_id, CallToolResult)
print(f"Result:\n{get_text(sampling_result)}")
@click.command()
@click.option("--url", default="http://localhost:8000/mcp", help="Server URL")
def main(url: str) -> int:
asyncio.run(run(url))
return 0
if __name__ == "__main__":
main()

View file

@ -1,43 +0,0 @@
[project]
name = "mcp-simple-task-interactive-client"
version = "0.1.0"
description = "A simple MCP client demonstrating interactive task responses"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
keywords = ["mcp", "llm", "tasks", "client", "elicitation", "sampling"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = ["click>=8.0", "mcp"]
[project.scripts]
mcp-simple-task-interactive-client = "mcp_simple_task_interactive_client.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_task_interactive_client"]
[tool.pyright]
include = ["mcp_simple_task_interactive_client"]
venvPath = "."
venv = ".venv"
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = []
[tool.ruff]
line-length = 120
target-version = "py310"
[dependency-groups]
dev = ["pyright>=1.1.378", "ruff>=0.6.9"]

View file

@ -1,74 +0,0 @@
# Simple Interactive Task Server
A minimal MCP server demonstrating interactive tasks with elicitation and sampling.
## Running
```bash
cd examples/servers/simple-task-interactive
uv run mcp-simple-task-interactive
```
The server starts on `http://localhost:8000/mcp` by default. Use `--port` to change.
## What it does
This server exposes two tools:
### `confirm_delete` (demonstrates elicitation)
Asks the user for confirmation before "deleting" a file.
- Uses `task.elicit()` to request user input
- Shows the elicitation flow: task -> input_required -> response -> complete
### `write_haiku` (demonstrates sampling)
Asks the LLM to write a haiku about a topic.
- Uses `task.create_message()` to request LLM completion
- Shows the sampling flow: task -> input_required -> response -> complete
## Usage with the client
In one terminal, start the server:
```bash
cd examples/servers/simple-task-interactive
uv run mcp-simple-task-interactive
```
In another terminal, run the interactive client:
```bash
cd examples/clients/simple-task-interactive-client
uv run mcp-simple-task-interactive-client
```
## Expected server output
When a client connects and calls the tools, you'll see:
```text
Starting server on http://localhost:8000/mcp
[Server] confirm_delete called for 'important.txt'
[Server] Task created: <task-id>
[Server] Sending elicitation request to client...
[Server] Received elicitation response: action=accept, content={'confirm': True}
[Server] Completing task with result: Deleted 'important.txt'
[Server] write_haiku called for topic 'autumn leaves'
[Server] Task created: <task-id>
[Server] Sending sampling request to client...
[Server] Received sampling response: Cherry blossoms fall
Softly on the quiet pon...
[Server] Completing task with haiku
```
## Key concepts
1. **ServerTaskContext**: Provides `elicit()` and `create_message()` for user interaction
2. **run_task()**: Spawns background work, auto-completes/fails, returns immediately
3. **TaskResultHandler**: Delivers queued messages and routes responses
4. **Response routing**: Responses are routed back to waiting resolvers

View file

@ -1,5 +0,0 @@
import sys
from .server import main
sys.exit(main()) # type: ignore[call-arg]

View file

@ -1,138 +0,0 @@
"""Simple interactive task server demonstrating elicitation and sampling.
This example shows the simplified task API where:
- server.experimental.enable_tasks() sets up all infrastructure
- ctx.experimental.run_task() handles task lifecycle automatically
- ServerTaskContext.elicit() and ServerTaskContext.create_message() queue requests properly
"""
from typing import Any
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.server.experimental.task_context import ServerTaskContext
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="confirm_delete",
description="Asks for confirmation before deleting (demonstrates elicitation)",
input_schema={
"type": "object",
"properties": {"filename": {"type": "string"}},
},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
),
types.Tool(
name="write_haiku",
description="Asks LLM to write a haiku (demonstrates sampling)",
input_schema={"type": "object", "properties": {"topic": {"type": "string"}}},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
),
]
)
async def handle_confirm_delete(ctx: ServerRequestContext, arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the confirm_delete tool - demonstrates elicitation."""
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
filename = arguments.get("filename", "unknown.txt")
print(f"\n[Server] confirm_delete called for '{filename}'")
async def work(task: ServerTaskContext) -> types.CallToolResult:
print(f"[Server] Task {task.task_id} starting elicitation...")
result = await task.elicit(
message=f"Are you sure you want to delete '{filename}'?",
requested_schema={
"type": "object",
"properties": {"confirm": {"type": "boolean"}},
"required": ["confirm"],
},
)
print(f"[Server] Received elicitation response: action={result.action}, content={result.content}")
if result.action == "accept" and result.content:
confirmed = result.content.get("confirm", False)
text = f"Deleted '{filename}'" if confirmed else "Deletion cancelled"
else:
text = "Deletion cancelled"
print(f"[Server] Completing task with result: {text}")
return types.CallToolResult(content=[types.TextContent(type="text", text=text)])
return await ctx.experimental.run_task(work)
async def handle_write_haiku(ctx: ServerRequestContext, arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the write_haiku tool - demonstrates sampling."""
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
topic = arguments.get("topic", "nature")
print(f"\n[Server] write_haiku called for topic '{topic}'")
async def work(task: ServerTaskContext) -> types.CallToolResult:
print(f"[Server] Task {task.task_id} starting sampling...")
result = await task.create_message(
messages=[
types.SamplingMessage(
role="user",
content=types.TextContent(type="text", text=f"Write a haiku about {topic}"),
)
],
max_tokens=50,
)
haiku = "No response"
if isinstance(result.content, types.TextContent):
haiku = result.content.text
print(f"[Server] Received sampling response: {haiku[:50]}...")
return types.CallToolResult(content=[types.TextContent(type="text", text=f"Haiku:\n{haiku}")])
return await ctx.experimental.run_task(work)
async def handle_call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
arguments = params.arguments or {}
if params.name == "confirm_delete":
return await handle_confirm_delete(ctx, arguments)
elif params.name == "write_haiku":
return await handle_write_haiku(ctx, arguments)
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")],
is_error=True,
)
server = Server(
"simple-task-interactive",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# Enable task support - this auto-registers all handlers
server.experimental.enable_tasks()
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
def main(port: int) -> int:
starlette_app = server.streamable_http_app()
print(f"Starting server on http://localhost:{port}/mcp")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0

Some files were not shown because too many files have changed in this diff Show more