Build Session Context
graph_build_contextBundles graph health, pending work, recent additions, top hubs, contradictions, and optional topic neighbourhood into one call to reduce round trips at session start.
Instructions
Single tool call that bundles a session's worth of context: graph health, pending work, last dream run summary, recent additions, top knowledge hubs, unresolved contradictions, and (optionally) a topic neighbourhood. Use this at session start instead of running graph_stats / graph_query / graph_contradictions separately. Cuts 4-5 round trips to one.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| topic | No | Optional topic to fetch a neighbourhood for (uses graph_query under the hood). | |
| project_cwd | No | Optional project directory for affinity scoring on the topic neighbourhood. | |
| recent_days | No | Window in days for 'recently added' entities (default 7). | |
| hub_count | No | Number of top knowledge hubs to include (default 5). | |
| include_contradictions | No | Include unresolved contradictions (default true). | |
| max_recent | No | Max recent entities to list (default 15). |
Implementation Reference
- src/mcp-server/index.ts:1121-1245 (registration)Registration of the graph_build_context tool with input schema, description, annotations (readOnlyHint), and the async handler function.
server.registerTool("graph_build_context", { title: "Build Session Context", description: "Single tool call that bundles a session's worth of context: graph health, pending work, last dream " + "run summary, recent additions, top knowledge hubs, unresolved contradictions, and (optionally) a " + "topic neighbourhood. Use this at session start instead of running graph_stats / graph_query / " + "graph_contradictions separately. Cuts 4-5 round trips to one.", inputSchema: { topic: z .string() .optional() .describe("Optional topic to fetch a neighbourhood for (uses graph_query under the hood)."), project_cwd: z .string() .optional() .describe("Optional project directory for affinity scoring on the topic neighbourhood."), recent_days: z .number() .int() .min(1) .max(90) .optional() .default(7) .describe("Window in days for 'recently added' entities (default 7)."), hub_count: z .number() .int() .min(1) .max(20) .optional() .default(5) .describe("Number of top knowledge hubs to include (default 5)."), include_contradictions: z .boolean() .optional() .default(true) .describe("Include unresolved contradictions (default true)."), max_recent: z .number() .int() .min(1) .max(50) .optional() .default(15) .describe("Max recent entities to list (default 15)."), }, annotations: { readOnlyHint: true }, }, async (args) => { try { const tenantId = currentTenant(); const recentDays = args.recent_days ?? 7; const hubCount = args.hub_count ?? 5; const includeContradictions = args.include_contradictions ?? true; const maxRecent = args.max_recent ?? 15; // Run the graph queries in parallel — independent const [statsResult, recent, hubs, contradictions, topicResult] = await Promise.all([ client.getStats(tenantId), client.getRecentAdditions(tenantId, recentDays, maxRecent), client.getTopHubs(tenantId, hubCount), includeContradictions ? client.findContradictions(tenantId, false) : Promise.resolve({ contradictions: [] as Array<Record<string, unknown>> }), args.topic ? client.query(tenantId, [args.topic], { max_hops: 2, min_weight: 0.3, limit: 15, project_context: args.project_cwd, current_only: true, }) : Promise.resolve(null), ]); // File-based context (non-graph) const pendingWork = countPendingWork(); const lastDream = readLastDreamFromAudit() ?? lastDreamFromManifest(); const hoursSinceLastDream = pendingWork.last_dream_run ? Math.round(((Date.now() - new Date(pendingWork.last_dream_run).getTime()) / (1000 * 60 * 60)) * 10) / 10 : null; return toolResult({ generated_at: new Date().toISOString(), graph_health: { nodes: statsResult.nodes.total, edges: statsResult.edges.total, by_node_type: statsResult.nodes.by_type, avg_weight: statsResult.health.avg_weight, orphaned: statsResult.health.orphaned_nodes, stale: statsResult.health.stale_nodes, unresolved_contradictions: statsResult.health.unresolved_contradictions, }, pending_work: { unprocessed_transcripts: pendingWork.unprocessed_transcripts, pending_ingests: pendingWork.pending_ingests, last_dream_run: pendingWork.last_dream_run, hours_since_last_dream: hoursSinceLastDream, }, last_dream: lastDream, recent_additions: { days: recentDays, entity_count: recent.nodes.length, edge_count: recent.edge_count, entities: recent.nodes, }, top_hubs: hubs, contradictions: includeContradictions ? (contradictions as { contradictions: Array<Record<string, unknown>> }).contradictions : null, topic_neighbourhood: topicResult ? { topic: args.topic ?? "", node_count: topicResult.nodes.length, edge_count: topicResult.edges.length, nodes: topicResult.nodes.slice(0, 15), edges: topicResult.edges.slice(0, 25), } : null, }); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); return toolError(`graph_build_context failed: ${e.message}`); } }); - src/mcp-server/index.ts:1168-1245 (handler)Handler function that bundles graph health stats, recent additions, top hubs, contradictions, pending work, last dream info, and optional topic neighbourhood into a single response.
}, async (args) => { try { const tenantId = currentTenant(); const recentDays = args.recent_days ?? 7; const hubCount = args.hub_count ?? 5; const includeContradictions = args.include_contradictions ?? true; const maxRecent = args.max_recent ?? 15; // Run the graph queries in parallel — independent const [statsResult, recent, hubs, contradictions, topicResult] = await Promise.all([ client.getStats(tenantId), client.getRecentAdditions(tenantId, recentDays, maxRecent), client.getTopHubs(tenantId, hubCount), includeContradictions ? client.findContradictions(tenantId, false) : Promise.resolve({ contradictions: [] as Array<Record<string, unknown>> }), args.topic ? client.query(tenantId, [args.topic], { max_hops: 2, min_weight: 0.3, limit: 15, project_context: args.project_cwd, current_only: true, }) : Promise.resolve(null), ]); // File-based context (non-graph) const pendingWork = countPendingWork(); const lastDream = readLastDreamFromAudit() ?? lastDreamFromManifest(); const hoursSinceLastDream = pendingWork.last_dream_run ? Math.round(((Date.now() - new Date(pendingWork.last_dream_run).getTime()) / (1000 * 60 * 60)) * 10) / 10 : null; return toolResult({ generated_at: new Date().toISOString(), graph_health: { nodes: statsResult.nodes.total, edges: statsResult.edges.total, by_node_type: statsResult.nodes.by_type, avg_weight: statsResult.health.avg_weight, orphaned: statsResult.health.orphaned_nodes, stale: statsResult.health.stale_nodes, unresolved_contradictions: statsResult.health.unresolved_contradictions, }, pending_work: { unprocessed_transcripts: pendingWork.unprocessed_transcripts, pending_ingests: pendingWork.pending_ingests, last_dream_run: pendingWork.last_dream_run, hours_since_last_dream: hoursSinceLastDream, }, last_dream: lastDream, recent_additions: { days: recentDays, entity_count: recent.nodes.length, edge_count: recent.edge_count, entities: recent.nodes, }, top_hubs: hubs, contradictions: includeContradictions ? (contradictions as { contradictions: Array<Record<string, unknown>> }).contradictions : null, topic_neighbourhood: topicResult ? { topic: args.topic ?? "", node_count: topicResult.nodes.length, edge_count: topicResult.edges.length, nodes: topicResult.nodes.slice(0, 15), edges: topicResult.edges.slice(0, 25), } : null, }); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); return toolError(`graph_build_context failed: ${e.message}`); } }); - src/mcp-server/index.ts:1128-1166 (schema)Input schema definition for graph_build_context with optional parameters: topic, project_cwd, recent_days, hub_count, include_contradictions, max_recent.
inputSchema: { topic: z .string() .optional() .describe("Optional topic to fetch a neighbourhood for (uses graph_query under the hood)."), project_cwd: z .string() .optional() .describe("Optional project directory for affinity scoring on the topic neighbourhood."), recent_days: z .number() .int() .min(1) .max(90) .optional() .default(7) .describe("Window in days for 'recently added' entities (default 7)."), hub_count: z .number() .int() .min(1) .max(20) .optional() .default(5) .describe("Number of top knowledge hubs to include (default 5)."), include_contradictions: z .boolean() .optional() .default(true) .describe("Include unresolved contradictions (default true)."), max_recent: z .number() .int() .min(1) .max(50) .optional() .default(15) .describe("Max recent entities to list (default 15)."), }, - src/shared/neo4j-client.ts:1687-1726 (helper)Helper on Neo4jClient that queries recently added entities and edge counts within a time window. Called by the graph_build_context handler.
async getRecentAdditions(tenantId: string, days: number, limit = 20): Promise<{ nodes: Array<{ id: string; name: string; type: string; first_seen: string; confidence: number }>; edge_count: number; }> { const nodeRows = await this.run( ` MATCH (n:Entity {tenant_id: $tenantId}) WHERE n.first_seen > datetime() - duration({days: $days}) RETURN n.id AS id, n.name AS name, [l IN labels(n) WHERE l <> 'Entity'][0] AS type, toString(n.first_seen) AS first_seen, n.confidence AS confidence ORDER BY n.first_seen DESC LIMIT $limit `, { tenantId, days, limit }, ); const edgeCountRows = await this.run( ` MATCH (a:Entity {tenant_id: $tenantId})-[r]->(b:Entity {tenant_id: $tenantId}) WHERE r.ingested_at IS NOT NULL AND r.ingested_at > datetime() - duration({days: $days}) RETURN count(r) AS edge_count `, { tenantId, days }, ); return { nodes: nodeRows.map((r) => ({ id: String(r["id"]), name: String(r["name"] ?? ""), type: String(r["type"] ?? "?"), first_seen: String(r["first_seen"] ?? ""), confidence: Number(r["confidence"] ?? 0), })), edge_count: Number(edgeCountRows[0]?.["edge_count"] ?? 0), }; } - src/shared/neo4j-client.ts:1730-1756 (helper)Helper on Neo4jClient that finds the most-connected entities (hubs) by degree. Called by the graph_build_context handler.
async getTopHubs(tenantId: string, count = 5, weight_threshold = 0.3): Promise<Array<{ id: string; name: string; type: string; degree: number; confidence: number; }>> { const rows = await this.run( ` MATCH (n:Entity {tenant_id: $tenantId})-[r]-(other:Entity {tenant_id: $tenantId}) WHERE r.weight > $threshold WITH n, count(r) AS degree WHERE degree >= 3 RETURN n.id AS id, n.name AS name, [l IN labels(n) WHERE l <> 'Entity'][0] AS type, n.confidence AS confidence, degree ORDER BY degree DESC LIMIT $count `, { tenantId, threshold: weight_threshold, count }, ); return rows.map((r) => ({ id: String(r["id"]), name: String(r["name"] ?? ""), type: String(r["type"] ?? "?"), degree: Number(r["degree"] ?? 0), confidence: Number(r["confidence"] ?? 0), })); }