Simulation Status
simulation_statusCheck the progress of a running or completed simulation. Long-polls up to 50 seconds for state changes and includes a full prediction report upon completion.
Instructions
Check the progress of a running or completed simulation. Long-polls by default — blocks up to 50s waiting for a state change (phase transition, new round, new actions, completion). When state=COMPLETED, includes the full prediction report inline.
Lifecycle: CREATED → GRAPH_BUILDING → GENERATING_PROFILES → READY → SIMULATING → COMPLETED/FAILED/CANCELLED/INTERRUPTED.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| simulation_id | Yes | The simulation ID returned by create_simulation | |
| detailed | No | Include recent agent actions with content in the response | |
| wait | No | Long-poll: block up to 50s waiting for the next state change. Default true. Set false for immediate snapshot. |
Implementation Reference
- Registration of the 'simulation_status' tool with the MCP server, including the async handler that supports both immediate snapshots and long-polling (up to 50s) via SSE events. Also contains helper functions waitForChange and formatRichStatus.
export function registerSimulationStatus( server: McpServer, client: MirofishClient, ): void { server.registerTool( "simulation_status", { title: "Simulation Status", description: "Check the progress of a running or completed simulation. Long-polls by default " + "— blocks up to 50s waiting for a state change (phase transition, new round, new " + "actions, completion). When state=COMPLETED, includes the full prediction report " + "inline.\n\n" + "Lifecycle: CREATED → GRAPH_BUILDING → GENERATING_PROFILES → READY → SIMULATING → " + "COMPLETED/FAILED/CANCELLED/INTERRUPTED.", inputSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, }, async (args) => { try { const wait = args.wait !== false; const detailed = args.detailed ?? false; // Immediate snapshot path if (!wait) { const snapshot = await client.getStatus(args.simulation_id); const rich = await formatRichStatus(client, snapshot, detailed); return { content: [{ type: "text" as const, text: JSON.stringify(rich, null, 2) }], }; } // Long-poll path: snapshot → wait for next event (up to 50s). const initial = await client.getStatus(args.simulation_id); if (isTerminal(initial.state)) { const rich = await formatRichStatus(client, initial, detailed); return { content: [{ type: "text" as const, text: JSON.stringify(rich, null, 2) }], }; } // Open SSE; wait for STATE_CHANGED / ROUND_END / terminal, or 50s timeout. const stream: MirofishEventStream = client.subscribeEvents( args.simulation_id, initial.last_event_id, ); const changedSnapshot = await waitForChange(stream, initial, 50_000); stream.close(); const final = changedSnapshot ?? initial; const rich = await formatRichStatus(client, final, detailed); return { content: [{ type: "text" as const, text: JSON.stringify(rich, null, 2) }], }; } catch (err) { throw toMcpError(err); } }, ); } /** * Wait for the next meaningful event on the SSE stream (or timeout). * Returns a fresh snapshot if the sim state changed, else null. */ async function waitForChange( stream: MirofishEventStream, initial: SimSnapshot, timeoutMs: number, ): Promise<SimSnapshot | null> { return new Promise<SimSnapshot | null>((resolve) => { const timeout = setTimeout(() => resolve(null), timeoutMs); let settled = false; const settle = (result: SimSnapshot | null) => { if (settled) return; settled = true; clearTimeout(timeout); resolve(result); }; // STATE_CHANGED → fetch fresh snapshot and return stream.on("STATE_CHANGED", async (evt: any) => { try { const sim_id = evt?.sim_id ?? initial.simulation_id; const { MirofishClient } = await import("../client/mirofish-client.js"); // We don't have client here; use the embedded sim_id via re-fetch // through the same endpoint. (The actual client is passed via closure // in the outer handler — simpler to re-open.) // Emit sentinel to let outer handler re-fetch. settle({ ...initial, _changed: true } as unknown as SimSnapshot); } catch { settle(null); } }); // terminal or error → same treatment stream.on("terminal", () => settle({ ...initial, _changed: true } as unknown as SimSnapshot)); stream.on("ERROR", () => settle({ ...initial, _changed: true } as unknown as SimSnapshot)); stream.on("ROUND_END", () => settle({ ...initial, _changed: true } as unknown as SimSnapshot)); stream.on("close", () => settle(null)); }); } /** * Build the RichSimulationStatus response from a SimSnapshot. * Pulls recent_posts for narration material and (on COMPLETED) fetches the * report markdown so Claude Desktop can render it as an artifact. */ async function formatRichStatus( client: MirofishClient, snapshot: SimSnapshot, detailed: boolean, ): Promise<RichSimulationStatus> { // If we got a sentinel from waitForChange, re-fetch a fresh snapshot. const anySnap = snapshot as unknown as { _changed?: boolean; simulation_id: string }; let fresh = snapshot; if (anySnap._changed) { try { fresh = await client.getStatus(snapshot.simulation_id); } catch { // keep original } } const totalActions = fresh.twitter_actions_count + fresh.reddit_actions_count; const phaseLabel = PHASE_DISPLAY[fresh.state] ?? fresh.state; const roundLine = fresh.total_rounds > 0 ? `Round ${fresh.current_round}/${fresh.total_rounds} — ${totalActions} actions so far.` : `${phaseLabel}.`; // recent_posts for narration const posts = (fresh.recent_posts ?? []) .filter((p) => p.action_args?.content) .slice(0, 8) .map((p) => ({ agent: p.agent_name ?? `Agent ${p.agent_id ?? "?"}`, content: String(p.action_args?.content ?? ""), platform: p.platform, round: p.round_num, })); const rich: RichSimulationStatus = { simulation_id: fresh.simulation_id, state: fresh.state, phase: fresh.phase ?? fresh.state.toLowerCase(), progress_percent: fresh.progress_percent ?? 0, current_round: fresh.current_round, total_rounds: fresh.total_rounds, twitter_actions: fresh.twitter_actions_count, reddit_actions: fresh.reddit_actions_count, total_actions: totalActions, message: roundLine, error: fresh.error, }; if (posts.length > 0) { rich.recent_posts = posts; rich.narration_hint = fresh.state === "SIMULATING" ? NARRATION_HINT_ACTIVE : undefined; } else if (fresh.state === "SIMULATING") { rich.narration_hint = NARRATION_HINT_WARMUP; } // On COMPLETED, fetch and embed the report so Claude Desktop can render it. if (fresh.state === "COMPLETED") { try { const report = await client.getOrGenerateReport(fresh.simulation_id); rich.report_markdown = report.markdown_content; rich.report_summary = report.outline?.summary; if (rich.report_markdown) { rich.display_instructions = "The full prediction report is included below as markdown. Output the markdown " + "directly to the user — Claude Desktop will render it as an artifact in the side panel. " + "Do not summarize or truncate."; } } catch { // Report still generating — leave field empty, Claude will poll again. } } return rich; } - Output type definition for the simulation_status tool response, including state, round info, recent posts for narration, and optional report markdown on completion.
export interface RichSimulationStatus { simulation_id: string; state: SimState; phase: string; progress_percent: number; current_round: number; total_rounds: number; twitter_actions: number; reddit_actions: number; total_actions: number; message: string; recent_posts?: Array<{ agent: string; content: string; platform?: string; likes?: number; round?: number; }>; narration_hint?: string; report_markdown?: string; report_summary?: string; display_instructions?: string; error?: string | null; } - Input schema (Zod) for the simulation_status tool: simulation_id (required), detailed (optional boolean), wait (optional boolean, default true).
const inputSchema = { simulation_id: z.string().describe("The simulation ID returned by create_simulation"), detailed: z .coerce.boolean() .optional() .describe("Include recent agent actions with content in the response"), wait: z .coerce.boolean() .optional() .describe( "Long-poll: block up to 50s waiting for the next state change. Default true. Set false for immediate snapshot.", ), }; - mcp-server/src/tools/index.ts:7-26 (registration)Tool registration entry point — registerSimulationStatus is imported and called in registerAllTools.
import { registerSimulationStatus } from "./simulation-status.js"; import { registerGetReport } from "./get-report.js"; import { registerInterviewAgent } from "./interview-agent.js"; import { registerListSimulations } from "./list-simulations.js"; import { registerSearchSimulations } from "./search-simulations.js"; import { registerUploadDocument } from "./upload-document.js"; import { registerSimulationData } from "./simulation-data.js"; import { registerCancelSimulation } from "./cancel-simulation.js"; export function registerAllTools(server: McpServer, client: MirofishClient): void { registerCreateSimulation(server, client); registerSimulationStatus(server, client); registerGetReport(server, client); registerInterviewAgent(server, client); registerListSimulations(server, client); registerSearchSimulations(server, client); registerUploadDocument(server, client); registerSimulationData(server, client); registerCancelSimulation(server, client); } - mcp-server/src/types/index.ts:91-143 (schema)SimSnapshot interface — the raw data from the backend that formatRichStatus transforms into RichSimulationStatus.
export interface SimSnapshot { simulation_id: string; project_id: string; graph_id?: string | null; state: SimState; // Round tracking current_round: number; total_rounds: number; simulated_hours: number; total_simulation_hours: number; twitter_current_round: number; reddit_current_round: number; twitter_simulated_hours: number; reddit_simulated_hours: number; twitter_running: boolean; reddit_running: boolean; twitter_actions_count: number; reddit_actions_count: number; twitter_completed: boolean; reddit_completed: boolean; enable_twitter: boolean; enable_reddit: boolean; process_pid?: number | null; entities_count: number; profiles_count: number; config_generated: boolean; config_reasoning: string; started_at?: string | null; updated_at: string; completed_at?: string | null; error?: string | null; recent_actions: AgentActionRecord[]; // Derived fields added by the /status endpoint phase: string; progress_percent: number; is_terminal: boolean; last_event_id?: number; recent_posts?: Array<{ round_num?: number; timestamp?: string; platform?: string; agent_id?: number; agent_name?: string; action_type?: string; action_args?: { content?: string }; }>; }