@giauphan/codeatlas-mcp 1.2.3 β†’ 1.4.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.
Files changed (4) hide show
  1. package/README.md +162 -31
  2. package/dist/index.js +466 -1
  3. package/index.ts +519 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,48 +1,76 @@
1
1
  # πŸ—ΊοΈ CodeAtlas MCP Server
2
2
 
3
- **MCP server for [CodeAtlas](https://github.com/giauphan/CodeAtlas) β€” Expose code analysis data to AI assistants**
4
-
5
3
  [![npm version](https://img.shields.io/npm/v/@giauphan/codeatlas-mcp.svg)](https://www.npmjs.com/package/@giauphan/codeatlas-mcp)
6
4
  ![License](https://img.shields.io/badge/License-MIT-green.svg)
7
5
  ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?logo=typescript)
6
+ ![Node](https://img.shields.io/badge/Node-%3E%3D18-brightgreen?logo=node.js)
8
7
 
9
- ## What is this?
8
+ > A standalone [MCP](https://modelcontextprotocol.io/) server that exposes [CodeAtlas](https://github.com/giauphan/CodeAtlas) analysis data to AI assistants β€” **Gemini, Claude, Cursor, Windsurf, VS Code Copilot**, and more.
10
9
 
11
- A standalone [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that reads CodeAtlas analysis data and exposes it to AI assistants like **Gemini, Claude, Cursor**, etc.
10
+ **NEW in v1.4.0**: 🧠 **AI System Memory** β€” AI remembers your system flow between conversations.
12
11
 
13
- ## Tools
12
+ ---
14
13
 
15
- | Tool | Description |
16
- |------|-------------|
17
- | `list_projects` | List all projects analyzed by CodeAtlas |
18
- | `get_project_structure` | Get modules, classes, functions, variables |
19
- | `get_dependencies` | Get import/call/containment relationships |
20
- | `get_insights` | Get AI-generated code insights |
21
- | `search_entities` | Search for functions, classes by name |
22
- | `get_file_entities` | Get all entities in a specific file |
14
+ ## ⚑ Quick Start
15
+
16
+ ### 1. Analyze your project
23
17
 
24
- ## Prerequisites
18
+ Install the [CodeAtlas VS Code extension](https://github.com/giauphan/CodeAtlas), then run:
25
19
 
26
- 1. Install the [CodeAtlas VS Code extension](https://github.com/giauphan/CodeAtlas)
27
- 2. Run **CodeAtlas: Analyze Project** in VS Code to generate `.codeatlas/analysis.json`
20
+ ```
21
+ Ctrl+Shift+P β†’ CodeAtlas: Analyze Project
22
+ ```
28
23
 
29
- ## Installation
24
+ This generates `.codeatlas/analysis.json` in your project root.
30
25
 
31
- ```bash
32
- npm install -g @giauphan/codeatlas-mcp
26
+ ### 2. Add MCP config
27
+
28
+ Pick your AI assistant and add the config:
29
+
30
+ <details>
31
+ <summary>πŸ”΅ <b>VS Code (Copilot / GitHub Copilot Chat)</b></summary>
32
+
33
+ Open **Settings** (`Ctrl+,`) β†’ search `mcp` β†’ click **Edit in settings.json**, then add:
34
+
35
+ ```json
36
+ {
37
+ "mcp": {
38
+ "servers": {
39
+ "codeatlas": {
40
+ "command": "npx",
41
+ "args": ["-y", "@giauphan/codeatlas-mcp"]
42
+ }
43
+ }
44
+ }
45
+ }
33
46
  ```
34
47
 
35
- Or use directly with `npx` (no install needed):
48
+ Or add via workspace `.vscode/settings.json` for per-project config.
36
49
 
37
- ```bash
38
- npx @giauphan/codeatlas-mcp
50
+ </details>
51
+
52
+ <details>
53
+ <summary>🟒 <b>Gemini</b></summary>
54
+
55
+ Add to `.gemini/settings.json`:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "codeatlas": {
61
+ "command": "npx",
62
+ "args": ["-y", "@giauphan/codeatlas-mcp"]
63
+ }
64
+ }
65
+ }
39
66
  ```
40
67
 
41
- ## Configuration
68
+ </details>
42
69
 
43
- Add to your AI assistant's MCP config:
70
+ <details>
71
+ <summary>🟣 <b>Claude Desktop</b></summary>
44
72
 
45
- ### Gemini (`.gemini/settings.json`)
73
+ Add to `claude_desktop_config.json`:
46
74
 
47
75
  ```json
48
76
  {
@@ -55,7 +83,12 @@ Add to your AI assistant's MCP config:
55
83
  }
56
84
  ```
57
85
 
58
- ### Claude (`claude_desktop_config.json`)
86
+ </details>
87
+
88
+ <details>
89
+ <summary>⚫ <b>Cursor</b></summary>
90
+
91
+ Add to `.cursor/mcp.json`:
59
92
 
60
93
  ```json
61
94
  {
@@ -68,7 +101,12 @@ Add to your AI assistant's MCP config:
68
101
  }
69
102
  ```
70
103
 
71
- ### Cursor (`.cursor/mcp.json`)
104
+ </details>
105
+
106
+ <details>
107
+ <summary>πŸ”΄ <b>Windsurf</b></summary>
108
+
109
+ Add to `.windsurf/mcp.json`:
72
110
 
73
111
  ```json
74
112
  {
@@ -81,15 +119,108 @@ Add to your AI assistant's MCP config:
81
119
  }
82
120
  ```
83
121
 
84
- ## Environment Variables
122
+ </details>
123
+
124
+ > **That's it!** Your AI assistant can now query your codebase structure, dependencies, and insights.
125
+
126
+ ---
127
+
128
+ ## πŸ› οΈ Available Tools
129
+
130
+ ### Code Analysis (6 tools)
131
+
132
+ | Tool | Description |
133
+ |------|-------------|
134
+ | `list_projects` | List all analyzed projects (auto-discovers `~/`) |
135
+ | `get_project_structure` | Get modules, classes, functions, variables |
136
+ | `get_dependencies` | Get import / call / containment relationships |
137
+ | `get_insights` | Get AI-generated code quality insights |
138
+ | `search_entities` | Search functions, classes by name (fuzzy match) |
139
+ | `get_file_entities` | Get all entities defined in a specific file |
140
+
141
+ ### 🧠 AI System Memory (3 tools β€” NEW in v1.4.0)
142
+
143
+ | Tool | Description |
144
+ |------|-------------|
145
+ | `generate_system_flow` | Auto-generate Mermaid architecture diagrams. Scopes: `modules-only`, `full`, `feature` |
146
+ | `sync_system_memory` | Create/update `.agents/memory/` folder β€” AI's persistent long-term memory |
147
+ | `trace_feature_flow` | Trace a feature's flow through the codebase. Returns files in dependency order |
148
+
149
+ ---
150
+
151
+ ## 🧠 AI System Memory
152
+
153
+ AI assistants lose context between conversations. CodeAtlas MCP solves this with **persistent memory files**.
154
+
155
+ ### How it works
156
+
157
+ ```
158
+ Conversation 1 β†’ AI writes code β†’ calls sync_system_memory
159
+ β”‚
160
+ .agents/memory/
161
+ β”œβ”€β”€ system-map.md
162
+ β”œβ”€β”€ modules.json
163
+ β”œβ”€β”€ business-rules.json
164
+ β”œβ”€β”€ conventions.md
165
+ β”œβ”€β”€ feature-flows.json
166
+ └── change-log.json
167
+ β”‚
168
+ Conversation 2 β†’ AI reads .agents/memory/ β†’ knows full system flow instantly
169
+ ```
170
+
171
+ ### Setup AI Memory
172
+
173
+ 1. Copy rule templates to your project:
174
+
175
+ ```bash
176
+ mkdir -p /path/to/your-project/.agents/rules/
177
+ ```
178
+
179
+ 2. Create `.agents/rules/auto-memory.md` with the rule that tells AI to:
180
+ - Read `.agents/memory/` at the start of every conversation
181
+ - Use `trace_feature_flow` before making changes
182
+ - Call `sync_system_memory` after completing changes
183
+
184
+ 3. Run `sync_system_memory` once to generate the initial memory snapshot.
185
+
186
+ > πŸ“– Full setup guide & rule templates: [CodeAtlas docs](https://github.com/giauphan/CodeAtlas/tree/main/docs)
187
+
188
+ ---
189
+
190
+ ## πŸ“¦ Alternative: Global Install
191
+
192
+ If you prefer installing globally instead of using `npx`:
193
+
194
+ ```bash
195
+ npm install -g @giauphan/codeatlas-mcp
196
+ ```
197
+
198
+ Then use `"command": "codeatlas-mcp"` (no `args` needed) in your MCP config.
199
+
200
+ ---
201
+
202
+ ## πŸ”§ Environment Variables
85
203
 
86
204
  | Variable | Description |
87
205
  |----------|-------------|
88
- | `CODEATLAS_PROJECT_DIR` | Optional: specify a project directory to analyze |
206
+ | `CODEATLAS_PROJECT_DIR` | Force a specific project directory |
207
+
208
+ > By default, the server **auto-discovers** all projects with `.codeatlas/analysis.json` under your home directory.
209
+
210
+ ---
211
+
212
+ ## 🌐 Supported Languages
213
+
214
+ | Language | Features |
215
+ |----------|----------|
216
+ | TypeScript / JavaScript | Full AST: imports, classes, functions, variables, calls |
217
+ | Python | Classes, functions, variables, imports, calls |
218
+ | PHP | Classes, interfaces, traits, enums, functions, properties, constants |
219
+ | Blade Templates | `@extends`, `@include`, `@component`, `<x-component>` |
89
220
 
90
- The server auto-discovers projects by scanning `~/` for `.codeatlas/analysis.json` files.
221
+ ---
91
222
 
92
- ## Development
223
+ ## πŸ§‘β€πŸ’» Development
93
224
 
94
225
  ```bash
95
226
  git clone https://github.com/giauphan/codeatlas-mcp.git
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ function loadAnalysis(projectDir) {
77
77
  // Create MCP server
78
78
  const server = new McpServer({
79
79
  name: "codeatlas",
80
- version: "1.2.2",
80
+ version: "1.4.0",
81
81
  });
82
82
  // Tool 0: List all discovered projects
83
83
  server.tool("list_projects", "List all projects that have been analyzed by CodeAtlas. Returns project names, paths, and last analysis time.", {}, async () => {
@@ -267,6 +267,471 @@ server.tool("get_file_entities", "Get all entities (classes, functions, variable
267
267
  };
268
268
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
269
269
  });
270
+ // Tool 6: Generate System Flow β€” Auto-generate Mermaid diagram from code analysis
271
+ server.tool("generate_system_flow", "Auto-generate a Mermaid flowchart diagram showing how modules, classes, and functions connect in the system. Returns a Mermaid diagram string that AI can read to understand the full system flow without reading every file.", {
272
+ project: z.string().optional().describe("Project name or path"),
273
+ scope: z.enum(["full", "modules-only", "feature"]).optional().describe("Scope of the diagram: 'full' shows all entities, 'modules-only' shows only module relationships (recommended for large projects), 'feature' requires the 'feature' param"),
274
+ feature: z.string().optional().describe("Feature keyword to focus the diagram on (e.g. 'auth', 'crawl', 'payment'). Only used when scope='feature'"),
275
+ maxNodes: z.number().optional().describe("Maximum nodes in diagram (default: 60). Reduce for large projects"),
276
+ }, async ({ project, scope, feature, maxNodes }) => {
277
+ const loaded = loadAnalysis(project);
278
+ if (!loaded) {
279
+ return { content: [{ type: "text", text: "No analysis data found. Run 'CodeAtlas: Analyze Project' first." }] };
280
+ }
281
+ const max = maxNodes || 60;
282
+ const diagramScope = scope || "modules-only";
283
+ let nodes = loaded.analysis.graph.nodes;
284
+ let links = loaded.analysis.graph.links;
285
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
286
+ // Filter by scope
287
+ if (diagramScope === "modules-only") {
288
+ nodes = nodes.filter((n) => n.type === "module" && n.filePath);
289
+ const nodeIds = new Set(nodes.map((n) => n.id));
290
+ links = links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target) && l.type === "import");
291
+ }
292
+ else if (diagramScope === "feature" && feature) {
293
+ const q = feature.toLowerCase();
294
+ // Find nodes matching the feature keyword
295
+ const matchingNodes = new Set();
296
+ nodes.forEach((n) => {
297
+ if (n.label.toLowerCase().includes(q) || (n.filePath && n.filePath.toLowerCase().includes(q))) {
298
+ matchingNodes.add(n.id);
299
+ }
300
+ });
301
+ // Expand to include connected nodes (1 hop)
302
+ links.forEach((l) => {
303
+ if (matchingNodes.has(l.source))
304
+ matchingNodes.add(l.target);
305
+ if (matchingNodes.has(l.target))
306
+ matchingNodes.add(l.source);
307
+ });
308
+ nodes = nodes.filter((n) => matchingNodes.has(n.id));
309
+ const nodeIds = new Set(nodes.map((n) => n.id));
310
+ links = links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
311
+ }
312
+ // Truncate if too many nodes
313
+ if (nodes.length > max) {
314
+ // Prioritize: modules > classes > functions > variables
315
+ const priorityOrder = ["module", "class", "function", "variable"];
316
+ nodes.sort((a, b) => {
317
+ const ia = priorityOrder.indexOf(a.type);
318
+ const ib = priorityOrder.indexOf(b.type);
319
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
320
+ });
321
+ nodes = nodes.slice(0, max);
322
+ }
323
+ const truncatedNodeIds = new Set(nodes.map((n) => n.id));
324
+ links = links.filter((l) => truncatedNodeIds.has(l.source) && truncatedNodeIds.has(l.target));
325
+ // Remove duplicate links
326
+ const linkSet = new Set();
327
+ links = links.filter((l) => {
328
+ const key = `${l.source}|${l.target}|${l.type}`;
329
+ if (linkSet.has(key))
330
+ return false;
331
+ linkSet.add(key);
332
+ return true;
333
+ });
334
+ // Build Mermaid diagram
335
+ const sanitize = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_").substring(0, 40);
336
+ const nodeIdMap = new Map();
337
+ let counter = 0;
338
+ const getMermaidId = (nodeId) => {
339
+ if (!nodeIdMap.has(nodeId)) {
340
+ nodeIdMap.set(nodeId, `n${counter++}`);
341
+ }
342
+ return nodeIdMap.get(nodeId);
343
+ };
344
+ const lines = ["graph TD"];
345
+ // Add node declarations
346
+ for (const node of nodes) {
347
+ const mid = getMermaidId(node.id);
348
+ const label = node.label.replace(/"/g, "'");
349
+ const typeIcon = node.type === "module" ? "πŸ“„" : node.type === "class" ? "πŸ—οΈ" : node.type === "function" ? "⚑" : "πŸ“¦";
350
+ if (node.type === "module") {
351
+ lines.push(` ${mid}["${typeIcon} ${label}"]`);
352
+ }
353
+ else if (node.type === "class") {
354
+ lines.push(` ${mid}[["${typeIcon} ${label}"]]`);
355
+ }
356
+ else {
357
+ lines.push(` ${mid}("${typeIcon} ${label}")`);
358
+ }
359
+ }
360
+ // Add link declarations
361
+ const arrowMap = { import: "-->", call: "-.->", contains: "-->" };
362
+ const labelMap = { import: "imports", call: "calls", contains: "contains" };
363
+ for (const link of links) {
364
+ const src = getMermaidId(link.source);
365
+ const tgt = getMermaidId(link.target);
366
+ if (src && tgt) {
367
+ const arrow = arrowMap[link.type] || "-->";
368
+ if (link.type === "contains") {
369
+ lines.push(` ${src} ${arrow} ${tgt}`);
370
+ }
371
+ else {
372
+ lines.push(` ${src} ${arrow}|${labelMap[link.type] || link.type}| ${tgt}`);
373
+ }
374
+ }
375
+ }
376
+ const mermaid = lines.join("\n");
377
+ const result = {
378
+ project: loaded.projectName,
379
+ scope: diagramScope,
380
+ feature: feature || null,
381
+ nodeCount: nodes.length,
382
+ linkCount: links.length,
383
+ truncated: loaded.analysis.graph.nodes.length > max,
384
+ mermaidDiagram: mermaid,
385
+ summary: `System flow for ${loaded.projectName}: ${nodes.filter((n) => n.type === "module").length} modules, ${nodes.filter((n) => n.type === "class").length} classes, ${nodes.filter((n) => n.type === "function").length} functions connected by ${links.length} relationships.`,
386
+ };
387
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
388
+ });
389
+ // Tool 7: Sync System Memory β€” Create/update .agents/memory/ persistent files
390
+ server.tool("sync_system_memory", "Create or update the .agents/memory/ folder with auto-generated system documentation. This folder serves as AI's 'long-term memory' β€” it persists between conversations. After calling this, AI in any future conversation can read these files to understand the full system flow without re-analyzing. Call this after completing any code changes.", {
391
+ project: z.string().optional().describe("Project name or path"),
392
+ businessRule: z.string().optional().describe("Optional: A new business rule to add to the memory (e.g. 'VIP users get free shipping')"),
393
+ changeDescription: z.string().optional().describe("Optional: Description of what was just changed (for the changelog)"),
394
+ }, async ({ project, businessRule, changeDescription }) => {
395
+ const loaded = loadAnalysis(project);
396
+ if (!loaded) {
397
+ return { content: [{ type: "text", text: "No analysis data found. Run 'CodeAtlas: Analyze Project' first." }] };
398
+ }
399
+ const memoryDir = path.join(loaded.projectDir, ".agents", "memory");
400
+ // Create directory structure
401
+ try {
402
+ fs.mkdirSync(memoryDir, { recursive: true });
403
+ }
404
+ catch (e) {
405
+ return { content: [{ type: "text", text: `Failed to create .agents/memory/ directory: ${e}` }] };
406
+ }
407
+ const nodes = loaded.analysis.graph.nodes;
408
+ const links = loaded.analysis.graph.links;
409
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
410
+ const nodeNameMap = new Map(nodes.map((n) => [n.id, n.label]));
411
+ // === 1. system-map.md β€” Mermaid diagram of module relationships ===
412
+ const modules = nodes.filter((n) => n.type === "module" && n.filePath);
413
+ const moduleLinks = links.filter((l) => {
414
+ const src = nodeMap.get(l.source);
415
+ const tgt = nodeMap.get(l.target);
416
+ return src?.type === "module" && tgt?.type === "module" && src.filePath && tgt.filePath && l.type === "import";
417
+ });
418
+ // Deduplicate module links
419
+ const mlSet = new Set();
420
+ const dedupModuleLinks = moduleLinks.filter((l) => {
421
+ const key = `${l.source}|${l.target}`;
422
+ if (mlSet.has(key))
423
+ return false;
424
+ mlSet.add(key);
425
+ return true;
426
+ });
427
+ // Build compact mermaid (max 50 modules for readability)
428
+ const topModules = modules.slice(0, 50);
429
+ const topModuleIds = new Set(topModules.map((m) => m.id));
430
+ const topModuleLinks = dedupModuleLinks.filter((l) => topModuleIds.has(l.source) && topModuleIds.has(l.target));
431
+ let mermaidLines = ["```mermaid", "graph TD"];
432
+ let nodeCounter = 0;
433
+ const mermaidIdMap = new Map();
434
+ for (const mod of topModules) {
435
+ const mid = `m${nodeCounter++}`;
436
+ mermaidIdMap.set(mod.id, mid);
437
+ mermaidLines.push(` ${mid}["πŸ“„ ${mod.label}"]`);
438
+ }
439
+ for (const link of topModuleLinks) {
440
+ const s = mermaidIdMap.get(link.source);
441
+ const t = mermaidIdMap.get(link.target);
442
+ if (s && t)
443
+ mermaidLines.push(` ${s} -->|imports| ${t}`);
444
+ }
445
+ mermaidLines.push("```");
446
+ const systemMapContent = [
447
+ `# System Map β€” ${loaded.projectName}`,
448
+ `> Auto-generated by CodeAtlas MCP on ${new Date().toISOString()}`,
449
+ `> **DO NOT EDIT MANUALLY** β€” This file is regenerated by \`sync_system_memory\``,
450
+ "",
451
+ `## Overview`,
452
+ `- **Modules**: ${loaded.analysis.stats.files}`,
453
+ `- **Functions**: ${loaded.analysis.stats.functions}`,
454
+ `- **Classes**: ${loaded.analysis.stats.classes}`,
455
+ `- **Dependencies**: ${loaded.analysis.stats.dependencies}`,
456
+ `- **Circular Deps**: ${loaded.analysis.stats.circularDeps}`,
457
+ "",
458
+ `## Module Dependency Graph`,
459
+ ...mermaidLines,
460
+ "",
461
+ `## Key Modules (by connection count)`,
462
+ ].join("\n");
463
+ // Count connections per module
464
+ const moduleConnections = new Map();
465
+ for (const link of links) {
466
+ if (link.type === "import") {
467
+ moduleConnections.set(link.source, (moduleConnections.get(link.source) || 0) + 1);
468
+ moduleConnections.set(link.target, (moduleConnections.get(link.target) || 0) + 1);
469
+ }
470
+ }
471
+ const keyModules = modules
472
+ .map((m) => ({ name: m.label, path: m.filePath, connections: moduleConnections.get(m.id) || 0 }))
473
+ .sort((a, b) => b.connections - a.connections)
474
+ .slice(0, 20);
475
+ const keyModulesSection = keyModules
476
+ .map((m, i) => `${i + 1}. **${m.name}** (${m.connections} connections) β€” \`${path.relative(loaded.projectDir, m.path || "")}\``)
477
+ .join("\n");
478
+ fs.writeFileSync(path.join(memoryDir, "system-map.md"), systemMapContent + "\n" + keyModulesSection + "\n");
479
+ // === 2. modules.json β€” Module registry ===
480
+ const modulesJson = modules.map((m) => {
481
+ const contained = links
482
+ .filter((l) => l.source === m.id && l.type === "contains")
483
+ .map((l) => {
484
+ const target = nodeMap.get(l.target);
485
+ return target ? { name: target.label, type: target.type } : null;
486
+ })
487
+ .filter(Boolean);
488
+ const imports = links
489
+ .filter((l) => l.source === m.id && l.type === "import")
490
+ .map((l) => nodeNameMap.get(l.target) || l.target);
491
+ return {
492
+ name: m.label,
493
+ path: m.filePath ? path.relative(loaded.projectDir, m.filePath) : null,
494
+ contains: contained,
495
+ imports: imports,
496
+ connectionCount: moduleConnections.get(m.id) || 0,
497
+ };
498
+ });
499
+ fs.writeFileSync(path.join(memoryDir, "modules.json"), JSON.stringify(modulesJson, null, 2));
500
+ // === 3. feature-flows.json β€” Auto-detect feature groups ===
501
+ // Group files by directory as "features"
502
+ const featureMap = new Map();
503
+ for (const mod of modules) {
504
+ if (!mod.filePath)
505
+ continue;
506
+ const rel = path.relative(loaded.projectDir, mod.filePath);
507
+ const dir = path.dirname(rel).split(path.sep)[0] || ".";
508
+ if (!featureMap.has(dir))
509
+ featureMap.set(dir, []);
510
+ featureMap.get(dir).push(rel);
511
+ }
512
+ const featureFlows = {};
513
+ for (const [dir, files] of featureMap) {
514
+ // Entry points: files with most outgoing imports
515
+ const entryPoints = files
516
+ .map((f) => {
517
+ const moduleId = `module:${f.replace(/\\/g, "/")}`;
518
+ const outgoing = links.filter((l) => l.source === moduleId && l.type === "import").length;
519
+ return { file: f, outgoing };
520
+ })
521
+ .sort((a, b) => b.outgoing - a.outgoing)
522
+ .slice(0, 3)
523
+ .map((e) => e.file);
524
+ featureFlows[dir] = { files, entryPoints };
525
+ }
526
+ fs.writeFileSync(path.join(memoryDir, "feature-flows.json"), JSON.stringify(featureFlows, null, 2));
527
+ // === 4. business-rules.json β€” Persist business rules ===
528
+ const businessRulesPath = path.join(memoryDir, "business-rules.json");
529
+ let businessRules = [];
530
+ if (fs.existsSync(businessRulesPath)) {
531
+ try {
532
+ businessRules = JSON.parse(fs.readFileSync(businessRulesPath, "utf-8"));
533
+ }
534
+ catch { /* start fresh */ }
535
+ }
536
+ if (businessRule) {
537
+ businessRules.push({ rule: businessRule, addedAt: new Date().toISOString() });
538
+ }
539
+ fs.writeFileSync(businessRulesPath, JSON.stringify(businessRules, null, 2));
540
+ // === 5. change-log.json β€” Track recent changes ===
541
+ const changeLogPath = path.join(memoryDir, "change-log.json");
542
+ let changeLog = [];
543
+ if (fs.existsSync(changeLogPath)) {
544
+ try {
545
+ changeLog = JSON.parse(fs.readFileSync(changeLogPath, "utf-8"));
546
+ }
547
+ catch { /* start fresh */ }
548
+ }
549
+ if (changeDescription) {
550
+ changeLog.unshift({ description: changeDescription, timestamp: new Date().toISOString() });
551
+ // Keep only last 50 entries
552
+ changeLog = changeLog.slice(0, 50);
553
+ }
554
+ fs.writeFileSync(changeLogPath, JSON.stringify(changeLog, null, 2));
555
+ // === 6. conventions.md β€” Auto-detect conventions ===
556
+ const langs = new Map();
557
+ modules.forEach((m) => {
558
+ if (!m.filePath)
559
+ return;
560
+ const ext = path.extname(m.filePath);
561
+ langs.set(ext, (langs.get(ext) || 0) + 1);
562
+ });
563
+ const topLangs = Array.from(langs.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5);
564
+ const dirs = Array.from(featureMap.keys()).sort();
565
+ const conventionsContent = [
566
+ `# Conventions β€” ${loaded.projectName}`,
567
+ `> Auto-generated by CodeAtlas MCP on ${new Date().toISOString()}`,
568
+ `> **DO NOT EDIT MANUALLY**`,
569
+ "",
570
+ `## Languages`,
571
+ ...topLangs.map(([ext, count]) => `- \`${ext}\`: ${count} files`),
572
+ "",
573
+ `## Project Structure`,
574
+ ...dirs.map((d) => `- \`${d}/\` β€” ${featureMap.get(d)?.length || 0} files`),
575
+ "",
576
+ `## Architecture Patterns Detected`,
577
+ modules.some((m) => m.filePath?.includes("controller") || m.filePath?.includes("Controller"))
578
+ ? "- βœ… MVC Pattern (Controllers detected)"
579
+ : "",
580
+ modules.some((m) => m.filePath?.includes("service") || m.filePath?.includes("Service"))
581
+ ? "- βœ… Service Layer (Services detected)"
582
+ : "",
583
+ modules.some((m) => m.filePath?.includes("model") || m.filePath?.includes("Model"))
584
+ ? "- βœ… Model Layer (Models detected)"
585
+ : "",
586
+ modules.some((m) => m.filePath?.includes("middleware") || m.filePath?.includes("Middleware"))
587
+ ? "- βœ… Middleware Pattern"
588
+ : "",
589
+ modules.some((m) => m.filePath?.includes("test") || m.filePath?.includes("spec"))
590
+ ? "- βœ… Test Suite Present"
591
+ : "",
592
+ ].filter(Boolean).join("\n");
593
+ fs.writeFileSync(path.join(memoryDir, "conventions.md"), conventionsContent);
594
+ const result = {
595
+ success: true,
596
+ project: loaded.projectName,
597
+ memoryDir,
598
+ filesCreated: [
599
+ "system-map.md",
600
+ "modules.json",
601
+ "feature-flows.json",
602
+ "business-rules.json",
603
+ "change-log.json",
604
+ "conventions.md",
605
+ ],
606
+ stats: {
607
+ modules: modules.length,
608
+ totalEntities: nodes.length,
609
+ totalLinks: links.length,
610
+ businessRulesCount: businessRules.length,
611
+ changeLogEntries: changeLog.length,
612
+ },
613
+ message: `System memory synced for ${loaded.projectName}. AI can read .agents/memory/ at the start of any new conversation to restore full context.`,
614
+ };
615
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
616
+ });
617
+ // Tool 8: Trace Feature Flow β€” Trace how a feature flows through the codebase
618
+ server.tool("trace_feature_flow", "Trace the complete flow of a feature through the codebase. Given a keyword (e.g. 'login', 'payment', 'crawl'), finds all related files, classes, and functions, then orders them by dependency chain to show the execution flow. This helps AI understand which files to read when working on a feature.", {
619
+ project: z.string().optional().describe("Project name or path"),
620
+ keyword: z.string().describe("Feature keyword to trace (e.g. 'auth', 'crawl', 'payment', 'upload')"),
621
+ depth: z.number().optional().describe("How many hops to follow from matching nodes (default: 2)"),
622
+ }, async ({ project, keyword, depth }) => {
623
+ const loaded = loadAnalysis(project);
624
+ if (!loaded) {
625
+ return { content: [{ type: "text", text: "No analysis data found. Run 'CodeAtlas: Analyze Project' first." }] };
626
+ }
627
+ const maxDepth = depth || 2;
628
+ const q = keyword.toLowerCase();
629
+ const nodes = loaded.analysis.graph.nodes;
630
+ const links = loaded.analysis.graph.links;
631
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
632
+ // Step 1: Find seed nodes matching the keyword
633
+ const seedNodes = new Set();
634
+ for (const node of nodes) {
635
+ if (node.label.toLowerCase().includes(q) ||
636
+ (node.filePath && node.filePath.toLowerCase().includes(q)) ||
637
+ node.id.toLowerCase().includes(q)) {
638
+ seedNodes.add(node.id);
639
+ }
640
+ }
641
+ if (seedNodes.size === 0) {
642
+ return {
643
+ content: [
644
+ {
645
+ type: "text",
646
+ text: JSON.stringify({
647
+ keyword,
648
+ matchCount: 0,
649
+ message: `No entities found matching '${keyword}'. Try a broader keyword.`,
650
+ suggestions: nodes
651
+ .filter((n) => n.type === "module" && n.filePath)
652
+ .map((n) => n.label)
653
+ .slice(0, 10),
654
+ }, null, 2),
655
+ },
656
+ ],
657
+ };
658
+ }
659
+ // Step 2: BFS expansion to find connected nodes
660
+ const visited = new Set(seedNodes);
661
+ let frontier = new Set(seedNodes);
662
+ for (let d = 0; d < maxDepth; d++) {
663
+ const nextFrontier = new Set();
664
+ for (const link of links) {
665
+ if (frontier.has(link.source) && !visited.has(link.target)) {
666
+ nextFrontier.add(link.target);
667
+ visited.add(link.target);
668
+ }
669
+ if (frontier.has(link.target) && !visited.has(link.source)) {
670
+ nextFrontier.add(link.source);
671
+ visited.add(link.source);
672
+ }
673
+ }
674
+ frontier = nextFrontier;
675
+ }
676
+ // Step 3: Build the trace result
677
+ const traceNodes = nodes.filter((n) => visited.has(n.id));
678
+ const traceLinks = links.filter((l) => visited.has(l.source) && visited.has(l.target));
679
+ // Group by file for readability
680
+ const byFile = new Map();
681
+ for (const node of traceNodes) {
682
+ const filePath = node.filePath || "external";
683
+ if (!byFile.has(filePath))
684
+ byFile.set(filePath, []);
685
+ byFile.get(filePath).push({
686
+ name: node.label,
687
+ type: node.type,
688
+ isSeed: seedNodes.has(node.id),
689
+ line: node.line || null,
690
+ });
691
+ }
692
+ // Sort files: seed files first, then by entity count
693
+ const filesArray = Array.from(byFile.entries())
694
+ .map(([filePath, entities]) => ({
695
+ filePath: filePath === "external" ? "external" : path.relative(loaded.projectDir, filePath),
696
+ absolutePath: filePath,
697
+ entities,
698
+ hasSeedMatch: entities.some((e) => e.isSeed),
699
+ entityCount: entities.length,
700
+ }))
701
+ .sort((a, b) => {
702
+ if (a.hasSeedMatch && !b.hasSeedMatch)
703
+ return -1;
704
+ if (!a.hasSeedMatch && b.hasSeedMatch)
705
+ return 1;
706
+ return b.entityCount - a.entityCount;
707
+ });
708
+ // Build a reading order (dependency-sorted)
709
+ const fileModuleIds = new Map();
710
+ for (const node of traceNodes) {
711
+ if (node.type === "module" && node.filePath) {
712
+ fileModuleIds.set(node.filePath, node.id);
713
+ }
714
+ }
715
+ const result = {
716
+ keyword,
717
+ project: loaded.projectName,
718
+ seedMatches: seedNodes.size,
719
+ totalConnected: visited.size,
720
+ depth: maxDepth,
721
+ files: filesArray.filter((f) => f.filePath !== "external").slice(0, 30),
722
+ externalDeps: filesArray.find((f) => f.filePath === "external")?.entities.map((e) => e.name) || [],
723
+ relationships: traceLinks.slice(0, 50).map((l) => ({
724
+ from: nodeMap.get(l.source)?.label || l.source,
725
+ to: nodeMap.get(l.target)?.label || l.target,
726
+ type: l.type,
727
+ })),
728
+ readingOrder: filesArray
729
+ .filter((f) => f.hasSeedMatch && f.filePath !== "external")
730
+ .map((f) => f.filePath),
731
+ message: `Found ${seedNodes.size} direct matches and ${visited.size - seedNodes.size} connected entities for '${keyword}'. Start reading from the files in 'readingOrder'.`,
732
+ };
733
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
734
+ });
270
735
  // Start server
271
736
  async function main() {
272
737
  const transport = new StdioServerTransport();
package/index.ts CHANGED
@@ -107,7 +107,7 @@ function loadAnalysis(projectDir?: string): { analysis: AnalysisResult; projectN
107
107
  // Create MCP server
108
108
  const server = new McpServer({
109
109
  name: "codeatlas",
110
- version: "1.2.2",
110
+ version: "1.4.0",
111
111
  });
112
112
 
113
113
  // Tool 0: List all discovered projects
@@ -357,6 +357,524 @@ server.tool(
357
357
  }
358
358
  );
359
359
 
360
+ // Tool 6: Generate System Flow β€” Auto-generate Mermaid diagram from code analysis
361
+ server.tool(
362
+ "generate_system_flow",
363
+ "Auto-generate a Mermaid flowchart diagram showing how modules, classes, and functions connect in the system. Returns a Mermaid diagram string that AI can read to understand the full system flow without reading every file.",
364
+ {
365
+ project: z.string().optional().describe("Project name or path"),
366
+ scope: z.enum(["full", "modules-only", "feature"]).optional().describe("Scope of the diagram: 'full' shows all entities, 'modules-only' shows only module relationships (recommended for large projects), 'feature' requires the 'feature' param"),
367
+ feature: z.string().optional().describe("Feature keyword to focus the diagram on (e.g. 'auth', 'crawl', 'payment'). Only used when scope='feature'"),
368
+ maxNodes: z.number().optional().describe("Maximum nodes in diagram (default: 60). Reduce for large projects"),
369
+ },
370
+ async ({ project, scope, feature, maxNodes }) => {
371
+ const loaded = loadAnalysis(project);
372
+ if (!loaded) {
373
+ return { content: [{ type: "text" as const, text: "No analysis data found. Run 'CodeAtlas: Analyze Project' first." }] };
374
+ }
375
+
376
+ const max = maxNodes || 60;
377
+ const diagramScope = scope || "modules-only";
378
+ let nodes = loaded.analysis.graph.nodes;
379
+ let links = loaded.analysis.graph.links;
380
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
381
+
382
+ // Filter by scope
383
+ if (diagramScope === "modules-only") {
384
+ nodes = nodes.filter((n) => n.type === "module" && n.filePath);
385
+ const nodeIds = new Set(nodes.map((n) => n.id));
386
+ links = links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target) && l.type === "import");
387
+ } else if (diagramScope === "feature" && feature) {
388
+ const q = feature.toLowerCase();
389
+ // Find nodes matching the feature keyword
390
+ const matchingNodes = new Set<string>();
391
+ nodes.forEach((n) => {
392
+ if (n.label.toLowerCase().includes(q) || (n.filePath && n.filePath.toLowerCase().includes(q))) {
393
+ matchingNodes.add(n.id);
394
+ }
395
+ });
396
+ // Expand to include connected nodes (1 hop)
397
+ links.forEach((l) => {
398
+ if (matchingNodes.has(l.source)) matchingNodes.add(l.target);
399
+ if (matchingNodes.has(l.target)) matchingNodes.add(l.source);
400
+ });
401
+ nodes = nodes.filter((n) => matchingNodes.has(n.id));
402
+ const nodeIds = new Set(nodes.map((n) => n.id));
403
+ links = links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
404
+ }
405
+
406
+ // Truncate if too many nodes
407
+ if (nodes.length > max) {
408
+ // Prioritize: modules > classes > functions > variables
409
+ const priorityOrder = ["module", "class", "function", "variable"];
410
+ nodes.sort((a, b) => {
411
+ const ia = priorityOrder.indexOf(a.type);
412
+ const ib = priorityOrder.indexOf(b.type);
413
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
414
+ });
415
+ nodes = nodes.slice(0, max);
416
+ }
417
+
418
+ const truncatedNodeIds = new Set(nodes.map((n) => n.id));
419
+ links = links.filter((l) => truncatedNodeIds.has(l.source) && truncatedNodeIds.has(l.target));
420
+
421
+ // Remove duplicate links
422
+ const linkSet = new Set<string>();
423
+ links = links.filter((l) => {
424
+ const key = `${l.source}|${l.target}|${l.type}`;
425
+ if (linkSet.has(key)) return false;
426
+ linkSet.add(key);
427
+ return true;
428
+ });
429
+
430
+ // Build Mermaid diagram
431
+ const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_]/g, "_").substring(0, 40);
432
+ const nodeIdMap = new Map<string, string>();
433
+ let counter = 0;
434
+
435
+ const getMermaidId = (nodeId: string) => {
436
+ if (!nodeIdMap.has(nodeId)) {
437
+ nodeIdMap.set(nodeId, `n${counter++}`);
438
+ }
439
+ return nodeIdMap.get(nodeId)!;
440
+ };
441
+
442
+ const lines: string[] = ["graph TD"];
443
+
444
+ // Add node declarations
445
+ for (const node of nodes) {
446
+ const mid = getMermaidId(node.id);
447
+ const label = node.label.replace(/"/g, "'");
448
+ const typeIcon = node.type === "module" ? "πŸ“„" : node.type === "class" ? "πŸ—οΈ" : node.type === "function" ? "⚑" : "πŸ“¦";
449
+ if (node.type === "module") {
450
+ lines.push(` ${mid}["${typeIcon} ${label}"]`);
451
+ } else if (node.type === "class") {
452
+ lines.push(` ${mid}[["${typeIcon} ${label}"]]`);
453
+ } else {
454
+ lines.push(` ${mid}("${typeIcon} ${label}")`);
455
+ }
456
+ }
457
+
458
+ // Add link declarations
459
+ const arrowMap: Record<string, string> = { import: "-->", call: "-.->", contains: "-->" };
460
+ const labelMap: Record<string, string> = { import: "imports", call: "calls", contains: "contains" };
461
+ for (const link of links) {
462
+ const src = getMermaidId(link.source);
463
+ const tgt = getMermaidId(link.target);
464
+ if (src && tgt) {
465
+ const arrow = arrowMap[link.type] || "-->";
466
+ if (link.type === "contains") {
467
+ lines.push(` ${src} ${arrow} ${tgt}`);
468
+ } else {
469
+ lines.push(` ${src} ${arrow}|${labelMap[link.type] || link.type}| ${tgt}`);
470
+ }
471
+ }
472
+ }
473
+
474
+ const mermaid = lines.join("\n");
475
+
476
+ const result = {
477
+ project: loaded.projectName,
478
+ scope: diagramScope,
479
+ feature: feature || null,
480
+ nodeCount: nodes.length,
481
+ linkCount: links.length,
482
+ truncated: loaded.analysis.graph.nodes.length > max,
483
+ mermaidDiagram: mermaid,
484
+ summary: `System flow for ${loaded.projectName}: ${nodes.filter((n) => n.type === "module").length} modules, ${nodes.filter((n) => n.type === "class").length} classes, ${nodes.filter((n) => n.type === "function").length} functions connected by ${links.length} relationships.`,
485
+ };
486
+
487
+ return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
488
+ }
489
+ );
490
+
491
+ // Tool 7: Sync System Memory β€” Create/update .agents/memory/ persistent files
492
+ server.tool(
493
+ "sync_system_memory",
494
+ "Create or update the .agents/memory/ folder with auto-generated system documentation. This folder serves as AI's 'long-term memory' β€” it persists between conversations. After calling this, AI in any future conversation can read these files to understand the full system flow without re-analyzing. Call this after completing any code changes.",
495
+ {
496
+ project: z.string().optional().describe("Project name or path"),
497
+ businessRule: z.string().optional().describe("Optional: A new business rule to add to the memory (e.g. 'VIP users get free shipping')"),
498
+ changeDescription: z.string().optional().describe("Optional: Description of what was just changed (for the changelog)"),
499
+ },
500
+ async ({ project, businessRule, changeDescription }) => {
501
+ const loaded = loadAnalysis(project);
502
+ if (!loaded) {
503
+ return { content: [{ type: "text" as const, text: "No analysis data found. Run 'CodeAtlas: Analyze Project' first." }] };
504
+ }
505
+
506
+ const memoryDir = path.join(loaded.projectDir, ".agents", "memory");
507
+
508
+ // Create directory structure
509
+ try {
510
+ fs.mkdirSync(memoryDir, { recursive: true });
511
+ } catch (e) {
512
+ return { content: [{ type: "text" as const, text: `Failed to create .agents/memory/ directory: ${e}` }] };
513
+ }
514
+
515
+ const nodes = loaded.analysis.graph.nodes;
516
+ const links = loaded.analysis.graph.links;
517
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
518
+ const nodeNameMap = new Map(nodes.map((n) => [n.id, n.label]));
519
+
520
+ // === 1. system-map.md β€” Mermaid diagram of module relationships ===
521
+ const modules = nodes.filter((n) => n.type === "module" && n.filePath);
522
+ const moduleLinks = links.filter((l) => {
523
+ const src = nodeMap.get(l.source);
524
+ const tgt = nodeMap.get(l.target);
525
+ return src?.type === "module" && tgt?.type === "module" && src.filePath && tgt.filePath && l.type === "import";
526
+ });
527
+
528
+ // Deduplicate module links
529
+ const mlSet = new Set<string>();
530
+ const dedupModuleLinks = moduleLinks.filter((l) => {
531
+ const key = `${l.source}|${l.target}`;
532
+ if (mlSet.has(key)) return false;
533
+ mlSet.add(key);
534
+ return true;
535
+ });
536
+
537
+ // Build compact mermaid (max 50 modules for readability)
538
+ const topModules = modules.slice(0, 50);
539
+ const topModuleIds = new Set(topModules.map((m) => m.id));
540
+ const topModuleLinks = dedupModuleLinks.filter((l) => topModuleIds.has(l.source) && topModuleIds.has(l.target));
541
+
542
+ let mermaidLines = ["```mermaid", "graph TD"];
543
+ let nodeCounter = 0;
544
+ const mermaidIdMap = new Map<string, string>();
545
+ for (const mod of topModules) {
546
+ const mid = `m${nodeCounter++}`;
547
+ mermaidIdMap.set(mod.id, mid);
548
+ mermaidLines.push(` ${mid}["πŸ“„ ${mod.label}"]`);
549
+ }
550
+ for (const link of topModuleLinks) {
551
+ const s = mermaidIdMap.get(link.source);
552
+ const t = mermaidIdMap.get(link.target);
553
+ if (s && t) mermaidLines.push(` ${s} -->|imports| ${t}`);
554
+ }
555
+ mermaidLines.push("```");
556
+
557
+ const systemMapContent = [
558
+ `# System Map β€” ${loaded.projectName}`,
559
+ `> Auto-generated by CodeAtlas MCP on ${new Date().toISOString()}`,
560
+ `> **DO NOT EDIT MANUALLY** β€” This file is regenerated by \`sync_system_memory\``,
561
+ "",
562
+ `## Overview`,
563
+ `- **Modules**: ${loaded.analysis.stats.files}`,
564
+ `- **Functions**: ${loaded.analysis.stats.functions}`,
565
+ `- **Classes**: ${loaded.analysis.stats.classes}`,
566
+ `- **Dependencies**: ${loaded.analysis.stats.dependencies}`,
567
+ `- **Circular Deps**: ${loaded.analysis.stats.circularDeps}`,
568
+ "",
569
+ `## Module Dependency Graph`,
570
+ ...mermaidLines,
571
+ "",
572
+ `## Key Modules (by connection count)`,
573
+ ].join("\n");
574
+
575
+ // Count connections per module
576
+ const moduleConnections = new Map<string, number>();
577
+ for (const link of links) {
578
+ if (link.type === "import") {
579
+ moduleConnections.set(link.source, (moduleConnections.get(link.source) || 0) + 1);
580
+ moduleConnections.set(link.target, (moduleConnections.get(link.target) || 0) + 1);
581
+ }
582
+ }
583
+ const keyModules = modules
584
+ .map((m) => ({ name: m.label, path: m.filePath, connections: moduleConnections.get(m.id) || 0 }))
585
+ .sort((a, b) => b.connections - a.connections)
586
+ .slice(0, 20);
587
+
588
+ const keyModulesSection = keyModules
589
+ .map((m, i) => `${i + 1}. **${m.name}** (${m.connections} connections) β€” \`${path.relative(loaded.projectDir, m.path || "")}\``)
590
+ .join("\n");
591
+
592
+ fs.writeFileSync(path.join(memoryDir, "system-map.md"), systemMapContent + "\n" + keyModulesSection + "\n");
593
+
594
+ // === 2. modules.json β€” Module registry ===
595
+ const modulesJson = modules.map((m) => {
596
+ const contained = links
597
+ .filter((l) => l.source === m.id && l.type === "contains")
598
+ .map((l) => {
599
+ const target = nodeMap.get(l.target);
600
+ return target ? { name: target.label, type: target.type } : null;
601
+ })
602
+ .filter(Boolean);
603
+
604
+ const imports = links
605
+ .filter((l) => l.source === m.id && l.type === "import")
606
+ .map((l) => nodeNameMap.get(l.target) || l.target);
607
+
608
+ return {
609
+ name: m.label,
610
+ path: m.filePath ? path.relative(loaded.projectDir, m.filePath) : null,
611
+ contains: contained,
612
+ imports: imports,
613
+ connectionCount: moduleConnections.get(m.id) || 0,
614
+ };
615
+ });
616
+
617
+ fs.writeFileSync(path.join(memoryDir, "modules.json"), JSON.stringify(modulesJson, null, 2));
618
+
619
+ // === 3. feature-flows.json β€” Auto-detect feature groups ===
620
+ // Group files by directory as "features"
621
+ const featureMap = new Map<string, string[]>();
622
+ for (const mod of modules) {
623
+ if (!mod.filePath) continue;
624
+ const rel = path.relative(loaded.projectDir, mod.filePath);
625
+ const dir = path.dirname(rel).split(path.sep)[0] || ".";
626
+ if (!featureMap.has(dir)) featureMap.set(dir, []);
627
+ featureMap.get(dir)!.push(rel);
628
+ }
629
+
630
+ const featureFlows: Record<string, { files: string[]; entryPoints: string[] }> = {};
631
+ for (const [dir, files] of featureMap) {
632
+ // Entry points: files with most outgoing imports
633
+ const entryPoints = files
634
+ .map((f) => {
635
+ const moduleId = `module:${f.replace(/\\/g, "/")}`;
636
+ const outgoing = links.filter((l) => l.source === moduleId && l.type === "import").length;
637
+ return { file: f, outgoing };
638
+ })
639
+ .sort((a, b) => b.outgoing - a.outgoing)
640
+ .slice(0, 3)
641
+ .map((e) => e.file);
642
+
643
+ featureFlows[dir] = { files, entryPoints };
644
+ }
645
+
646
+ fs.writeFileSync(path.join(memoryDir, "feature-flows.json"), JSON.stringify(featureFlows, null, 2));
647
+
648
+ // === 4. business-rules.json β€” Persist business rules ===
649
+ const businessRulesPath = path.join(memoryDir, "business-rules.json");
650
+ let businessRules: Array<{ rule: string; addedAt: string }> = [];
651
+ if (fs.existsSync(businessRulesPath)) {
652
+ try {
653
+ businessRules = JSON.parse(fs.readFileSync(businessRulesPath, "utf-8"));
654
+ } catch { /* start fresh */ }
655
+ }
656
+ if (businessRule) {
657
+ businessRules.push({ rule: businessRule, addedAt: new Date().toISOString() });
658
+ }
659
+ fs.writeFileSync(businessRulesPath, JSON.stringify(businessRules, null, 2));
660
+
661
+ // === 5. change-log.json β€” Track recent changes ===
662
+ const changeLogPath = path.join(memoryDir, "change-log.json");
663
+ let changeLog: Array<{ description: string; timestamp: string }> = [];
664
+ if (fs.existsSync(changeLogPath)) {
665
+ try {
666
+ changeLog = JSON.parse(fs.readFileSync(changeLogPath, "utf-8"));
667
+ } catch { /* start fresh */ }
668
+ }
669
+ if (changeDescription) {
670
+ changeLog.unshift({ description: changeDescription, timestamp: new Date().toISOString() });
671
+ // Keep only last 50 entries
672
+ changeLog = changeLog.slice(0, 50);
673
+ }
674
+ fs.writeFileSync(changeLogPath, JSON.stringify(changeLog, null, 2));
675
+
676
+ // === 6. conventions.md β€” Auto-detect conventions ===
677
+ const langs = new Map<string, number>();
678
+ modules.forEach((m) => {
679
+ if (!m.filePath) return;
680
+ const ext = path.extname(m.filePath);
681
+ langs.set(ext, (langs.get(ext) || 0) + 1);
682
+ });
683
+
684
+ const topLangs = Array.from(langs.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5);
685
+ const dirs = Array.from(featureMap.keys()).sort();
686
+
687
+ const conventionsContent = [
688
+ `# Conventions β€” ${loaded.projectName}`,
689
+ `> Auto-generated by CodeAtlas MCP on ${new Date().toISOString()}`,
690
+ `> **DO NOT EDIT MANUALLY**`,
691
+ "",
692
+ `## Languages`,
693
+ ...topLangs.map(([ext, count]) => `- \`${ext}\`: ${count} files`),
694
+ "",
695
+ `## Project Structure`,
696
+ ...dirs.map((d) => `- \`${d}/\` β€” ${featureMap.get(d)?.length || 0} files`),
697
+ "",
698
+ `## Architecture Patterns Detected`,
699
+ modules.some((m) => m.filePath?.includes("controller") || m.filePath?.includes("Controller"))
700
+ ? "- βœ… MVC Pattern (Controllers detected)"
701
+ : "",
702
+ modules.some((m) => m.filePath?.includes("service") || m.filePath?.includes("Service"))
703
+ ? "- βœ… Service Layer (Services detected)"
704
+ : "",
705
+ modules.some((m) => m.filePath?.includes("model") || m.filePath?.includes("Model"))
706
+ ? "- βœ… Model Layer (Models detected)"
707
+ : "",
708
+ modules.some((m) => m.filePath?.includes("middleware") || m.filePath?.includes("Middleware"))
709
+ ? "- βœ… Middleware Pattern"
710
+ : "",
711
+ modules.some((m) => m.filePath?.includes("test") || m.filePath?.includes("spec"))
712
+ ? "- βœ… Test Suite Present"
713
+ : "",
714
+ ].filter(Boolean).join("\n");
715
+
716
+ fs.writeFileSync(path.join(memoryDir, "conventions.md"), conventionsContent);
717
+
718
+ const result = {
719
+ success: true,
720
+ project: loaded.projectName,
721
+ memoryDir,
722
+ filesCreated: [
723
+ "system-map.md",
724
+ "modules.json",
725
+ "feature-flows.json",
726
+ "business-rules.json",
727
+ "change-log.json",
728
+ "conventions.md",
729
+ ],
730
+ stats: {
731
+ modules: modules.length,
732
+ totalEntities: nodes.length,
733
+ totalLinks: links.length,
734
+ businessRulesCount: businessRules.length,
735
+ changeLogEntries: changeLog.length,
736
+ },
737
+ message: `System memory synced for ${loaded.projectName}. AI can read .agents/memory/ at the start of any new conversation to restore full context.`,
738
+ };
739
+
740
+ return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
741
+ }
742
+ );
743
+
744
+ // Tool 8: Trace Feature Flow β€” Trace how a feature flows through the codebase
745
+ server.tool(
746
+ "trace_feature_flow",
747
+ "Trace the complete flow of a feature through the codebase. Given a keyword (e.g. 'login', 'payment', 'crawl'), finds all related files, classes, and functions, then orders them by dependency chain to show the execution flow. This helps AI understand which files to read when working on a feature.",
748
+ {
749
+ project: z.string().optional().describe("Project name or path"),
750
+ keyword: z.string().describe("Feature keyword to trace (e.g. 'auth', 'crawl', 'payment', 'upload')"),
751
+ depth: z.number().optional().describe("How many hops to follow from matching nodes (default: 2)"),
752
+ },
753
+ async ({ project, keyword, depth }) => {
754
+ const loaded = loadAnalysis(project);
755
+ if (!loaded) {
756
+ return { content: [{ type: "text" as const, text: "No analysis data found. Run 'CodeAtlas: Analyze Project' first." }] };
757
+ }
758
+
759
+ const maxDepth = depth || 2;
760
+ const q = keyword.toLowerCase();
761
+ const nodes = loaded.analysis.graph.nodes;
762
+ const links = loaded.analysis.graph.links;
763
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
764
+
765
+ // Step 1: Find seed nodes matching the keyword
766
+ const seedNodes = new Set<string>();
767
+ for (const node of nodes) {
768
+ if (
769
+ node.label.toLowerCase().includes(q) ||
770
+ (node.filePath && node.filePath.toLowerCase().includes(q)) ||
771
+ node.id.toLowerCase().includes(q)
772
+ ) {
773
+ seedNodes.add(node.id);
774
+ }
775
+ }
776
+
777
+ if (seedNodes.size === 0) {
778
+ return {
779
+ content: [
780
+ {
781
+ type: "text" as const,
782
+ text: JSON.stringify({
783
+ keyword,
784
+ matchCount: 0,
785
+ message: `No entities found matching '${keyword}'. Try a broader keyword.`,
786
+ suggestions: nodes
787
+ .filter((n) => n.type === "module" && n.filePath)
788
+ .map((n) => n.label)
789
+ .slice(0, 10),
790
+ }, null, 2),
791
+ },
792
+ ],
793
+ };
794
+ }
795
+
796
+ // Step 2: BFS expansion to find connected nodes
797
+ const visited = new Set<string>(seedNodes);
798
+ let frontier = new Set<string>(seedNodes);
799
+
800
+ for (let d = 0; d < maxDepth; d++) {
801
+ const nextFrontier = new Set<string>();
802
+ for (const link of links) {
803
+ if (frontier.has(link.source) && !visited.has(link.target)) {
804
+ nextFrontier.add(link.target);
805
+ visited.add(link.target);
806
+ }
807
+ if (frontier.has(link.target) && !visited.has(link.source)) {
808
+ nextFrontier.add(link.source);
809
+ visited.add(link.source);
810
+ }
811
+ }
812
+ frontier = nextFrontier;
813
+ }
814
+
815
+ // Step 3: Build the trace result
816
+ const traceNodes = nodes.filter((n) => visited.has(n.id));
817
+ const traceLinks = links.filter((l) => visited.has(l.source) && visited.has(l.target));
818
+
819
+ // Group by file for readability
820
+ const byFile = new Map<string, Array<{ name: string; type: string; isSeed: boolean; line: number | null }>>();
821
+ for (const node of traceNodes) {
822
+ const filePath = node.filePath || "external";
823
+ if (!byFile.has(filePath)) byFile.set(filePath, []);
824
+ byFile.get(filePath)!.push({
825
+ name: node.label,
826
+ type: node.type,
827
+ isSeed: seedNodes.has(node.id),
828
+ line: node.line || null,
829
+ });
830
+ }
831
+
832
+ // Sort files: seed files first, then by entity count
833
+ const filesArray = Array.from(byFile.entries())
834
+ .map(([filePath, entities]) => ({
835
+ filePath: filePath === "external" ? "external" : path.relative(loaded.projectDir, filePath),
836
+ absolutePath: filePath,
837
+ entities,
838
+ hasSeedMatch: entities.some((e) => e.isSeed),
839
+ entityCount: entities.length,
840
+ }))
841
+ .sort((a, b) => {
842
+ if (a.hasSeedMatch && !b.hasSeedMatch) return -1;
843
+ if (!a.hasSeedMatch && b.hasSeedMatch) return 1;
844
+ return b.entityCount - a.entityCount;
845
+ });
846
+
847
+ // Build a reading order (dependency-sorted)
848
+ const fileModuleIds = new Map<string, string>();
849
+ for (const node of traceNodes) {
850
+ if (node.type === "module" && node.filePath) {
851
+ fileModuleIds.set(node.filePath, node.id);
852
+ }
853
+ }
854
+
855
+ const result = {
856
+ keyword,
857
+ project: loaded.projectName,
858
+ seedMatches: seedNodes.size,
859
+ totalConnected: visited.size,
860
+ depth: maxDepth,
861
+ files: filesArray.filter((f) => f.filePath !== "external").slice(0, 30),
862
+ externalDeps: filesArray.find((f) => f.filePath === "external")?.entities.map((e) => e.name) || [],
863
+ relationships: traceLinks.slice(0, 50).map((l) => ({
864
+ from: nodeMap.get(l.source)?.label || l.source,
865
+ to: nodeMap.get(l.target)?.label || l.target,
866
+ type: l.type,
867
+ })),
868
+ readingOrder: filesArray
869
+ .filter((f) => f.hasSeedMatch && f.filePath !== "external")
870
+ .map((f) => f.filePath),
871
+ message: `Found ${seedNodes.size} direct matches and ${visited.size - seedNodes.size} connected entities for '${keyword}'. Start reading from the files in 'readingOrder'.`,
872
+ };
873
+
874
+ return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
875
+ }
876
+ );
877
+
360
878
  // Start server
361
879
  async function main() {
362
880
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giauphan/codeatlas-mcp",
3
- "version": "1.2.3",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for CodeAtlas β€” exposes code analysis data to AI assistants via Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",