60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
162 lines
4.7 KiB
Python
162 lines
4.7 KiB
Python
"""
|
|
FastMCP Tasks Example Client
|
|
|
|
Demonstrates calling tools both immediately and as background tasks,
|
|
with real-time progress updates via status callbacks.
|
|
|
|
Usage:
|
|
# Make sure environment is configured (source .envrc or use direnv)
|
|
source .envrc
|
|
|
|
# Background task execution with progress callbacks (default)
|
|
python client.py --duration 10
|
|
|
|
# Immediate execution (blocks until complete)
|
|
python client.py immediate --duration 5
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Annotated
|
|
|
|
import cyclopts
|
|
from mcp_types import GetTaskResult
|
|
from rich.console import Console
|
|
|
|
from fastmcp.client import Client
|
|
from fastmcp.types import TextContent
|
|
|
|
console = Console()
|
|
app = cyclopts.App(name="tasks-client", help="FastMCP Tasks Example Client")
|
|
|
|
|
|
def load_server():
|
|
"""Load the example server."""
|
|
examples_dir = Path(__file__).parent.parent.parent
|
|
if str(examples_dir) not in sys.path:
|
|
sys.path.insert(0, str(examples_dir))
|
|
|
|
import examples.tasks.server as server_module
|
|
|
|
return server_module.mcp
|
|
|
|
|
|
# Track last message to deduplicate consecutive identical notifications
|
|
# Note: Docket fires separate events for progress.increment() and progress.set_message(),
|
|
# but MCP's status_message field only carries the text message (no numerical progress).
|
|
# This means we often get duplicate notifications with identical messages.
|
|
_last_notification_message = None
|
|
|
|
|
|
def print_notification(status: GetTaskResult) -> None:
|
|
"""Callback function for push notifications from server.
|
|
|
|
This is called automatically when the server sends notifications/tasks/status.
|
|
Deduplicates identical consecutive messages to keep output clean.
|
|
"""
|
|
global _last_notification_message
|
|
|
|
# Skip if this is the same message we just printed
|
|
if status.status_message == _last_notification_message:
|
|
return
|
|
|
|
_last_notification_message = status.status_message
|
|
|
|
color = {
|
|
"working": "yellow",
|
|
"completed": "green",
|
|
"failed": "red",
|
|
}.get(status.status, "yellow")
|
|
|
|
icon = {
|
|
"working": "🚀",
|
|
"completed": "✅",
|
|
"failed": "❌",
|
|
}.get(status.status, "⚠️")
|
|
|
|
console.print(
|
|
f"[{color}]📢 Notification: {status.status} {icon} - {status.status_message}[/{color}]"
|
|
)
|
|
|
|
|
|
@app.default
|
|
async def task(
|
|
duration: Annotated[
|
|
int,
|
|
cyclopts.Parameter(help="Duration of computation in seconds (1-60)"),
|
|
] = 10,
|
|
):
|
|
"""Execute as background task with real-time progress callbacks."""
|
|
if duration < 1 or duration > 60:
|
|
console.print("[red]Error: Duration must be between 1 and 60 seconds[/red]")
|
|
sys.exit(1)
|
|
|
|
server = load_server()
|
|
|
|
console.print(f"\n[bold]Calling slow_computation(duration={duration})[/bold]")
|
|
console.print("Mode: [cyan]Background task[/cyan]\n")
|
|
|
|
async with Client(server) as client:
|
|
task_obj = await client.call_tool(
|
|
"slow_computation",
|
|
arguments={"duration": duration},
|
|
task=True,
|
|
)
|
|
|
|
console.print(f"Task started: [cyan]{task_obj.task_id}[/cyan]\n")
|
|
|
|
# Register callback for real-time push notifications
|
|
task_obj.on_status_change(print_notification)
|
|
|
|
console.print(
|
|
"[dim]Notifications will appear as the server sends them...[/dim]\n"
|
|
)
|
|
|
|
# Do other work while task runs in background
|
|
for i in range(3):
|
|
await asyncio.sleep(0.5)
|
|
console.print(f"[dim]Client doing other work... ({i + 1}/3)[/dim]")
|
|
|
|
console.print()
|
|
|
|
# Wait for task to complete
|
|
console.print("[dim]Waiting for final result...[/dim]")
|
|
result = await task_obj.result()
|
|
|
|
console.print("\n[bold]Result:[/bold]")
|
|
assert isinstance(result.content[0], TextContent)
|
|
console.print(f" {result.content[0].text}")
|
|
|
|
|
|
@app.command
|
|
async def immediate(
|
|
duration: Annotated[
|
|
int,
|
|
cyclopts.Parameter(help="Duration of computation in seconds (1-60)"),
|
|
] = 5,
|
|
):
|
|
"""Execute the tool immediately (blocks until complete)."""
|
|
if duration < 1 or duration > 60:
|
|
console.print("[red]Error: Duration must be between 1 and 60 seconds[/red]")
|
|
sys.exit(1)
|
|
|
|
server = load_server()
|
|
|
|
console.print(f"\n[bold]Calling slow_computation(duration={duration})[/bold]")
|
|
console.print("Mode: [cyan]Immediate execution[/cyan]\n")
|
|
|
|
async with Client(server) as client:
|
|
result = await client.call_tool(
|
|
"slow_computation",
|
|
arguments={"duration": duration},
|
|
)
|
|
|
|
console.print("\n[bold]Result:[/bold]")
|
|
assert isinstance(result.content[0], TextContent)
|
|
console.print(f" {result.content[0].text}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|