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
20 changed files with 63 additions and 12963 deletions

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

View file

@ -1,43 +0,0 @@
[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 = "Model Context Protocol a Series of LF Projects, LLC." }]
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

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

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

View file

@ -1,70 +0,0 @@
"""Simple task server demonstrating MCP tasks over streamable HTTP."""
import anyio
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="long_running_task",
description="A task that takes a few seconds to complete with status updates",
input_schema={"type": "object", "properties": {}},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
)
]
)
async def handle_call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
if params.name == "long_running_task":
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)
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")],
is_error=True,
)
server = Server(
"simple-task-server",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# One-line setup: auto-registers get_task, get_task_result, list_tasks, cancel_task
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

View file

@ -1,43 +0,0 @@
[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 = "Model Context Protocol a Series of LF Projects, LLC." }]
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"]