"""Dispatch MCP server — lets Hope spawn background tasks. The main agent calls dispatch_task(description) when the user asks for something long-running. The tool returns immediately with a task ID; the actual work runs in a background asyncio task via TaskWorker. Communication with the main agent process happens via /tmp/tasks/events.jsonl (the file-based registry). """ from __future__ import annotations import asyncio import logging import os import time from mcp.server.fastmcp import FastMCP logger = logging.getLogger("voice-agent.dispatch") mcp = FastMCP("dispatch") @mcp.tool() async def dispatch_task(description: str) -> str: """Spawn a background task to research or look something up. Use this when the user asks you to do something that will take more than a few seconds (research, look up news, multi-step investigation). The task runs in parallel while you continue the conversation. When it completes, the result will be delivered back to you automatically — you can tell the user "I'll let you know when I find out." Returns a short confirmation with the task ID. """ try: from agent.task_registry import registry from agent.task_worker import TaskWorker except ImportError: from task_registry import registry from task_worker import TaskWorker task = registry.create(description) worker = TaskWorker(task.id, description) asyncio.create_task(_run_worker(worker, task.id)) logger.info("Dispatched task %s: %s", task.id, description) return f"Task {task.id} started. I'll report back when it's done." async def _run_worker(worker: "TaskWorker", task_id: str): try: from agent.task_registry import registry except ImportError: from task_registry import registry timeout_seconds = float(os.environ.get("TASK_TIMEOUT_SECONDS", "180")) try: await asyncio.wait_for(worker.run(), timeout=timeout_seconds) except asyncio.TimeoutError: message = f"Timed out after {timeout_seconds:g} seconds" logger.error("Task %s %s", task_id, message.lower()) registry.fail(task_id, message) except Exception as e: logger.exception("Task %s crashed: %s", task_id, e) registry.fail(task_id, str(e)) def _status_summary(task_id: str, stalled_s: float = 45.0) -> str: """Human-readable live status for one task, plus a stall hint.""" try: from agent.task_registry import status_summary except ImportError: from task_registry import status_summary return status_summary(task_id, stalled_s) @mcp.tool() async def task_status(task_id: str) -> str: """Check the live status of a background task you dispatched earlier, including whether it is pending, running, completed, failed, or looking stalled (no activity for a while). Pass the task id from dispatch_task (or from list_tasks).""" return _status_summary(task_id) @mcp.tool() async def list_tasks() -> str: """List every background task Hope has dispatched in this conversation, with their live status so you can report progress to the user.""" try: from agent.task_registry import registry except ImportError: from task_registry import registry tasks = registry.list_all() if not tasks: return "No background tasks have been dispatched yet." return "\n".join(_status_summary(t["id"]) for t in tasks) if __name__ == "__main__": mcp.run(transport="stdio")