Graph Stats
graph_statsAssess graph health by returning counts of nodes, edges, orphans, and other aggregate metrics. Use at session start or after maintenance to verify graph state.
Instructions
Graph health dashboard — node/edge counts by type, average weight, orphan count, unresolved contradictions, stale entries, schema version, and pending ingest backlog. Returns aggregate counts only; for individual entities use graph_entities. Call at session start to size up the graph before deeper queries, after graph_decay or graph_prune to verify the result, or when debugging unexpected query output. No parameters.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/mcp-server/index.ts:748-793 (registration)Registration of the graph_stats tool via server.registerTool with empty input schema and readOnlyHint annotation.
// ─── Tool: graph_stats ─── server.registerTool("graph_stats", { title: "Graph Stats", description: "Graph health dashboard — node/edge counts by type, average weight, orphan count, unresolved contradictions, stale entries, schema version, and pending ingest backlog. Returns aggregate counts only; for individual entities use graph_entities. Call at session start to size up the graph before deeper queries, after graph_decay or graph_prune to verify the result, or when debugging unexpected query output. No parameters.", inputSchema: {}, annotations: { readOnlyHint: true }, }, async () => { debugLog('graph_stats called'); try { debugLog('attempting getStats()...'); const stats = await client.getStats(currentTenant()); debugLog('getStats() succeeded'); // Add schema version and ingest status let schemaVersion = "1"; try { schemaVersion = readFileSync( join(GRAPH_MEMORY_HOME, "schema", "current_version.txt"), "utf-8", ).trim(); } catch { /* default to 1 */ } let pendingIngest = 0; try { const pendingDir = join(GRAPH_MEMORY_HOME, "ingest", "pending"); pendingIngest = readdirSync(pendingDir).filter((f) => !f.endsWith(".meta.json")).length; } catch { /* dir doesn't exist */ } return toolResult({ schema_version: schemaVersion, ...stats, health: { ...stats.health, pending_ingest_docs: pendingIngest, }, }); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); debugLog(`graph_stats FULL ERROR: ${e.constructor.name}: ${e.message}`); debugLog(`code=${(e as NodeJS.ErrnoException).code ?? 'none'}`); debugLog(`stack=${e.stack ?? 'no stack'}`); return toolError(`graph_stats failed: ${e.message}`); } }); - src/mcp-server/index.ts:756-793 (handler)Handler function for graph_stats: calls client.getStats(currentTenant()) then augments with schema version (read from file) and pending ingest count (read from directory). Returns combined result via toolResult().
}, async () => { debugLog('graph_stats called'); try { debugLog('attempting getStats()...'); const stats = await client.getStats(currentTenant()); debugLog('getStats() succeeded'); // Add schema version and ingest status let schemaVersion = "1"; try { schemaVersion = readFileSync( join(GRAPH_MEMORY_HOME, "schema", "current_version.txt"), "utf-8", ).trim(); } catch { /* default to 1 */ } let pendingIngest = 0; try { const pendingDir = join(GRAPH_MEMORY_HOME, "ingest", "pending"); pendingIngest = readdirSync(pendingDir).filter((f) => !f.endsWith(".meta.json")).length; } catch { /* dir doesn't exist */ } return toolResult({ schema_version: schemaVersion, ...stats, health: { ...stats.health, pending_ingest_docs: pendingIngest, }, }); } catch (err) { const e = err instanceof Error ? err : new Error(String(err)); debugLog(`graph_stats FULL ERROR: ${e.constructor.name}: ${e.message}`); debugLog(`code=${(e as NodeJS.ErrnoException).code ?? 'none'}`); debugLog(`stack=${e.stack ?? 'no stack'}`); return toolError(`graph_stats failed: ${e.message}`); } }); - src/mcp-server/index.ts:754-755 (schema)Empty input schema for graph_stats — the tool takes no parameters.
inputSchema: {}, annotations: { readOnlyHint: true }, - src/shared/neo4j-client.ts:1102-1171 (helper)The getStats method on Neo4jClient that queries Neo4j for node/edge counts by type, and health metrics (avg weight, orphans, contradictions, stale nodes), all scoped to a tenant.
async getStats(tenantId: string): Promise<{ nodes: { total: number; by_type: Record<string, number> }; edges: { total: number; by_type: Record<string, number> }; health: { avg_weight: number; orphaned_nodes: number; unresolved_contradictions: number; stale_nodes: number; }; }> { // Node counts by type (tenant-scoped) const nodeRows = await this.run(` MATCH (n:Entity {tenant_id: $tenantId}) WITH labels(n) AS labels, count(n) AS count UNWIND labels AS label WITH label, sum(count) AS total WHERE label <> 'Entity' RETURN label, total ORDER BY total DESC `, { tenantId }); const byType: Record<string, number> = {}; let totalNodes = 0; for (const row of nodeRows) { const count = Number(row["total"] ?? 0); byType[String(row["label"])] = count; totalNodes += count; } // Edge counts by type (tenant-scoped — both endpoints in tenant) const edgeRows = await this.run(` MATCH (a:Entity {tenant_id: $tenantId})-[r]->(b:Entity {tenant_id: $tenantId}) RETURN type(r) AS type, count(r) AS count ORDER BY count DESC `, { tenantId }); const edgeByType: Record<string, number> = {}; let totalEdges = 0; for (const row of edgeRows) { const count = Number(row["count"] ?? 0); edgeByType[String(row["type"])] = count; totalEdges += count; } // Health metrics (tenant-scoped) const healthRows = await this.run(` OPTIONAL MATCH (a:Entity {tenant_id: $tenantId})-[r]->(b:Entity {tenant_id: $tenantId}) WITH avg(r.weight) AS avgWeight OPTIONAL MATCH (orphan:Entity {tenant_id: $tenantId}) WHERE NOT (orphan)-[]-() WITH avgWeight, count(orphan) AS orphanCount OPTIONAL MATCH (a2:Entity {tenant_id: $tenantId})-[c:CONTRADICTS]->(b2:Entity {tenant_id: $tenantId}) WHERE c.resolved = false WITH avgWeight, orphanCount, count(c) AS contradictions OPTIONAL MATCH (stale:Entity {tenant_id: $tenantId}) WHERE stale.confidence < 0.2 AND stale.last_seen < datetime() - duration('P90D') RETURN avgWeight, orphanCount, contradictions, count(stale) AS staleCount `, { tenantId }); const hRow = healthRows[0]; const avgWeight = hRow ? Number(hRow["avgWeight"] ?? 0) : 0; const orphaned = Number(hRow?.["orphanCount"] ?? 0); const contradictions = Number(hRow?.["contradictions"] ?? 0); const staleNodes = Number(hRow?.["staleCount"] ?? 0); return { nodes: { total: totalNodes, by_type: byType }, edges: { total: totalEdges, by_type: edgeByType }, health: { avg_weight: Math.round(avgWeight * 100) / 100, orphaned_nodes: orphaned, unresolved_contradictions: contradictions, stale_nodes: staleNodes, }, }; }