Skip to content

Commit 8460a5f

Browse files
committed
Add interactive task examples and move call_tool_as_task to experimental
This commit adds working examples for the Tasks SEP demonstrating elicitation and sampling flows, along with supporting infrastructure changes. Examples: - simple-task-interactive server: Exposes confirm_delete (elicitation) and write_haiku (sampling) tools that run as tasks - simple-task-interactive-client: Connects to server, handles callbacks, and demonstrates the correct task result retrieval pattern Key changes: - Move call_tool_as_task() from ClientSession to session.experimental.call_tool_as_task() for API consistency - Add comprehensive tests mirroring the example patterns - Add server-side print outputs for visibility into task execution The critical insight: clients must call get_task_result() to receive elicitation/sampling requests - simply polling get_task() will not trigger the callbacks.
1 parent 77155fc commit 8460a5f

File tree

26 files changed

+3006
-66
lines changed

26 files changed

+3006
-66
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Simple Interactive Task Client
2+
3+
A minimal MCP client demonstrating responses to interactive tasks (elicitation and sampling).
4+
5+
## Running
6+
7+
First, start the interactive task server in another terminal:
8+
9+
```bash
10+
cd examples/servers/simple-task-interactive
11+
uv run mcp-simple-task-interactive
12+
```
13+
14+
Then run the client:
15+
16+
```bash
17+
cd examples/clients/simple-task-interactive-client
18+
uv run mcp-simple-task-interactive-client
19+
```
20+
21+
Use `--url` to connect to a different server.
22+
23+
## What it does
24+
25+
1. Connects to the server via streamable HTTP
26+
2. Calls `confirm_delete` - server asks for confirmation, client responds via terminal
27+
3. Calls `write_haiku` - server requests LLM completion, client returns a hardcoded haiku
28+
29+
## Key concepts
30+
31+
### Elicitation callback
32+
33+
```python
34+
async def elicitation_callback(context, params) -> ElicitResult:
35+
# Handle user input request from server
36+
return ElicitResult(action="accept", content={"confirm": True})
37+
```
38+
39+
### Sampling callback
40+
41+
```python
42+
async def sampling_callback(context, params) -> CreateMessageResult:
43+
# Handle LLM completion request from server
44+
return CreateMessageResult(model="...", role="assistant", content=...)
45+
```
46+
47+
### Using call_tool_as_task
48+
49+
```python
50+
# Call a tool as a task (returns immediately with task reference)
51+
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
52+
task_id = result.task.taskId
53+
54+
# Get result - this delivers elicitation/sampling requests and blocks until complete
55+
final = await session.experimental.get_task_result(task_id, CallToolResult)
56+
```
57+
58+
**Important**: The `get_task_result()` call is what triggers the delivery of elicitation
59+
and sampling requests to your callbacks. It blocks until the task completes and returns
60+
the final result.
61+
62+
## Expected output
63+
64+
```text
65+
Available tools: ['confirm_delete', 'write_haiku']
66+
67+
--- Demo 1: Elicitation ---
68+
Calling confirm_delete tool...
69+
Task created: <task-id>
70+
71+
[Elicitation] Server asks: Are you sure you want to delete 'important.txt'?
72+
Your response (y/n): y
73+
[Elicitation] Responding with: confirm=True
74+
Result: Deleted 'important.txt'
75+
76+
--- Demo 2: Sampling ---
77+
Calling write_haiku tool...
78+
Task created: <task-id>
79+
80+
[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves
81+
[Sampling] Responding with haiku
82+
Result:
83+
Haiku:
84+
Cherry blossoms fall
85+
Softly on the quiet pond
86+
Spring whispers goodbye
87+
```

examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import sys
2+
3+
from .main import main
4+
5+
sys.exit(main()) # type: ignore[call-arg]
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Simple interactive task client demonstrating elicitation and sampling responses."""
2+
3+
import asyncio
4+
from typing import Any
5+
6+
import click
7+
from mcp import ClientSession
8+
from mcp.client.streamable_http import streamablehttp_client
9+
from mcp.shared.context import RequestContext
10+
from mcp.types import (
11+
CallToolResult,
12+
CreateMessageRequestParams,
13+
CreateMessageResult,
14+
ElicitRequestParams,
15+
ElicitResult,
16+
TextContent,
17+
)
18+
19+
20+
async def elicitation_callback(
21+
context: RequestContext[ClientSession, Any],
22+
params: ElicitRequestParams,
23+
) -> ElicitResult:
24+
"""Handle elicitation requests from the server."""
25+
print(f"\n[Elicitation] Server asks: {params.message}")
26+
27+
# Simple terminal prompt
28+
response = input("Your response (y/n): ").strip().lower()
29+
confirmed = response in ("y", "yes", "true", "1")
30+
31+
print(f"[Elicitation] Responding with: confirm={confirmed}")
32+
return ElicitResult(action="accept", content={"confirm": confirmed})
33+
34+
35+
async def sampling_callback(
36+
context: RequestContext[ClientSession, Any],
37+
params: CreateMessageRequestParams,
38+
) -> CreateMessageResult:
39+
"""Handle sampling requests from the server."""
40+
# Get the prompt from the first message
41+
prompt = "unknown"
42+
if params.messages:
43+
content = params.messages[0].content
44+
if isinstance(content, TextContent):
45+
prompt = content.text
46+
47+
print(f"\n[Sampling] Server requests LLM completion for: {prompt}")
48+
49+
# Return a hardcoded haiku (in real use, call your LLM here)
50+
haiku = """Cherry blossoms fall
51+
Softly on the quiet pond
52+
Spring whispers goodbye"""
53+
54+
print("[Sampling] Responding with haiku")
55+
return CreateMessageResult(
56+
model="mock-haiku-model",
57+
role="assistant",
58+
content=TextContent(type="text", text=haiku),
59+
)
60+
61+
62+
def get_text(result: CallToolResult) -> str:
63+
"""Extract text from a CallToolResult."""
64+
if result.content and isinstance(result.content[0], TextContent):
65+
return result.content[0].text
66+
return "(no text)"
67+
68+
69+
async def run(url: str) -> None:
70+
async with streamablehttp_client(url) as (read, write, _):
71+
async with ClientSession(
72+
read,
73+
write,
74+
elicitation_callback=elicitation_callback,
75+
sampling_callback=sampling_callback,
76+
) as session:
77+
await session.initialize()
78+
79+
# List tools
80+
tools = await session.list_tools()
81+
print(f"Available tools: {[t.name for t in tools.tools]}")
82+
83+
# Demo 1: Elicitation (confirm_delete)
84+
print("\n--- Demo 1: Elicitation ---")
85+
print("Calling confirm_delete tool...")
86+
87+
result = await session.experimental.call_tool_as_task("confirm_delete", {"filename": "important.txt"})
88+
task_id = result.task.taskId
89+
print(f"Task created: {task_id}")
90+
91+
# get_task_result() delivers elicitation requests and blocks until complete
92+
final = await session.experimental.get_task_result(task_id, CallToolResult)
93+
print(f"Result: {get_text(final)}")
94+
95+
# Demo 2: Sampling (write_haiku)
96+
print("\n--- Demo 2: Sampling ---")
97+
print("Calling write_haiku tool...")
98+
99+
result = await session.experimental.call_tool_as_task("write_haiku", {"topic": "autumn leaves"})
100+
task_id = result.task.taskId
101+
print(f"Task created: {task_id}")
102+
103+
# get_task_result() delivers sampling requests and blocks until complete
104+
final = await session.experimental.get_task_result(task_id, CallToolResult)
105+
print(f"Result:\n{get_text(final)}")
106+
107+
108+
@click.command()
109+
@click.option("--url", default="http://localhost:8000/mcp", help="Server URL")
110+
def main(url: str) -> int:
111+
asyncio.run(run(url))
112+
return 0
113+
114+
115+
if __name__ == "__main__":
116+
main()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[project]
2+
name = "mcp-simple-task-interactive-client"
3+
version = "0.1.0"
4+
description = "A simple MCP client demonstrating interactive task responses"
5+
readme = "README.md"
6+
requires-python = ">=3.10"
7+
authors = [{ name = "Anthropic, PBC." }]
8+
keywords = ["mcp", "llm", "tasks", "client", "elicitation", "sampling"]
9+
license = { text = "MIT" }
10+
classifiers = [
11+
"Development Status :: 4 - Beta",
12+
"Intended Audience :: Developers",
13+
"License :: OSI Approved :: MIT License",
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.10",
16+
]
17+
dependencies = ["click>=8.0", "mcp"]
18+
19+
[project.scripts]
20+
mcp-simple-task-interactive-client = "mcp_simple_task_interactive_client.main:main"
21+
22+
[build-system]
23+
requires = ["hatchling"]
24+
build-backend = "hatchling.build"
25+
26+
[tool.hatch.build.targets.wheel]
27+
packages = ["mcp_simple_task_interactive_client"]
28+
29+
[tool.pyright]
30+
include = ["mcp_simple_task_interactive_client"]
31+
venvPath = "."
32+
venv = ".venv"
33+
34+
[tool.ruff.lint]
35+
select = ["E", "F", "I"]
36+
ignore = []
37+
38+
[tool.ruff]
39+
line-length = 120
40+
target-version = "py310"
41+
42+
[dependency-groups]
43+
dev = ["pyright>=1.1.378", "ruff>=0.6.9"]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Simple Interactive Task Server
2+
3+
A minimal MCP server demonstrating interactive tasks with elicitation and sampling.
4+
5+
## Running
6+
7+
```bash
8+
cd examples/servers/simple-task-interactive
9+
uv run mcp-simple-task-interactive
10+
```
11+
12+
The server starts on `http://localhost:8000/mcp` by default. Use `--port` to change.
13+
14+
## What it does
15+
16+
This server exposes two tools:
17+
18+
### `confirm_delete` (demonstrates elicitation)
19+
20+
Asks the user for confirmation before "deleting" a file.
21+
22+
- Uses `TaskSession.elicit()` to request user input
23+
- Shows the elicitation flow: task -> input_required -> response -> complete
24+
25+
### `write_haiku` (demonstrates sampling)
26+
27+
Asks the LLM to write a haiku about a topic.
28+
29+
- Uses `TaskSession.create_message()` to request LLM completion
30+
- Shows the sampling flow: task -> input_required -> response -> complete
31+
32+
## Usage with the client
33+
34+
In one terminal, start the server:
35+
36+
```bash
37+
cd examples/servers/simple-task-interactive
38+
uv run mcp-simple-task-interactive
39+
```
40+
41+
In another terminal, run the interactive client:
42+
43+
```bash
44+
cd examples/clients/simple-task-interactive-client
45+
uv run mcp-simple-task-interactive-client
46+
```
47+
48+
## Expected server output
49+
50+
When a client connects and calls the tools, you'll see:
51+
52+
```text
53+
Starting server on http://localhost:8000/mcp
54+
55+
[Server] confirm_delete called for 'important.txt'
56+
[Server] Task created: <task-id>
57+
[Server] Sending elicitation request to client...
58+
[Server] Received elicitation response: action=accept, content={'confirm': True}
59+
[Server] Completing task with result: Deleted 'important.txt'
60+
61+
[Server] write_haiku called for topic 'autumn leaves'
62+
[Server] Task created: <task-id>
63+
[Server] Sending sampling request to client...
64+
[Server] Received sampling response: Cherry blossoms fall
65+
Softly on the quiet pon...
66+
[Server] Completing task with haiku
67+
```
68+
69+
## Key concepts
70+
71+
1. **TaskSession**: Wraps ServerSession to enqueue elicitation/sampling requests
72+
2. **TaskResultHandler**: Delivers queued messages and routes responses
73+
3. **task_execution()**: Context manager for safe task execution with auto-fail
74+
4. **Response routing**: Responses are routed back to waiting resolvers

examples/servers/simple-task-interactive/mcp_simple_task_interactive/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import sys
2+
3+
from .server import main
4+
5+
sys.exit(main()) # type: ignore[call-arg]

0 commit comments

Comments
 (0)