SEP-1686: Tasks (#1645)

This commit is contained in:
Max Isbey 2025-11-28 18:51:58 +00:00 • committed by GitHub
commit c92bb2f7ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 14230 additions and 120 deletions

View file

@ -0,0 +1,74 @@
# 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

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

View file

@ -0,0 +1,147 @@
"""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 collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
import click
import mcp.types as types
import uvicorn
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
server = Server("simple-task-interactive")
# Enable task support - this auto-registers all handlers
server.experimental.enable_tasks()
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="confirm_delete",
description="Asks for confirmation before deleting (demonstrates elicitation)",
inputSchema={
"type": "object",
"properties": {"filename": {"type": "string"}},
},
execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED),
),
types.Tool(
name="write_haiku",
description="Asks LLM to write a haiku (demonstrates sampling)",
inputSchema={"type": "object", "properties": {"topic": {"type": "string"}}},
execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED),
),
]
async def handle_confirm_delete(arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the confirm_delete tool - demonstrates elicitation."""
ctx = server.request_context
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}'?",
requestedSchema={
"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(arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the write_haiku tool - demonstrates sampling."""
ctx = server.request_context
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)
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
if name == "confirm_delete":
return await handle_confirm_delete(arguments)
elif name == "write_haiku":
return await handle_write_haiku(arguments)
else:
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
isError=True,
)
def create_app(session_manager: StreamableHTTPSessionManager) -> Starlette:
@asynccontextmanager
async def app_lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield
return Starlette(
routes=[Mount("/mcp", app=session_manager.handle_request)],
lifespan=app_lifespan,
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
def main(port: int) -> int:
session_manager = StreamableHTTPSessionManager(app=server)
starlette_app = create_app(session_manager)
print(f"Starting server on http://localhost:{port}/mcp")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0

View file

@ -0,0 +1,43 @@
[project]
name = "mcp-simple-task-interactive"
version = "0.1.0"
description = "A simple MCP server demonstrating interactive tasks (elicitation & sampling)"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Anthropic, PBC." }]
keywords = ["mcp", "llm", "tasks", "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 = ["anyio>=4.5", "click>=8.0", "mcp", "starlette", "uvicorn"]
[project.scripts]
mcp-simple-task-interactive = "mcp_simple_task_interactive.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_task_interactive"]
[tool.pyright]
include = ["mcp_simple_task_interactive"]
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

@ -0,0 +1,37 @@
# Simple Task Server
A minimal MCP server demonstrating the experimental tasks feature over streamable HTTP.
## Running
```bash
cd examples/servers/simple-task
uv run mcp-simple-task
```
The server starts on `http://localhost:8000/mcp` by default. Use `--port` to change.
## What it does
This server exposes a single tool `long_running_task` that:
1. Must be called as a task (with `task` metadata in the request)
2. Takes ~3 seconds to complete
3. Sends status updates during execution
4. Returns a result when complete
## Usage with the client
In one terminal, start the server:
```bash
cd examples/servers/simple-task
uv run mcp-simple-task
```
In another terminal, run the client:
```bash
cd examples/clients/simple-task-client
uv run mcp-simple-task-client
```

View file

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

View file

@ -0,0 +1,84 @@
"""Simple task server demonstrating MCP tasks over streamable HTTP."""
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
import anyio
import click
import mcp.types as types
import uvicorn
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
server = Server("simple-task-server")
# One-line setup: auto-registers get_task, get_task_result, list_tasks, cancel_task
server.experimental.enable_tasks()
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="long_running_task",
description="A task that takes a few seconds to complete with status updates",
inputSchema={"type": "object", "properties": {}},
execution=types.ToolExecution(taskSupport=types.TASK_REQUIRED),
)
]
async def handle_long_running_task(arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the long_running_task tool - demonstrates status updates."""
ctx = server.request_context
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
async def work(task: ServerTaskContext) -> types.CallToolResult:
await task.update_status("Starting work...")
await anyio.sleep(1)
await task.update_status("Processing step 1...")
await anyio.sleep(1)
await task.update_status("Processing step 2...")
await anyio.sleep(1)
return types.CallToolResult(content=[types.TextContent(type="text", text="Task completed!")])
return await ctx.experimental.run_task(work)
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
if name == "long_running_task":
return await handle_long_running_task(arguments)
else:
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
isError=True,
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
def main(port: int) -> int:
session_manager = StreamableHTTPSessionManager(app=server)
@asynccontextmanager
async def app_lifespan(app: Starlette) -> AsyncIterator[None]:
async with session_manager.run():
yield
starlette_app = Starlette(
routes=[Mount("/mcp", app=session_manager.handle_request)],
lifespan=app_lifespan,
)
print(f"Starting server on http://localhost:{port}/mcp")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0

View file

@ -0,0 +1,43 @@
[project]
name = "mcp-simple-task"
version = "0.1.0"
description = "A simple MCP server demonstrating tasks"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Anthropic, PBC." }]
keywords = ["mcp", "llm", "tasks"]
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 = ["anyio>=4.5", "click>=8.0", "mcp", "starlette", "uvicorn"]
[project.scripts]
mcp-simple-task = "mcp_simple_task.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["mcp_simple_task"]
[tool.pyright]
include = ["mcp_simple_task"]
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"]