@giauphan/codeatlas-mcp 1.2.4 → 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 +147 -19
  2. package/dist/index.js +466 -1
  3. package/index.ts +519 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
  ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?logo=typescript)
6
6
  ![Node](https://img.shields.io/badge/Node-%3E%3D18-brightgreen?logo=node.js)
7
7
 
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**, and more.
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.
9
+
10
+ **NEW in v1.4.0**: 🧠 **AI System Memory** — AI remembers your system flow between conversations.
9
11
 
10
12
  ---
11
13
 
@@ -23,14 +25,88 @@ This generates `.codeatlas/analysis.json` in your project root.
23
25
 
24
26
  ### 2. Add MCP config
25
27
 
26
- Copy the JSON block below into **one** of these files depending on your AI assistant:
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
+ }
46
+ ```
47
+
48
+ Or add via workspace `.vscode/settings.json` for per-project config.
49
+
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
+ }
66
+ ```
67
+
68
+ </details>
69
+
70
+ <details>
71
+ <summary>🟣 <b>Claude Desktop</b></summary>
72
+
73
+ Add to `claude_desktop_config.json`:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "codeatlas": {
79
+ "command": "npx",
80
+ "args": ["-y", "@giauphan/codeatlas-mcp"]
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ </details>
87
+
88
+ <details>
89
+ <summary>⚫ <b>Cursor</b></summary>
90
+
91
+ Add to `.cursor/mcp.json`:
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "codeatlas": {
97
+ "command": "npx",
98
+ "args": ["-y", "@giauphan/codeatlas-mcp"]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ </details>
27
105
 
28
- | AI Assistant | Config file |
29
- |---|---|
30
- | **Gemini** | `.gemini/settings.json` |
31
- | **Claude Desktop** | `claude_desktop_config.json` |
32
- | **Cursor** | `.cursor/mcp.json` |
33
- | **Windsurf** | `.windsurf/mcp.json` |
106
+ <details>
107
+ <summary>🔴 <b>Windsurf</b></summary>
108
+
109
+ Add to `.windsurf/mcp.json`:
34
110
 
35
111
  ```json
36
112
  {
@@ -43,12 +119,16 @@ Copy the JSON block below into **one** of these files depending on your AI assis
43
119
  }
44
120
  ```
45
121
 
122
+ </details>
123
+
46
124
  > **That's it!** Your AI assistant can now query your codebase structure, dependencies, and insights.
47
125
 
48
126
  ---
49
127
 
50
128
  ## 🛠️ Available Tools
51
129
 
130
+ ### Code Analysis (6 tools)
131
+
52
132
  | Tool | Description |
53
133
  |------|-------------|
54
134
  | `list_projects` | List all analyzed projects (auto-discovers `~/`) |
@@ -58,6 +138,53 @@ Copy the JSON block below into **one** of these files depending on your AI assis
58
138
  | `search_entities` | Search functions, classes by name (fuzzy match) |
59
139
  | `get_file_entities` | Get all entities defined in a specific file |
60
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
+
61
188
  ---
62
189
 
63
190
  ## 📦 Alternative: Global Install
@@ -68,17 +195,7 @@ If you prefer installing globally instead of using `npx`:
68
195
  npm install -g @giauphan/codeatlas-mcp
69
196
  ```
70
197
 
71
- Then update your MCP config to:
72
-
73
- ```json
74
- {
75
- "mcpServers": {
76
- "codeatlas": {
77
- "command": "codeatlas-mcp"
78
- }
79
- }
80
- }
81
- ```
198
+ Then use `"command": "codeatlas-mcp"` (no `args` needed) in your MCP config.
82
199
 
83
200
  ---
84
201
 
@@ -92,6 +209,17 @@ Then update your MCP config to:
92
209
 
93
210
  ---
94
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>` |
220
+
221
+ ---
222
+
95
223
  ## 🧑‍💻 Development
96
224
 
97
225
  ```bash
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.4",
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",