Remove Tasks (SEP-1686) from the SDK (#2714)
This commit is contained in:
parent
a5b2ebb660
commit
8cc187fac0
81 changed files with 63 additions and 12963 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import sys
|
||||
|
||||
from .main import main
|
||||
|
||||
sys.exit(main()) # type: ignore[call-arg]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import sys
|
||||
|
||||
from .main import main
|
||||
|
||||
sys.exit(main()) # type: ignore[call-arg]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import sys
|
||||
|
||||
from .server import main
|
||||
|
||||
sys.exit(main()) # type: ignore[call-arg]
|
||||
|
|
@ -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
Loading…
Reference in a new issue