SEP-1686: Tasks (#1645)
This commit is contained in:
parent
5983a650cc
commit
c92bb2f7ff
10 changed files with 14230 additions and 120 deletions
43
examples/clients/simple-task-client/README.md
Normal file
43
examples/clients/simple-task-client/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# 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!
|
||||
```
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import sys
|
||||
|
||||
from .main import main
|
||||
|
||||
sys.exit(main()) # type: ignore[call-arg]
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
"""Simple task client demonstrating MCP tasks polling over streamable HTTP."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import click
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
|
||||
async def run(url: str) -> None:
|
||||
async with streamablehttp_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.taskId
|
||||
print(f"Task created: {task_id}")
|
||||
|
||||
# Poll until done (respects server's pollInterval hint)
|
||||
async for status in session.experimental.poll_task(task_id):
|
||||
print(f" Status: {status.status} - {status.statusMessage or ''}")
|
||||
|
||||
# Check final status
|
||||
if 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()
|
||||
43
examples/clients/simple-task-client/pyproject.toml
Normal file
43
examples/clients/simple-task-client/pyproject.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[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 = "Anthropic, PBC." }]
|
||||
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"]
|
||||
87
examples/clients/simple-task-interactive-client/README.md
Normal file
87
examples/clients/simple-task-interactive-client/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# 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.taskId
|
||||
|
||||
# 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
|
||||
```
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import sys
|
||||
|
||||
from .main import main
|
||||
|
||||
sys.exit(main()) # type: ignore[call-arg]
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
"""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
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.types import (
|
||||
CallToolResult,
|
||||
CreateMessageRequestParams,
|
||||
CreateMessageResult,
|
||||
ElicitRequestParams,
|
||||
ElicitResult,
|
||||
TextContent,
|
||||
)
|
||||
|
||||
|
||||
async def elicitation_callback(
|
||||
context: RequestContext[ClientSession, Any],
|
||||
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: RequestContext[ClientSession, Any],
|
||||
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 streamablehttp_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.taskId
|
||||
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.taskId
|
||||
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()
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
[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 = "Anthropic, PBC." }]
|
||||
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"]
|
||||
Loading…
Reference in a new issue