mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 02:10:38 +02:00
Merge main into sampling-tools-sep-1577
This commit is contained in:
commit
b544cddca7
81 changed files with 10424 additions and 567 deletions
16
examples/tasks/.envrc
Normal file
16
examples/tasks/.envrc
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# FastMCP Tasks Example Environment Configuration
|
||||
# This file is loaded by direnv (https://direnv.net/) when you cd into this directory
|
||||
# Run `direnv allow` to enable automatic environment loading
|
||||
|
||||
# Enable Docket support for background task execution
|
||||
export FASTMCP_ENABLE_DOCKET=true
|
||||
|
||||
# Enable MCP SEP-1686 task protocol support
|
||||
export FASTMCP_ENABLE_TASKS=true
|
||||
|
||||
# Configure Docket backend URL
|
||||
# Use Redis backend (requires docker-compose up)
|
||||
export FASTMCP_DOCKET_URL=redis://localhost:24242/0
|
||||
|
||||
# Or uncomment to use memory:// for single-process testing
|
||||
# export FASTMCP_DOCKET_URL=memory://
|
||||
62
examples/tasks/README.md
Normal file
62
examples/tasks/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# FastMCP Tasks Example
|
||||
|
||||
Demonstrates background task execution with Docket, including progress tracking, distributed backends, and CLI worker management.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# From the fastmcp root directory
|
||||
uv sync
|
||||
|
||||
# Start Redis
|
||||
cd examples/tasks
|
||||
docker compose up -d
|
||||
|
||||
# Load environment (or source .envrc manually)
|
||||
direnv allow
|
||||
|
||||
# Run the server
|
||||
fastmcp run server.py
|
||||
```
|
||||
|
||||
For single-process mode without Redis, set `FASTMCP_DOCKET_URL=memory://` (note: CLI workers won't work).
|
||||
|
||||
## Running the Client
|
||||
|
||||
```bash
|
||||
# Background execution with progress callbacks
|
||||
python examples/tasks/client.py --duration 10
|
||||
|
||||
# Immediate execution (blocks)
|
||||
python examples/tasks/client.py immediate --duration 5
|
||||
```
|
||||
|
||||
## Starting Additional Workers
|
||||
|
||||
With Redis, you can run additional workers to process tasks in parallel:
|
||||
|
||||
```bash
|
||||
fastmcp tasks worker server.py
|
||||
|
||||
# Configure via environment:
|
||||
export FASTMCP_DOCKET_CONCURRENCY=20
|
||||
fastmcp tasks worker server.py
|
||||
```
|
||||
|
||||
**Backend options:**
|
||||
- `memory://` - Single-process only (default)
|
||||
- `redis://` - Distributed, multi-process (Redis or Valkey)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `FASTMCP_ENABLE_DOCKET` | `false` | Enable Docket task system |
|
||||
| `FASTMCP_ENABLE_TASKS` | `false` | Enable MCP task protocol (SEP-1686) |
|
||||
| `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL |
|
||||
|
||||
## Learn More
|
||||
|
||||
- [FastMCP Tasks Documentation](https://gofastmcp.com/docs/tasks)
|
||||
- [Docket Documentation](https://github.com/PrefectHQ/docket)
|
||||
- [MCP Task Protocol (SEP-1686)](https://spec.modelcontextprotocol.io/specification/architecture/tasks/)
|
||||
160
examples/tasks/client.py
Normal file
160
examples/tasks/client.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"""
|
||||
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, TextContent
|
||||
from rich.console import Console
|
||||
|
||||
from fastmcp.client import Client
|
||||
|
||||
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 statusMessage 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.statusMessage == _last_notification_message:
|
||||
return
|
||||
|
||||
_last_notification_message = status.statusMessage
|
||||
|
||||
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.statusMessage}[/{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()
|
||||
10
examples/tasks/docker-compose.yml
Normal file
10
examples/tasks/docker-compose.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "24242:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
75
examples/tasks/server.py
Normal file
75
examples/tasks/server.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""
|
||||
FastMCP Tasks Example Server
|
||||
|
||||
Demonstrates background task execution with progress tracking using Docket.
|
||||
|
||||
Setup:
|
||||
1. Start Redis: docker compose up -d
|
||||
2. Load environment: source .envrc
|
||||
3. Run server: fastmcp run server.py
|
||||
|
||||
The example uses Redis by default to demonstrate distributed task execution
|
||||
and the fastmcp tasks CLI commands.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from docket import Logged
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.dependencies import Progress
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Tasks Example")
|
||||
|
||||
|
||||
@mcp.tool(task=True)
|
||||
async def slow_computation(
|
||||
duration: Annotated[int, Logged],
|
||||
progress: Progress = Progress(),
|
||||
) -> str:
|
||||
"""
|
||||
Perform a slow computation that takes `duration` seconds.
|
||||
|
||||
This tool demonstrates progress tracking with background tasks.
|
||||
It logs progress every 1-2 seconds and reports progress via Docket.
|
||||
|
||||
Args:
|
||||
duration: Number of seconds the computation should take (1-60)
|
||||
|
||||
Returns:
|
||||
A completion message with the total duration
|
||||
"""
|
||||
if duration < 1 or duration > 60:
|
||||
raise ValueError("Duration must be between 1 and 60 seconds")
|
||||
|
||||
logger.info(f"Starting slow computation for {duration} seconds")
|
||||
|
||||
# Set total progress units
|
||||
await progress.set_total(duration)
|
||||
|
||||
# Process each second
|
||||
for i in range(duration):
|
||||
# Sleep for 1 second
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Update progress
|
||||
elapsed = i + 1
|
||||
remaining = duration - elapsed
|
||||
await progress.increment()
|
||||
await progress.set_message(
|
||||
f"Working... {elapsed}/{duration}s ({remaining}s remaining)"
|
||||
)
|
||||
|
||||
# Log every 1-2 seconds
|
||||
if elapsed % 2 == 0 or elapsed == duration:
|
||||
logger.info(f"Progress: {elapsed}/{duration}s")
|
||||
|
||||
logger.info(f"Completed computation in {duration} seconds")
|
||||
return f"Computation completed successfully in {duration} seconds!"
|
||||
Loading…
Add table
Add a link
Reference in a new issue