Simulation Data
simulation_dataFetch simulation data: agent profiles, configuration, action logs, social media posts, round-by-round timeline, per-agent activity stats, and interview history. Paginated—use offset to get more results when has_more is true.
Instructions
Access simulation data: agent profiles, configuration, action logs, social media posts, round-by-round timeline, per-agent activity stats, and interview history. Paginated — use offset to get more results when has_more is true.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| simulation_id | Yes | The simulation ID | |
| data_type | Yes | What data to retrieve: overview (condensed summary: entities, agents, graph, config, action stats — start here), profiles (full agent personas), config (simulation parameters), actions (agent action log), posts (social media posts from SQLite), timeline (per-round summaries), agent_stats (per-agent activity breakdown), interview_history (past interview transcripts) | |
| platform | No | Filter by platform (for actions and posts) | |
| agent_name | No | Filter actions by agent name | |
| action_type | No | Filter actions by type (CREATE_POST, LIKE_POST, etc.) | |
| limit | No | Max results per page (default 50) | |
| offset | No | Offset for pagination (default 0) |
Implementation Reference
- mcp-server/src/tools/simulation-data.ts:109-198 (registration)Registration of the 'simulation_data' tool on the MCP server with its input schema and handler callback
export function registerSimulationData(server: McpServer, client: MirofishClient): void { server.registerTool( "simulation_data", { title: "Simulation Data", description: "Access simulation data: agent profiles, configuration, action logs, " + "social media posts, round-by-round timeline, per-agent activity stats, " + "and interview history. Paginated — use offset to get more results when has_more is true.", inputSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, }, async (args) => { try { let data: unknown; const limit = args.limit ?? PAGE_SIZE; const offset = args.offset ?? 0; switch (args.data_type) { case "overview": data = await buildOverview(client, args.simulation_id); break; case "profiles": { const all = await client.getSimulationProfiles(args.simulation_id); data = paginate(all as any[], limit, offset); break; } case "config": data = await client.getSimulationConfig(args.simulation_id); break; case "actions": { const resp = await client.getSimulationActions(args.simulation_id, { platform: args.platform, limit: 500, // fetch more, paginate client-side }); let items = resp.actions as any[]; if (args.agent_name) { const q = args.agent_name.toLowerCase(); items = items.filter((a) => String(a.agent_name ?? "").toLowerCase().includes(q), ); } if (args.action_type) { items = items.filter((a) => a.action_type === args.action_type); } data = paginate(items, limit, offset); break; } case "posts": { data = await client.getSimulationPosts(args.simulation_id, { limit, offset, }); break; } case "timeline": { const all = await client.getSimulationTimeline(args.simulation_id); data = paginate(all as any[], limit, offset); break; } case "agent_stats": data = await client.getAgentStats(args.simulation_id); break; case "interview_history": { const all = await client.getInterviewHistory(args.simulation_id); data = paginate(all as any[], limit, offset); break; } } return { content: [ { type: "text" as const, text: JSON.stringify(data, null, 2), }, ], }; } catch (err) { throw toMcpError(err); } }, ); - Input schema using Zod defining simulation_id, data_type (8 variants), platform, agent_name, action_type, limit, and offset fields
const inputSchema = { simulation_id: z.string().describe("The simulation ID"), data_type: z .enum(["overview", "profiles", "config", "actions", "posts", "timeline", "agent_stats", "interview_history"]) .describe( "What data to retrieve: " + "overview (condensed summary: entities, agents, graph, config, action stats — start here), " + "profiles (full agent personas), " + "config (simulation parameters), " + "actions (agent action log), " + "posts (social media posts from SQLite), " + "timeline (per-round summaries), " + "agent_stats (per-agent activity breakdown), " + "interview_history (past interview transcripts)", ), platform: z .enum(["twitter", "reddit"]) .optional() .describe("Filter by platform (for actions and posts)"), agent_name: z.string().optional().describe("Filter actions by agent name"), action_type: z.string().optional().describe("Filter actions by type (CREATE_POST, LIKE_POST, etc.)"), limit: z.coerce.number().int().min(1).max(200).optional().describe("Max results per page (default 50)"), offset: z.coerce.number().int().min(0).optional().describe("Offset for pagination (default 0)"), }; - Handler implementation: dispatches on data_type to fetch overview (aggregated summary), profiles, config, actions, posts, timeline, agent_stats, or interview_history from the backend client, with pagination support
async (args) => { try { let data: unknown; const limit = args.limit ?? PAGE_SIZE; const offset = args.offset ?? 0; switch (args.data_type) { case "overview": data = await buildOverview(client, args.simulation_id); break; case "profiles": { const all = await client.getSimulationProfiles(args.simulation_id); data = paginate(all as any[], limit, offset); break; } case "config": data = await client.getSimulationConfig(args.simulation_id); break; case "actions": { const resp = await client.getSimulationActions(args.simulation_id, { platform: args.platform, limit: 500, // fetch more, paginate client-side }); let items = resp.actions as any[]; if (args.agent_name) { const q = args.agent_name.toLowerCase(); items = items.filter((a) => String(a.agent_name ?? "").toLowerCase().includes(q), ); } if (args.action_type) { items = items.filter((a) => a.action_type === args.action_type); } data = paginate(items, limit, offset); break; } case "posts": { data = await client.getSimulationPosts(args.simulation_id, { limit, offset, }); break; } case "timeline": { const all = await client.getSimulationTimeline(args.simulation_id); data = paginate(all as any[], limit, offset); break; } case "agent_stats": data = await client.getAgentStats(args.simulation_id); break; case "interview_history": { const all = await client.getInterviewHistory(args.simulation_id); data = paginate(all as any[], limit, offset); break; } } return { content: [ { type: "text" as const, text: JSON.stringify(data, null, 2), }, ], }; } catch (err) { throw toMcpError(err); } }, - Helper function that builds the 'overview' data type by aggregating status, profiles, config, agent stats, timeline, and knowledge graph into a condensed summary
async function buildOverview(client: MirofishClient, simulationId: string): Promise<Record<string, unknown>> { const [sim, profiles, config, agentStats, timeline] = await Promise.all([ client.getStatus(simulationId).catch(() => null), client.getSimulationProfiles(simulationId).catch(() => []), client.getSimulationConfig(simulationId).catch(() => ({})), client.getAgentStats(simulationId).catch(() => ({})), client.getSimulationTimeline(simulationId).catch(() => []), ]); // Fetch knowledge graph if graph_id is available const graphId = (sim as any)?.graph_id; let knowledgeGraph: { entities: number; relations: number; top_entities: any[] } | null = null; if (graphId) { try { const gd = await client.getGraphData(graphId) as any; const nodes = gd?.nodes ?? []; const edges = gd?.edges ?? []; knowledgeGraph = { entities: nodes.length, relations: edges.length, top_entities: nodes.slice(0, 15).map((n: any) => ({ name: n.name, type: n.entity_type || n.type, summary: n.summary?.slice(0, 100) || "", })), }; } catch { /* graph not available */ } } // Condense profiles to name + type + stance (first 50 only) const allAgents = (profiles as any[]).map((p: any, i: number) => ({ id: i, name: p.realname || p.name || p.username || `Agent ${i}`, type: p.entity_type || p.profession || "Unknown", stance: p.stance || p.persona?.slice(0, 80) || "", })); const timeConfig = (config as any)?.time_config; const entityTypes = (config as any)?.entity_types || (sim as any)?.entity_types || []; // Condense timeline to 5 snapshots const tl = timeline as any[]; const timelineSnapshot = tl.length <= 5 ? tl : [ tl[0], tl[Math.floor(tl.length / 4)], tl[Math.floor(tl.length / 2)], tl[Math.floor(tl.length * 3 / 4)], tl[tl.length - 1], ]; return { simulation_id: simulationId, status: (sim as any)?.status ?? "unknown", entity_types: entityTypes, agents_count: allAgents.length, agents: allAgents.slice(0, PAGE_SIZE), agents_has_more: allAgents.length > PAGE_SIZE, simulation_config: timeConfig ? { total_hours: timeConfig.total_simulation_hours, minutes_per_round: timeConfig.minutes_per_round, platforms: [(sim as any)?.enable_twitter && "twitter", (sim as any)?.enable_reddit && "reddit"].filter(Boolean), } : null, knowledge_graph: knowledgeGraph, activity: agentStats, timeline_snapshot: timelineSnapshot, }; } - MirofishClient helper methods that make HTTP calls to the backend API endpoints for simulation data (profiles, actions, posts, timeline, agent-stats, config, graph data, interview history)
// ------------------------------------------------------------------ // Simulation data (actions, posts, profiles, timeline, stats) // ------------------------------------------------------------------ async getSimulationProfiles(simulationId: string): Promise<unknown[]> { const resp = await this.http.get<MirofishApiResponse<unknown[]>>( `/api/simulation/${simulationId}/profiles`, ); return (resp.data?.data as unknown[]) ?? []; } async getSimulationActions( simulationId: string, params: { limit?: number; offset?: number; platform?: string; agent_id?: number; round_num?: number } = {}, ): Promise<{ count: number; actions: unknown[] }> { const resp = await this.http.get<MirofishApiResponse<{ count: number; actions: unknown[] }>>( `/api/simulation/${simulationId}/actions`, { params }, ); return resp.data?.data ?? { count: 0, actions: [] }; } async getSimulationPosts( simulationId: string, params: { limit?: number; offset?: number } = {}, ): Promise<{ posts: Array<{ user_id: number; content: string; num_likes?: number }>; total: number }> { const resp = await this.http.get< MirofishApiResponse<{ posts: Array<{ user_id: number; content: string; num_likes?: number }>; total: number }> >(`/api/simulation/${simulationId}/posts`, { params }); return resp.data?.data ?? { posts: [], total: 0 }; } async getSimulationTimeline(simulationId: string): Promise<unknown[]> { const resp = await this.http.get<MirofishApiResponse<{ timeline: unknown[] }>>( `/api/simulation/${simulationId}/timeline`, ); return resp.data?.data?.timeline ?? []; } async getAgentStats(simulationId: string): Promise<Record<string, unknown>> { const resp = await this.http.get<MirofishApiResponse<{ stats: Record<string, unknown> }>>( `/api/simulation/${simulationId}/agent-stats`, ); return resp.data?.data?.stats ?? {}; } async getSimulationConfig(simulationId: string): Promise<Record<string, unknown>> { const resp = await this.http.get<MirofishApiResponse<Record<string, unknown>>>( `/api/simulation/${simulationId}/config`, ); return resp.data?.data ?? {}; } async getGraphData(graphId: string): Promise<Record<string, unknown>> { const resp = await this.http.get<MirofishApiResponse<Record<string, unknown>>>( `/api/graph/data/${graphId}`, ); return resp.data?.data ?? {}; } async getInterviewHistory( simulationId: string, agentId?: number, ): Promise<unknown[]> { const resp = await this.http.post<MirofishApiResponse<unknown[]>>( `/api/simulation/interview/history`, { simulation_id: simulationId, agent_id: agentId }, ); return (resp.data?.data as unknown[]) ?? []; } // ------------------------------------------------------------------ // Legacy pipeline helpers (private, used only by _legacyCreateAndRun) // ------------------------------------------------------------------ private async _legacyGenerateOntology( prompt: string, documentId?: string, ): Promise<{ project_id: string }> { const resp = await this.http.post<MirofishApiResponse<{ project_id: string }>>( "/api/graph/ontology/generate", { simulation_goal: prompt, document_id: documentId }, ); if (!resp.data?.data?.project_id) { throw new MirofishBackendError("Ontology generation failed", 500); } return resp.data.data; } private async _legacyBuildGraph(projectId: string): Promise<{ task_id: string }> { const resp = await this.http.post<MirofishApiResponse<{ task_id: string }>>( "/api/graph/build", { project_id: projectId }, ); if (!resp.data?.data?.task_id) { throw new MirofishBackendError("Graph build failed to start", 500); } return resp.data.data; } private async _legacyGetProject(projectId: string): Promise<{ graph_id: string }> { const resp = await this.http.get<MirofishApiResponse<{ graph_id: string }>>( `/api/graph/project/${projectId}`, ); if (!resp.data?.data?.graph_id) { throw new MirofishBackendError("Project has no graph_id", 500); } return resp.data.data; } private async _legacyReadDocumentText(documentId: string): Promise<string> { try { const resp = await this.http.get<MirofishApiResponse<{ text: string }>>( `/api/documents/${documentId}`, ); return resp.data?.data?.text ?? ""; } catch { return ""; } } private async _legacyPollTask(taskId: string, timeoutMs = 600_000): Promise<void> { const start = Date.now(); while (Date.now() - start < timeoutMs) { const resp = await this.http.get<MirofishApiResponse<{ status: string; error?: string }>>( `/api/tasks/${taskId}`, ); const data = resp.data?.data; if (data?.status === "completed") return; if (data?.status === "failed") { throw new MirofishBackendError(`Task failed: ${data.error ?? "unknown"}`, 500); } await new Promise((r) => setTimeout(r, 2000)); } throw new MirofishBackendError("Task timed out", 504); } private _resolveRounds(preset?: string): number { switch (preset) { case "quick": return 20; case "deep": return 72; case "standard": default: return 40; } } }