@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.
- package/README.md +147 -19
- package/dist/index.js +466 -1
- package/index.ts +519 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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