rag-memory-epf-mcp 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,7 @@ A **project-local RAG memory** MCP server — knowledge graph + multilingual vec
12
12
  - **Project-local isolation** — each project gets its own `.memory/rag-memory.db`. Multiple projects run simultaneously without interference.
13
13
  - **3-signal hybrid search** — vector similarity (bge-m3, 1024-dim) + FTS5 BM25 keyword matching + knowledge graph re-ranking, combined via Reciprocal Rank Fusion
14
14
  - **100+ languages** — Korean, Chinese, Japanese, Arabic, and more. Cross-lingual search works out of the box.
15
+ - **Graph-aware scoring** — per-entity geometric decay (0.5^i) with hard cap prevents any single document from dominating results
15
16
  - **27 MCP tools** — entity/relation CRUD, document pipeline, multi-hop graph traversal, export/import, temporal queries
16
17
  - **SQLite optimized** — WAL mode, 32MB cache, 256MB mmap, FTS5 triggers, 7 indexes
17
18
  - **MCP SDK 1.27.1** — Tool Annotations (readOnly/destructive/idempotent), latest protocol 2025-11-25
@@ -55,7 +56,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
55
56
  | `embedChunks` | Generate 1024-dim embeddings + auto-link entities | idempotent |
56
57
  | `embedAllEntities` | Batch embed all entities (32 parallel) | idempotent |
57
58
  | `extractTerms` | Extract potential entity terms | — |
58
- | `linkEntitiesToDocument` | Manually link entities to document chunks | idempotent |
59
+ | `linkEntitiesToDocument` | Link entities to chunks where they actually appear (text-matched) | idempotent |
59
60
  | `deleteDocuments` | Remove documents and associated data | destructive |
60
61
  | `listDocuments` | View all stored documents | readOnly |
61
62
 
@@ -127,6 +128,13 @@ storeDocument(id, content, metadata)
127
128
 
128
129
  ## Changelog
129
130
 
131
+ ### v3.2.1
132
+ - **Fix: `autoLinkEntities` silent failure** — was JOINing a non-existent `observations` table (observations are stored as JSON array column in `entities`). Changed to direct column select + `JSON.parse()`.
133
+
134
+ ### v3.2.0
135
+ - **Chunk-level entity linking in `linkEntitiesToDocument`** — entities are now linked only to chunks where they actually appear (using `buildEntityMatcher` word-boundary/CJK matching), instead of blanket-linking to all chunks. Fixes search result domination by heavily-linked documents.
136
+ - **Graph boost decay + hard cap** — per-entity scores are sorted descending and decayed geometrically (0.5^i): 1st entity 100%, 2nd 50%, 3rd 25%, etc. Hard cap at 0.4 prevents graph signal from overwhelming vector similarity.
137
+
130
138
  ### v3.0.0
131
139
  - **Back to self-contained embeddings** — reverted from Ollama dependency (v2.x) to built-in `@huggingface/transformers` with bge-m3 (1024-dim). No external services required.
132
140
  - **Cross-lingual search** — auto-detects non-English queries and performs dual-language search
package/dist/index.js CHANGED
@@ -9,6 +9,14 @@ import fsSync from 'fs';
9
9
  import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { pipeline, env } from '@huggingface/transformers';
12
+ // Import graphology for graph analytics
13
+ import Graph from 'graphology';
14
+ import louvain from 'graphology-communities-louvain';
15
+ import degree from 'graphology-metrics/centrality/degree.js';
16
+ import betweennessCentrality from 'graphology-metrics/centrality/betweenness.js';
17
+ import closenessCentrality from 'graphology-metrics/centrality/closeness.js';
18
+ import pagerank from 'graphology-metrics/centrality/pagerank.js';
19
+ import modularity from 'graphology-metrics/graph/modularity.js';
12
20
  // Import our new structured tool system
13
21
  import { getAllMCPTools, validateToolArgs, getSystemInfo } from './src/tools/tool-registry.js';
14
22
  // Import migration system
@@ -1264,11 +1272,7 @@ class RAGKnowledgeGraphManager {
1264
1272
  if (chunks.length === 0)
1265
1273
  return 0;
1266
1274
  // Get all entities with observations for richer matching
1267
- const entities = this.db.prepare(`SELECT e.id, e.name, e.entityType,
1268
- GROUP_CONCAT(o.content, ' ||| ') as observations
1269
- FROM entities e
1270
- LEFT JOIN observations o ON o.entityId = e.id
1271
- GROUP BY e.id`).all();
1275
+ const entities = this.db.prepare(`SELECT id, name, entityType, observations FROM entities`).all();
1272
1276
  // Minimum name length: 2 for CJK (e.g. "할랄"), 4 for Latin (avoid "API", "Bug")
1273
1277
  const MIN_LEN_CJK = 2;
1274
1278
  const MIN_LEN_LATIN = 4;
@@ -1284,7 +1288,13 @@ class RAGKnowledgeGraphManager {
1284
1288
  // Also collect observation-derived aliases (short keywords from observations)
1285
1289
  const aliases = [];
1286
1290
  if (entity.observations) {
1287
- const obs = entity.observations.split(' ||| ');
1291
+ let obs;
1292
+ try {
1293
+ obs = JSON.parse(entity.observations);
1294
+ }
1295
+ catch {
1296
+ obs = [];
1297
+ }
1288
1298
  for (const ob of obs) {
1289
1299
  // Extract file paths or identifiers mentioned in observations (e.g. "gemini_converter.py")
1290
1300
  const pathMatch = ob.match(/[\w\-]+\.\w{1,4}\b/g);
@@ -2077,6 +2087,246 @@ class RAGKnowledgeGraphManager {
2077
2087
  chunks: chunkCount.count
2078
2088
  };
2079
2089
  }
2090
+ // === GRAPH ANALYTICS TOOLS (graphology) ===
2091
+ _buildGraphologyGraph() {
2092
+ if (!this.db)
2093
+ throw new Error('Database not initialized');
2094
+ const graph = new Graph({ type: 'undirected', allowSelfLoops: false });
2095
+ // Load entities as nodes
2096
+ const entities = this.db.prepare('SELECT id, name, entityType FROM entities').all();
2097
+ for (const entity of entities) {
2098
+ graph.addNode(entity.id, { name: entity.name, entityType: entity.entityType });
2099
+ }
2100
+ // Load relationships as edges
2101
+ const relations = this.db.prepare('SELECT id, source_entity, target_entity, relationType FROM relationships').all();
2102
+ for (const rel of relations) {
2103
+ if (graph.hasNode(rel.source_entity) && graph.hasNode(rel.target_entity)) {
2104
+ try {
2105
+ graph.addEdge(rel.source_entity, rel.target_entity, {
2106
+ relationType: rel.relationType,
2107
+ id: rel.id,
2108
+ });
2109
+ }
2110
+ catch (e) {
2111
+ // Skip duplicate edges (graphology undirected merges A→B and B→A)
2112
+ }
2113
+ }
2114
+ }
2115
+ return { graph, entities };
2116
+ }
2117
+ async getGraphMetrics(entityNames, metrics, limit = 10) {
2118
+ if (!this.db)
2119
+ throw new Error('Database not initialized');
2120
+ const { graph } = this._buildGraphologyGraph();
2121
+ if (graph.order === 0) {
2122
+ return { metrics: {}, message: 'Knowledge graph is empty' };
2123
+ }
2124
+ const allMetrics = metrics || ['degree', 'betweenness', 'closeness', 'pagerank'];
2125
+ const result = {};
2126
+ if (allMetrics.includes('degree')) {
2127
+ result.degree = degree.degreeCentrality(graph);
2128
+ }
2129
+ if (allMetrics.includes('betweenness')) {
2130
+ result.betweenness = betweennessCentrality(graph);
2131
+ }
2132
+ if (allMetrics.includes('closeness')) {
2133
+ result.closeness = closenessCentrality(graph);
2134
+ }
2135
+ if (allMetrics.includes('pagerank')) {
2136
+ result.pagerank = pagerank(graph);
2137
+ }
2138
+ // Build id→name map
2139
+ const idToName = new Map();
2140
+ graph.forEachNode((id, attrs) => {
2141
+ idToName.set(id, attrs.name);
2142
+ });
2143
+ // If specific entities requested, filter to those
2144
+ if (entityNames && entityNames.length > 0) {
2145
+ const targetIds = new Set();
2146
+ for (const name of entityNames) {
2147
+ const id = `entity_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
2148
+ if (graph.hasNode(id))
2149
+ targetIds.add(id);
2150
+ }
2151
+ const filtered = {};
2152
+ for (const [metricName, scores] of Object.entries(result)) {
2153
+ filtered[metricName] = {};
2154
+ for (const id of targetIds) {
2155
+ const name = idToName.get(id) || id;
2156
+ filtered[metricName][name] = scores[id] ?? null;
2157
+ }
2158
+ }
2159
+ return { metrics: filtered, entityCount: graph.order, edgeCount: graph.size };
2160
+ }
2161
+ // Otherwise return top-N per metric
2162
+ const topResults = {};
2163
+ for (const [metricName, scores] of Object.entries(result)) {
2164
+ const sorted = Object.entries(scores)
2165
+ .sort((a, b) => b[1] - a[1])
2166
+ .slice(0, limit)
2167
+ .map(([id, score]) => ({
2168
+ name: idToName.get(id) || id,
2169
+ score: Math.round(score * 10000) / 10000,
2170
+ }));
2171
+ topResults[metricName] = sorted;
2172
+ }
2173
+ return { metrics: topResults, entityCount: graph.order, edgeCount: graph.size };
2174
+ }
2175
+ async detectCommunities(resolution = 1.0) {
2176
+ if (!this.db)
2177
+ throw new Error('Database not initialized');
2178
+ const { graph } = this._buildGraphologyGraph();
2179
+ if (graph.order === 0) {
2180
+ return { communities: [], modularity: 0, message: 'Knowledge graph is empty' };
2181
+ }
2182
+ // Run Louvain community detection — assign to node attributes
2183
+ louvain.assign(graph, { resolution });
2184
+ // Assign isolated nodes (Louvain skips degree-0 nodes)
2185
+ let maxCommunityId = -1;
2186
+ graph.forEachNode((id, attrs) => {
2187
+ if (typeof attrs.community === 'number' && attrs.community > maxCommunityId) {
2188
+ maxCommunityId = attrs.community;
2189
+ }
2190
+ });
2191
+ graph.forEachNode((id, attrs) => {
2192
+ if (attrs.community === undefined) {
2193
+ graph.setNodeAttribute(id, 'community', ++maxCommunityId);
2194
+ }
2195
+ });
2196
+ // Build id→name map
2197
+ const idToName = new Map();
2198
+ graph.forEachNode((id, attrs) => {
2199
+ idToName.set(id, attrs.name);
2200
+ });
2201
+ // Group entities by community
2202
+ const communityMap = new Map();
2203
+ graph.forEachNode((nodeId, attrs) => {
2204
+ const communityId = attrs.community;
2205
+ if (!communityMap.has(communityId)) {
2206
+ communityMap.set(communityId, []);
2207
+ }
2208
+ communityMap.get(communityId).push({
2209
+ name: idToName.get(nodeId) || nodeId,
2210
+ entityType: attrs.entityType,
2211
+ });
2212
+ });
2213
+ // Sort communities by size (largest first)
2214
+ const sortedCommunities = [...communityMap.entries()]
2215
+ .sort((a, b) => b[1].length - a[1].length)
2216
+ .map(([_id, members], index) => ({
2217
+ communityId: index,
2218
+ size: members.length,
2219
+ members: members.sort((a, b) => a.name.localeCompare(b.name)),
2220
+ }));
2221
+ // Calculate modularity (reads 'community' attribute from nodes)
2222
+ const modularityScore = modularity(graph);
2223
+ // Cross-community edges
2224
+ let crossEdges = 0;
2225
+ graph.forEachEdge((_edge, _attrs, source, target) => {
2226
+ const srcCommunity = graph.getNodeAttribute(source, 'community');
2227
+ const tgtCommunity = graph.getNodeAttribute(target, 'community');
2228
+ if (srcCommunity !== tgtCommunity)
2229
+ crossEdges++;
2230
+ });
2231
+ return {
2232
+ communities: sortedCommunities,
2233
+ totalCommunities: sortedCommunities.length,
2234
+ modularity: Math.round(modularityScore * 10000) / 10000,
2235
+ crossCommunityEdges: crossEdges,
2236
+ entityCount: graph.order,
2237
+ edgeCount: graph.size,
2238
+ };
2239
+ }
2240
+ async analyzeGraphStructure() {
2241
+ if (!this.db)
2242
+ throw new Error('Database not initialized');
2243
+ const { graph } = this._buildGraphologyGraph();
2244
+ if (graph.order === 0) {
2245
+ return { message: 'Knowledge graph is empty', entityCount: 0, edgeCount: 0 };
2246
+ }
2247
+ // Density
2248
+ const density = graph.size > 0
2249
+ ? (2 * graph.size) / (graph.order * (graph.order - 1))
2250
+ : 0;
2251
+ // Degree distribution
2252
+ const degrees = [];
2253
+ const isolatedNodes = [];
2254
+ graph.forEachNode((id, attrs) => {
2255
+ const deg = graph.degree(id);
2256
+ degrees.push(deg);
2257
+ if (deg === 0)
2258
+ isolatedNodes.push(attrs.name);
2259
+ });
2260
+ degrees.sort((a, b) => a - b);
2261
+ const avgDegree = degrees.reduce((s, d) => s + d, 0) / degrees.length;
2262
+ const medianDegree = degrees[Math.floor(degrees.length / 2)];
2263
+ // Connected components (BFS)
2264
+ const visited = new Set();
2265
+ const components = [];
2266
+ graph.forEachNode((startId) => {
2267
+ if (visited.has(startId))
2268
+ return;
2269
+ const component = [];
2270
+ const queue = [startId];
2271
+ visited.add(startId);
2272
+ while (queue.length > 0) {
2273
+ const nodeId = queue.shift();
2274
+ component.push(graph.getNodeAttribute(nodeId, 'name'));
2275
+ graph.forEachNeighbor(nodeId, (neighbor) => {
2276
+ if (!visited.has(neighbor)) {
2277
+ visited.add(neighbor);
2278
+ queue.push(neighbor);
2279
+ }
2280
+ });
2281
+ }
2282
+ components.push(component);
2283
+ });
2284
+ components.sort((a, b) => b.length - a.length);
2285
+ // Relationship type distribution
2286
+ const relTypeCount = {};
2287
+ graph.forEachEdge((_edge, attrs) => {
2288
+ const type = attrs.relationType || 'UNKNOWN';
2289
+ relTypeCount[type] = (relTypeCount[type] || 0) + 1;
2290
+ });
2291
+ // Average clustering coefficient
2292
+ let totalClustering = 0;
2293
+ let clusterableNodes = 0;
2294
+ graph.forEachNode((nodeId) => {
2295
+ const neighbors = graph.neighbors(nodeId);
2296
+ if (neighbors.length < 2)
2297
+ return;
2298
+ let triangles = 0;
2299
+ for (let i = 0; i < neighbors.length; i++) {
2300
+ for (let j = i + 1; j < neighbors.length; j++) {
2301
+ if (graph.hasEdge(neighbors[i], neighbors[j]))
2302
+ triangles++;
2303
+ }
2304
+ }
2305
+ const possibleTriangles = (neighbors.length * (neighbors.length - 1)) / 2;
2306
+ totalClustering += triangles / possibleTriangles;
2307
+ clusterableNodes++;
2308
+ });
2309
+ const avgClustering = clusterableNodes > 0 ? totalClustering / clusterableNodes : 0;
2310
+ return {
2311
+ entityCount: graph.order,
2312
+ edgeCount: graph.size,
2313
+ density: Math.round(density * 10000) / 10000,
2314
+ connectedComponents: {
2315
+ count: components.length,
2316
+ largest: components[0]?.length || 0,
2317
+ sizes: components.map(c => c.length),
2318
+ isolatedNodes,
2319
+ },
2320
+ degreeDistribution: {
2321
+ min: degrees[0],
2322
+ max: degrees[degrees.length - 1],
2323
+ avg: Math.round(avgDegree * 100) / 100,
2324
+ median: medianDegree,
2325
+ },
2326
+ averageClusteringCoefficient: Math.round(avgClustering * 10000) / 10000,
2327
+ relationshipTypes: relTypeCount,
2328
+ };
2329
+ }
2080
2330
  // === MIGRATION TOOLS ===
2081
2331
  async getMigrationStatus() {
2082
2332
  if (!this.db)
@@ -2216,6 +2466,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2216
2466
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.runMigrations(), null, 2) }] };
2217
2467
  case "rollbackMigration":
2218
2468
  return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.rollbackMigration(validatedArgs.targetVersion), null, 2) }] };
2469
+ // Graph Analytics tools (graphology)
2470
+ case "getGraphMetrics":
2471
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getGraphMetrics(validatedArgs.entityNames, validatedArgs.metrics, validatedArgs.limit || 10), null, 2) }] };
2472
+ case "detectCommunities":
2473
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.detectCommunities(validatedArgs.resolution || 1.0), null, 2) }] };
2474
+ case "analyzeGraphStructure":
2475
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.analyzeGraphStructure(), null, 2) }] };
2219
2476
  default:
2220
2477
  throw new Error(`Unknown tool: ${name}`);
2221
2478
  }
@@ -0,0 +1,9 @@
1
+ import { ToolDefinition } from './types.js';
2
+ export declare const getGraphMetricsTool: ToolDefinition;
3
+ export declare const detectCommunitiesTool: ToolDefinition;
4
+ export declare const analyzeGraphStructureTool: ToolDefinition;
5
+ export declare const graphAnalyticsTools: {
6
+ getGraphMetrics: ToolDefinition;
7
+ detectCommunities: ToolDefinition;
8
+ analyzeGraphStructure: ToolDefinition;
9
+ };
@@ -0,0 +1,221 @@
1
+ import { z } from 'zod';
2
+ // === GET GRAPH METRICS TOOL ===
3
+ const getGraphMetricsCapability = {
4
+ description: 'Calculate centrality metrics for entities using graphology graph analysis',
5
+ parameters: {
6
+ type: 'object',
7
+ properties: {
8
+ entityNames: {
9
+ type: 'array',
10
+ description: 'Entity names to calculate metrics for. If empty, returns top entities by each metric.',
11
+ items: { type: 'string' },
12
+ },
13
+ metrics: {
14
+ type: 'array',
15
+ description: 'Which metrics to compute: degree, betweenness, closeness, pagerank',
16
+ items: { type: 'string' },
17
+ },
18
+ limit: {
19
+ type: 'number',
20
+ description: 'Max entities to return when entityNames is empty (default 10)',
21
+ default: 10,
22
+ },
23
+ },
24
+ required: [],
25
+ },
26
+ };
27
+ const getGraphMetricsDescription = () => `<description>
28
+ Calculate centrality metrics for entities using graphology graph analysis algorithms.
29
+ **Identifies the most important, influential, and bridging entities in your knowledge graph.**
30
+ Supports degree, betweenness, closeness centrality and PageRank.
31
+ </description>
32
+
33
+ <importantNotes>
34
+ - (!important!) **Builds in-memory graph** from SQLite data — lightweight for typical knowledge graphs
35
+ - (!important!) Returns centrality scores normalized to 0-1 range
36
+ - (!important!) When no entityNames provided, returns top entities ranked by each metric
37
+ - (!important!) Useful for identifying knowledge hubs, bridges, and peripheral entities
38
+ </importantNotes>
39
+
40
+ <whenToUseThisTool>
41
+ - To find the most connected/important entities in your knowledge graph
42
+ - To identify bridge entities that connect different knowledge domains
43
+ - To discover peripheral entities that may need more connections
44
+ - For knowledge graph health analysis and optimization
45
+ - Before consolidation — to identify which entities are structurally important
46
+ </whenToUseThisTool>
47
+
48
+ <features>
49
+ - Degree centrality: most connected entities (hub detection)
50
+ - Betweenness centrality: entities bridging different clusters
51
+ - Closeness centrality: entities with shortest average distance to all others
52
+ - PageRank: recursive importance based on connections to important entities
53
+ - Supports both targeted (specific entities) and discovery (top-N) modes
54
+ </features>
55
+
56
+ <bestPractices>
57
+ - Use without entityNames first to discover important entities
58
+ - Combine with detectCommunities for deeper structural analysis
59
+ - High betweenness + low degree = critical bridge (fragile point)
60
+ - High degree + high PageRank = knowledge hub
61
+ - Run periodically to track how graph structure evolves
62
+ </bestPractices>
63
+
64
+ <parameters>
65
+ - entityNames: (optional) Specific entities to analyze. If omitted, returns top entities by each metric.
66
+ - metrics: (optional) Array of metrics to compute: "degree", "betweenness", "closeness", "pagerank". Default: all.
67
+ - limit: (optional) Max results per metric when entityNames is empty. Default: 10.
68
+ </parameters>
69
+
70
+ <examples>
71
+ - Top entities: {} (returns top 10 by all metrics)
72
+ - Specific entities: {"entityNames": ["Entity A", "Entity B"]}
73
+ - PageRank only: {"metrics": ["pagerank"], "limit": 20}
74
+ </examples>`;
75
+ const getGraphMetricsSchema = {
76
+ entityNames: z.array(z.string().describe('Entity name')).optional().describe('Entity names to analyze'),
77
+ metrics: z.array(z.string().describe('Metric name')).optional().describe('Metrics to compute: degree, betweenness, closeness, pagerank'),
78
+ limit: z.number().optional().default(10).describe('Max results per metric'),
79
+ };
80
+ export const getGraphMetricsTool = {
81
+ capability: getGraphMetricsCapability,
82
+ description: getGraphMetricsDescription,
83
+ schema: getGraphMetricsSchema,
84
+ annotations: { readOnlyHint: true },
85
+ };
86
+ // === DETECT COMMUNITIES TOOL ===
87
+ const detectCommunitiesCapability = {
88
+ description: 'Detect communities/clusters in the knowledge graph using Louvain algorithm',
89
+ parameters: {
90
+ type: 'object',
91
+ properties: {
92
+ resolution: {
93
+ type: 'number',
94
+ description: 'Resolution parameter for Louvain (higher = more communities). Default: 1.0',
95
+ default: 1.0,
96
+ },
97
+ },
98
+ required: [],
99
+ },
100
+ };
101
+ const detectCommunitiesDescription = () => `<description>
102
+ Detect communities (clusters) in the knowledge graph using the Louvain modularity algorithm.
103
+ **Reveals natural groupings and topic clusters within your knowledge base.**
104
+ Automatically identifies which entities belong together based on connection patterns.
105
+ </description>
106
+
107
+ <importantNotes>
108
+ - (!important!) Uses Louvain algorithm — fast and effective for modularity optimization
109
+ - (!important!) Returns community assignments for every entity
110
+ - (!important!) Higher resolution parameter produces more, smaller communities
111
+ - (!important!) Modularity score indicates clustering quality (0-1, higher = better defined clusters)
112
+ </importantNotes>
113
+
114
+ <whenToUseThisTool>
115
+ - To discover natural topic clusters in your knowledge graph
116
+ - To understand how your knowledge is organized structurally
117
+ - Before reorganization — to see which entities naturally group together
118
+ - For identifying isolated clusters that may need cross-linking
119
+ - To validate that related entities are actually well-connected
120
+ </whenToUseThisTool>
121
+
122
+ <features>
123
+ - Louvain community detection with configurable resolution
124
+ - Returns named communities with member entities
125
+ - Modularity score for clustering quality assessment
126
+ - Community size distribution for balance analysis
127
+ - Cross-community relationship identification
128
+ </features>
129
+
130
+ <bestPractices>
131
+ - Start with default resolution (1.0), adjust if clusters are too large/small
132
+ - Resolution < 1.0 = fewer, larger communities; > 1.0 = more, smaller communities
133
+ - Single-entity communities may indicate orphaned or poorly connected entities
134
+ - Compare community structure over time to track knowledge evolution
135
+ - Use with getGraphMetrics to find important entities within each community
136
+ </bestPractices>
137
+
138
+ <parameters>
139
+ - resolution: (optional) Louvain resolution. Default: 1.0. Lower = fewer clusters, Higher = more clusters.
140
+ </parameters>
141
+
142
+ <examples>
143
+ - Default clustering: {}
144
+ - Fine-grained: {"resolution": 2.0}
145
+ - Coarse: {"resolution": 0.5}
146
+ </examples>`;
147
+ const detectCommunitiesSchema = {
148
+ resolution: z.number().optional().default(1.0).describe('Louvain resolution parameter'),
149
+ };
150
+ export const detectCommunitiesTool = {
151
+ capability: detectCommunitiesCapability,
152
+ description: detectCommunitiesDescription,
153
+ schema: detectCommunitiesSchema,
154
+ annotations: { readOnlyHint: true },
155
+ };
156
+ // === ANALYZE GRAPH STRUCTURE TOOL ===
157
+ const analyzeGraphStructureCapability = {
158
+ description: 'Analyze overall knowledge graph structure: density, components, diameter, clustering',
159
+ parameters: {
160
+ type: 'object',
161
+ properties: {},
162
+ required: [],
163
+ },
164
+ };
165
+ const analyzeGraphStructureDescription = () => `<description>
166
+ Analyze the overall structural properties of your knowledge graph.
167
+ **Provides a health dashboard for graph connectivity, density, and structure.**
168
+ Complements getKnowledgeGraphStats (which counts entities/relations) with structural analysis.
169
+ </description>
170
+
171
+ <importantNotes>
172
+ - (!important!) Returns graph-theoretic metrics: density, components, diameter, clustering coefficient
173
+ - (!important!) Identifies disconnected components (isolated subgraphs)
174
+ - (!important!) Detects structural issues: low density, fragmentation, bottlenecks
175
+ - (!important!) Computationally heavier than stats — use periodically, not every session
176
+ </importantNotes>
177
+
178
+ <whenToUseThisTool>
179
+ - For periodic graph health assessment (weekly or after major changes)
180
+ - To detect fragmentation — disconnected components that should be linked
181
+ - To measure graph density — is the knowledge well-interconnected?
182
+ - Before and after consolidation — to measure structural improvement
183
+ - When graph feels "sparse" and you want to quantify it
184
+ </whenToUseThisTool>
185
+
186
+ <features>
187
+ - Graph density (actual edges / possible edges)
188
+ - Connected component analysis with sizes
189
+ - Average clustering coefficient (local connectivity)
190
+ - Degree distribution statistics (min, max, avg, median)
191
+ - Isolated node detection
192
+ - Relationship type distribution
193
+ </features>
194
+
195
+ <bestPractices>
196
+ - Run after major entity additions to check integration
197
+ - Compare density over time — decreasing density may indicate growing but disconnected knowledge
198
+ - Multiple small components suggest domain silos that need cross-linking
199
+ - Use alongside detectCommunities for complete structural understanding
200
+ </bestPractices>
201
+
202
+ <parameters>
203
+ - None required — analyzes the entire graph structure
204
+ </parameters>
205
+
206
+ <examples>
207
+ - Full analysis: {} (no parameters needed)
208
+ </examples>`;
209
+ const analyzeGraphStructureSchema = {};
210
+ export const analyzeGraphStructureTool = {
211
+ capability: analyzeGraphStructureCapability,
212
+ description: analyzeGraphStructureDescription,
213
+ schema: analyzeGraphStructureSchema,
214
+ annotations: { readOnlyHint: true },
215
+ };
216
+ // === EXPORT ===
217
+ export const graphAnalyticsTools = {
218
+ getGraphMetrics: getGraphMetricsTool,
219
+ detectCommunities: detectCommunitiesTool,
220
+ analyzeGraphStructure: analyzeGraphStructureTool,
221
+ };
@@ -1,5 +1,8 @@
1
1
  import { ToolDefinition, MCPTool } from './types.js';
2
2
  export declare const allTools: {
3
+ getGraphMetrics: ToolDefinition;
4
+ detectCommunities: ToolDefinition;
5
+ analyzeGraphStructure: ToolDefinition;
3
6
  readGraph: ToolDefinition;
4
7
  searchNodes: ToolDefinition;
5
8
  openNodes: ToolDefinition;
@@ -53,6 +56,7 @@ export declare function getToolsByCategory(): {
53
56
  knowledgeGraph: string[];
54
57
  rag: string[];
55
58
  graphQuery: string[];
59
+ graphAnalytics: string[];
56
60
  migration: string[];
57
61
  all: string[];
58
62
  };
@@ -73,12 +77,14 @@ export declare function getSystemInfo(): {
73
77
  knowledgeGraph: number;
74
78
  rag: number;
75
79
  graphQuery: number;
80
+ graphAnalytics: number;
76
81
  total: number;
77
82
  };
78
83
  availableTools: {
79
84
  knowledgeGraph: string[];
80
85
  rag: string[];
81
86
  graphQuery: string[];
87
+ graphAnalytics: string[];
82
88
  migration: string[];
83
89
  all: string[];
84
90
  };
@@ -2,14 +2,15 @@ import { z } from 'zod';
2
2
  import { knowledgeGraphTools } from './knowledge-graph-tools.js';
3
3
  import { ragTools } from './rag-tools.js';
4
4
  import { graphQueryTools } from './graph-query-tools.js';
5
+ import { graphAnalyticsTools } from './graph-analytics-tools.js';
5
6
  import { migrationTools } from './migration-tools.js';
6
7
  // Central registry of all tools
7
8
  export const allTools = {
8
9
  ...knowledgeGraphTools,
9
10
  ...ragTools,
10
11
  ...graphQueryTools,
12
+ ...graphAnalyticsTools,
11
13
  ...migrationTools,
12
- // Add other tool categories here as needed
13
14
  };
14
15
  // Global settings for tool descriptions
15
16
  export const globalSettings = {
@@ -153,6 +154,7 @@ export function getToolsByCategory() {
153
154
  knowledgeGraph: Object.keys(knowledgeGraphTools),
154
155
  rag: Object.keys(ragTools),
155
156
  graphQuery: Object.keys(graphQueryTools),
157
+ graphAnalytics: Object.keys(graphAnalyticsTools),
156
158
  migration: Object.keys(migrationTools),
157
159
  all: Object.keys(allTools),
158
160
  };
@@ -178,6 +180,7 @@ export function getSystemInfo() {
178
180
  knowledgeGraph: categories.knowledgeGraph.length,
179
181
  rag: categories.rag.length,
180
182
  graphQuery: categories.graphQuery.length,
183
+ graphAnalytics: categories.graphAnalytics.length,
181
184
  total: categories.all.length,
182
185
  },
183
186
  availableTools: categories,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "MCP server for project-local RAG memory with knowledge graph and multilingual vector search",
5
5
  "license": "MIT",
6
6
  "author": "bripin123",
@@ -27,6 +27,11 @@
27
27
  "@huggingface/transformers": "^3.5.1",
28
28
  "@modelcontextprotocol/sdk": "^1.27.1",
29
29
  "better-sqlite3": "^12.8.0",
30
+ "graphology": "^0.26.0",
31
+ "graphology-communities-louvain": "^2.0.2",
32
+ "graphology-metrics": "^2.4.0",
33
+ "graphology-shortest-path": "^2.1.0",
34
+ "graphology-types": "^0.24.8",
30
35
  "sqlite-vec": "^0.1.7",
31
36
  "tiktoken": "^1.0.17",
32
37
  "zod": "^3.25.28"