codeatlas-mcp-server 2.20.2
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/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/dist/src/analyzer/parser.js +1072 -0
- package/dist/src/analyzer/parser.js.map +1 -0
- package/dist/src/analyzer/parser.test.js +73 -0
- package/dist/src/analyzer/parser.test.js.map +1 -0
- package/dist/src/analyzer/phpParser.js +147 -0
- package/dist/src/analyzer/phpParser.js.map +1 -0
- package/dist/src/analyzer/pythonParser.js +185 -0
- package/dist/src/analyzer/pythonParser.js.map +1 -0
- package/dist/src/analyzer/types.js +2 -0
- package/dist/src/analyzer/types.js.map +1 -0
- package/dist/src/context.js +3 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/memoryGenerator.js +293 -0
- package/dist/src/memoryGenerator.js.map +1 -0
- package/dist/src/oracleDatabase.js +298 -0
- package/dist/src/oracleDatabase.js.map +1 -0
- package/dist/src/presentation/httpServer.js +306 -0
- package/dist/src/presentation/httpServer.js.map +1 -0
- package/dist/src/presentation/mcpServer.js +1487 -0
- package/dist/src/presentation/mcpServer.js.map +1 -0
- package/dist/src/repositories.js +144 -0
- package/dist/src/repositories.js.map +1 -0
- package/dist/src/securityScanner.js +69 -0
- package/dist/src/securityScanner.js.map +1 -0
- package/dist/src/services/authService.js +24 -0
- package/dist/src/services/authService.js.map +1 -0
- package/dist/src/services/dreamingService.js +119 -0
- package/dist/src/services/dreamingService.js.map +1 -0
- package/dist/src/services/dreamingService.test.js +179 -0
- package/dist/src/services/dreamingService.test.js.map +1 -0
- package/dist/src/services/projectService.js +1068 -0
- package/dist/src/services/projectService.js.map +1 -0
- package/dist/src/services/projectService.test.js +217 -0
- package/dist/src/services/projectService.test.js.map +1 -0
- package/dist/src/services/watcherService.js +164 -0
- package/dist/src/services/watcherService.js.map +1 -0
- package/dist/src/services/watcherService.test.js +65 -0
- package/dist/src/services/watcherService.test.js.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1487 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import { checkAuth, logActivity } from "../services/authService.js";
|
|
7
|
+
import { discoverProjectsAsync, loadAnalysisAsync, getStats, fileExists, syncAnalysisToServer, getEpisodicMemoriesFromServer, inMemoryAnalysisCache } from "../services/projectService.js";
|
|
8
|
+
import { saveDreamMemory, queryDreamMemories } from "../services/dreamingService.js";
|
|
9
|
+
import { CodeAnalyzer } from "../analyzer/parser.js";
|
|
10
|
+
import { SecurityScanner } from "../securityScanner.js";
|
|
11
|
+
export function registerTools(server) {
|
|
12
|
+
// Tool -1: Analyze a project
|
|
13
|
+
server.tool("analyze", "Perform deep code analysis on a local project directory. Generates AST analysis in memory and syncs to CodeAtlas Cloud.", {
|
|
14
|
+
path: z.string().describe("Absolute path to the project directory to analyze"),
|
|
15
|
+
maxFiles: z.number().optional().describe("Maximum files to analyze (default: 5000)"),
|
|
16
|
+
}, async ({ path: projectPath, maxFiles }) => {
|
|
17
|
+
const auth = await checkAuth();
|
|
18
|
+
await logActivity(auth, "analyze", { path: projectPath, maxFiles });
|
|
19
|
+
if (!(await fileExists(projectPath))) {
|
|
20
|
+
return { content: [{ type: "text", text: `Error: Directory does not exist: ${projectPath}` }] };
|
|
21
|
+
}
|
|
22
|
+
// Safety: reject paths that are the user's home directory or system roots
|
|
23
|
+
const resolvedPath = path.resolve(projectPath);
|
|
24
|
+
const homeDir = os.homedir();
|
|
25
|
+
if (resolvedPath === homeDir || resolvedPath === "/" || resolvedPath === "/home") {
|
|
26
|
+
return { content: [{ type: "text", text: `Error: Refusing to analyze '${resolvedPath}' — path is too broad. Please specify a project subdirectory.` }] };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const analyzer = new CodeAnalyzer(projectPath, maxFiles || 5000);
|
|
30
|
+
const result = await analyzer.analyzeProject();
|
|
31
|
+
// Save in-memory cache
|
|
32
|
+
inMemoryAnalysisCache.set(path.resolve(projectPath), result);
|
|
33
|
+
// Sync to cloud server
|
|
34
|
+
try {
|
|
35
|
+
await syncAnalysisToServer(path.basename(projectPath), result);
|
|
36
|
+
}
|
|
37
|
+
catch (syncErr) {
|
|
38
|
+
console.error(`[Analyze-Tool] ❌ Background cloud sync failed: ${syncErr}`);
|
|
39
|
+
}
|
|
40
|
+
const stats = getStats(result);
|
|
41
|
+
const summary = `Analysis complete for ${path.basename(projectPath)}:
|
|
42
|
+
- Modules: ${stats.modules}
|
|
43
|
+
- Functions: ${stats.functions}
|
|
44
|
+
- Classes: ${stats.classes}
|
|
45
|
+
- Dependencies: ${stats.dependencies}
|
|
46
|
+
- Total files: ${result.totalFilesAnalyzed}
|
|
47
|
+
- Files skipped: ${result.totalFilesSkipped}
|
|
48
|
+
(Data kept in memory and background sync to CodeAtlas Cloud initiated)`;
|
|
49
|
+
return { content: [{ type: "text", text: summary }] };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { content: [{ type: "text", text: `Analysis failed: ${(error instanceof Error ? error.message : String(error))}` }] };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// Tool 0: List all discovered projects
|
|
56
|
+
server.tool("list_projects", "List all projects that have been analyzed by CodeAtlas. Returns project names, paths, and last analysis time.", {}, async () => {
|
|
57
|
+
const auth = await checkAuth();
|
|
58
|
+
await logActivity(auth, "list_projects", {});
|
|
59
|
+
const projects = await discoverProjectsAsync(auth.uid);
|
|
60
|
+
if (projects.length === 0) {
|
|
61
|
+
return { content: [{ type: "text", text: "No analyzed projects found. Run 'analyze' tool first." }] };
|
|
62
|
+
}
|
|
63
|
+
const result = {
|
|
64
|
+
projectCount: projects.length,
|
|
65
|
+
projects: projects.map((p) => ({
|
|
66
|
+
name: p.name,
|
|
67
|
+
path: p.dir,
|
|
68
|
+
lastAnalyzed: p.modifiedAt.toISOString(),
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
72
|
+
});
|
|
73
|
+
// Tool 1: Get project structure
|
|
74
|
+
server.tool("get_project_structure", "Get all modules, classes, functions, and variables in the analyzed project. Returns entity type, name, file path, and line number.", {
|
|
75
|
+
project: z.string().optional().describe("Project name or path (auto-detects if omitted)"),
|
|
76
|
+
type: z.enum(["all", "module", "class", "function", "variable"]).optional().describe("Filter by entity type"),
|
|
77
|
+
limit: z.number().optional().describe("Max results to return (default: 100)"),
|
|
78
|
+
}, async ({ project, type, limit }) => {
|
|
79
|
+
const auth = await checkAuth();
|
|
80
|
+
await logActivity(auth, "get_project_structure", { project, type, limit });
|
|
81
|
+
const loaded = await loadAnalysisAsync(project);
|
|
82
|
+
if (!loaded) {
|
|
83
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
84
|
+
}
|
|
85
|
+
let nodes = loaded.analysis.graph.nodes;
|
|
86
|
+
if (type && type !== "all") {
|
|
87
|
+
nodes = nodes.filter((n) => n.type === type);
|
|
88
|
+
}
|
|
89
|
+
// Filter out venv/node_modules entities
|
|
90
|
+
nodes = nodes.filter((n) => {
|
|
91
|
+
const fp = n.filePath || "";
|
|
92
|
+
return !fp.includes("node_modules") && !fp.includes("venv") && !fp.includes(".venv") && !fp.includes("site-packages");
|
|
93
|
+
});
|
|
94
|
+
const maxResults = limit || 500;
|
|
95
|
+
const truncated = nodes.length > maxResults;
|
|
96
|
+
nodes = nodes.slice(0, maxResults);
|
|
97
|
+
const stats = getStats(loaded.analysis);
|
|
98
|
+
const result = {
|
|
99
|
+
project: loaded.projectName,
|
|
100
|
+
projectDir: loaded.projectDir,
|
|
101
|
+
total: loaded.analysis.graph.nodes.length,
|
|
102
|
+
showing: nodes.length,
|
|
103
|
+
truncated,
|
|
104
|
+
stats,
|
|
105
|
+
entities: nodes.map((n) => ({
|
|
106
|
+
name: n.label,
|
|
107
|
+
type: n.type,
|
|
108
|
+
filePath: n.filePath ? (path.isAbsolute(n.filePath) ? n.filePath : path.resolve(loaded.projectDir, n.filePath)) : null,
|
|
109
|
+
line: n.line || null,
|
|
110
|
+
})),
|
|
111
|
+
};
|
|
112
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
113
|
+
});
|
|
114
|
+
// Tool 2: Get dependencies
|
|
115
|
+
server.tool("get_dependencies", "Get import/call/containment/implements relationships between entities. Shows how modules, classes, and functions are connected.", {
|
|
116
|
+
project: z.string().optional().describe("Project name or path"),
|
|
117
|
+
source: z.string().optional().describe("Filter by source entity name"),
|
|
118
|
+
target: z.string().optional().describe("Filter by target entity name"),
|
|
119
|
+
relationship: z.enum(["all", "import", "call", "contains", "implements"]).optional().describe("Filter by relationship type"),
|
|
120
|
+
limit: z.number().optional().describe("Max results (default: 100)"),
|
|
121
|
+
}, async ({ project, source, target, relationship, limit }) => {
|
|
122
|
+
const auth = await checkAuth();
|
|
123
|
+
await logActivity(auth, "get_dependencies", { project, source, target, relationship, limit });
|
|
124
|
+
const loaded = await loadAnalysisAsync(project);
|
|
125
|
+
if (!loaded) {
|
|
126
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
127
|
+
}
|
|
128
|
+
const nodeMap = new Map(loaded.analysis.graph.nodes.map((n) => [n.id, n.label]));
|
|
129
|
+
let links = loaded.analysis.graph.links;
|
|
130
|
+
if (relationship && relationship !== "all") {
|
|
131
|
+
links = links.filter((l) => l.type === relationship);
|
|
132
|
+
}
|
|
133
|
+
if (source) {
|
|
134
|
+
links = links.filter((l) => {
|
|
135
|
+
const label = nodeMap.get(l.source) || l.source;
|
|
136
|
+
return label.toLowerCase().includes(source.toLowerCase());
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (target) {
|
|
140
|
+
links = links.filter((l) => {
|
|
141
|
+
const label = nodeMap.get(l.target) || l.target;
|
|
142
|
+
return label.toLowerCase().includes(target.toLowerCase());
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Deduplicate links
|
|
146
|
+
const linkDedup = new Set();
|
|
147
|
+
links = links.filter((l) => {
|
|
148
|
+
const key = l.source + '|' + l.target + '|' + l.type;
|
|
149
|
+
if (linkDedup.has(key))
|
|
150
|
+
return false;
|
|
151
|
+
linkDedup.add(key);
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
const maxResults = limit || 100;
|
|
155
|
+
const truncated = links.length > maxResults;
|
|
156
|
+
links = links.slice(0, maxResults);
|
|
157
|
+
const result = {
|
|
158
|
+
total: loaded.analysis.graph.links.length,
|
|
159
|
+
showing: links.length,
|
|
160
|
+
truncated,
|
|
161
|
+
dependencies: links.map((l) => ({
|
|
162
|
+
source: nodeMap.get(l.source) || l.source,
|
|
163
|
+
target: nodeMap.get(l.target) || l.target,
|
|
164
|
+
type: l.type,
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
167
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
168
|
+
});
|
|
169
|
+
// Tool 3: Get AI insights
|
|
170
|
+
server.tool("get_insights", "Get AI-generated code insights including refactoring suggestions, security issues, and maintainability analysis.", {}, async () => {
|
|
171
|
+
const auth = await checkAuth();
|
|
172
|
+
await logActivity(auth, "get_insights", {});
|
|
173
|
+
const loaded = await loadAnalysisAsync();
|
|
174
|
+
if (!loaded) {
|
|
175
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
176
|
+
}
|
|
177
|
+
const stats = getStats(loaded.analysis);
|
|
178
|
+
const result = {
|
|
179
|
+
project: loaded.projectName,
|
|
180
|
+
stats,
|
|
181
|
+
insights: loaded.analysis.insights,
|
|
182
|
+
};
|
|
183
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
184
|
+
});
|
|
185
|
+
// Tool 4: Search entities
|
|
186
|
+
server.tool("search_entities", "Search for functions, classes, modules, or variables by name. Supports fuzzy matching.", {
|
|
187
|
+
project: z.string().optional().describe("Project name or path"),
|
|
188
|
+
query: z.string().describe("Search query (case-insensitive, partial match)"),
|
|
189
|
+
type: z.enum(["all", "module", "class", "function", "variable"]).optional().describe("Filter by entity type"),
|
|
190
|
+
}, async ({ project, query, type }) => {
|
|
191
|
+
const auth = await checkAuth();
|
|
192
|
+
await logActivity(auth, "search_entities", { project, query, type });
|
|
193
|
+
const loaded = await loadAnalysisAsync(project);
|
|
194
|
+
if (!loaded) {
|
|
195
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
196
|
+
}
|
|
197
|
+
let nodes = loaded.analysis.graph.nodes;
|
|
198
|
+
if (type && type !== "all") {
|
|
199
|
+
nodes = nodes.filter((n) => n.type === type);
|
|
200
|
+
}
|
|
201
|
+
// Filter out venv/node_modules entities for cleaner results
|
|
202
|
+
nodes = nodes.filter((n) => {
|
|
203
|
+
if (n.id.startsWith('external:'))
|
|
204
|
+
return false;
|
|
205
|
+
if (n.filePath && (n.filePath.includes('/venv/') ||
|
|
206
|
+
n.filePath.includes('/.venv/') ||
|
|
207
|
+
n.filePath.includes('/node_modules/') ||
|
|
208
|
+
n.filePath.includes('/site-packages/')))
|
|
209
|
+
return false;
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
const q = query.toLowerCase();
|
|
213
|
+
const matches = nodes.filter((n) => n.label.toLowerCase().includes(q));
|
|
214
|
+
// For each match, find its relationships
|
|
215
|
+
const links = loaded.analysis.graph.links;
|
|
216
|
+
const nodeMap = new Map(loaded.analysis.graph.nodes.map((n) => [n.id, n.label]));
|
|
217
|
+
const result = {
|
|
218
|
+
query,
|
|
219
|
+
matchCount: matches.length,
|
|
220
|
+
results: matches.slice(0, 50).map((n) => {
|
|
221
|
+
const incomingLinks = links
|
|
222
|
+
.filter((l) => l.target === n.id)
|
|
223
|
+
.map((l) => ({ from: nodeMap.get(l.source) || l.source, type: l.type }));
|
|
224
|
+
const outgoingLinks = links
|
|
225
|
+
.filter((l) => l.source === n.id)
|
|
226
|
+
.map((l) => ({ to: nodeMap.get(l.target) || l.target, type: l.type }));
|
|
227
|
+
return {
|
|
228
|
+
name: n.label,
|
|
229
|
+
type: n.type,
|
|
230
|
+
filePath: n.filePath ? (path.isAbsolute(n.filePath) ? n.filePath : path.resolve(loaded.projectDir, n.filePath)) : null,
|
|
231
|
+
line: n.line || null,
|
|
232
|
+
incomingRelationships: incomingLinks,
|
|
233
|
+
outgoingRelationships: outgoingLinks,
|
|
234
|
+
};
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
238
|
+
});
|
|
239
|
+
// Tool 5: Get file entities
|
|
240
|
+
server.tool("get_file_entities", "Get all entities (classes, functions, variables) defined in a specific file.", {
|
|
241
|
+
project: z.string().optional().describe("Project name or path"),
|
|
242
|
+
filePath: z.string().describe("File path (partial match, e.g. 'User.php' or 'src/models')"),
|
|
243
|
+
}, async ({ filePath, project }) => {
|
|
244
|
+
const auth = await checkAuth();
|
|
245
|
+
await logActivity(auth, "get_file_entities", { filePath, project });
|
|
246
|
+
const loaded = await loadAnalysisAsync(project);
|
|
247
|
+
if (!loaded) {
|
|
248
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
249
|
+
}
|
|
250
|
+
const q = filePath.toLowerCase().replace(/\\/g, "/");
|
|
251
|
+
const matches = loaded.analysis.graph.nodes.filter((n) => {
|
|
252
|
+
const fp = (n.filePath || n.id).toLowerCase().replace(/\\/g, "/");
|
|
253
|
+
return fp.includes(q);
|
|
254
|
+
});
|
|
255
|
+
const links = loaded.analysis.graph.links;
|
|
256
|
+
const nodeMap = new Map(loaded.analysis.graph.nodes.map((n) => [n.id, n.label]));
|
|
257
|
+
// Group by file
|
|
258
|
+
const byFile = new Map();
|
|
259
|
+
for (const n of matches) {
|
|
260
|
+
const fp = n.filePath || "unknown";
|
|
261
|
+
if (!byFile.has(fp))
|
|
262
|
+
byFile.set(fp, []);
|
|
263
|
+
byFile.get(fp).push(n);
|
|
264
|
+
}
|
|
265
|
+
let filesEntries = Array.from(byFile.entries());
|
|
266
|
+
const result = {
|
|
267
|
+
query: filePath,
|
|
268
|
+
filesFound: byFile.size,
|
|
269
|
+
showing: filesEntries.length,
|
|
270
|
+
truncated: byFile.size > filesEntries.length,
|
|
271
|
+
files: filesEntries.map(([fp, entities]) => ({
|
|
272
|
+
filePath: fp === "unknown" ? "unknown" : (path.isAbsolute(fp) ? fp : path.resolve(loaded.projectDir, fp)),
|
|
273
|
+
entities: entities.map((e) => ({
|
|
274
|
+
name: e.label,
|
|
275
|
+
type: e.type,
|
|
276
|
+
line: e.line || null,
|
|
277
|
+
dependencies: links
|
|
278
|
+
.filter((l) => l.source === e.id)
|
|
279
|
+
.map((l) => ({ to: nodeMap.get(l.target) || l.target, type: l.type })),
|
|
280
|
+
})),
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
284
|
+
});
|
|
285
|
+
// Tool 6: Generate System Flow
|
|
286
|
+
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.", {
|
|
287
|
+
project: z.string().optional().describe("Project name or path"),
|
|
288
|
+
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"),
|
|
289
|
+
feature: z.string().optional().describe("Feature keyword to focus the diagram on (e.g. 'auth', 'crawl', 'payment'). Only used when scope='feature'"),
|
|
290
|
+
maxNodes: z.number().optional().describe("Maximum nodes in diagram (default: 60). Reduce for large projects"),
|
|
291
|
+
}, async ({ project, scope, feature, maxNodes }) => {
|
|
292
|
+
const auth = await checkAuth();
|
|
293
|
+
await logActivity(auth, "generate_system_flow", { project, scope, feature, maxNodes });
|
|
294
|
+
const loaded = await loadAnalysisAsync(project);
|
|
295
|
+
if (!loaded) {
|
|
296
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
297
|
+
}
|
|
298
|
+
const max = maxNodes || 60;
|
|
299
|
+
const diagramScope = scope || "modules-only";
|
|
300
|
+
let nodes = loaded.analysis.graph.nodes;
|
|
301
|
+
let links = loaded.analysis.graph.links;
|
|
302
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
303
|
+
// Filter by scope
|
|
304
|
+
if (diagramScope === "modules-only") {
|
|
305
|
+
nodes = nodes.filter((n) => n.type === "module" && (n.filePath || n.id.startsWith("external:")));
|
|
306
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
307
|
+
links = links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target) && l.type === "import");
|
|
308
|
+
}
|
|
309
|
+
else if (diagramScope === "feature" && feature) {
|
|
310
|
+
const q = feature.toLowerCase();
|
|
311
|
+
const matchingNodes = new Set();
|
|
312
|
+
nodes.forEach((n) => {
|
|
313
|
+
if (n.label.toLowerCase().includes(q) || (n.filePath && n.filePath.toLowerCase().includes(q))) {
|
|
314
|
+
matchingNodes.add(n.id);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
links.forEach((l) => {
|
|
318
|
+
if (matchingNodes.has(l.source))
|
|
319
|
+
matchingNodes.add(l.target);
|
|
320
|
+
if (matchingNodes.has(l.target))
|
|
321
|
+
matchingNodes.add(l.source);
|
|
322
|
+
});
|
|
323
|
+
nodes = nodes.filter((n) => matchingNodes.has(n.id));
|
|
324
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
325
|
+
links = links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
326
|
+
}
|
|
327
|
+
// Truncate if too many nodes
|
|
328
|
+
if (nodes.length > max) {
|
|
329
|
+
const priorityOrder = ["module", "class", "function", "variable"];
|
|
330
|
+
nodes.sort((a, b) => {
|
|
331
|
+
const ia = priorityOrder.indexOf(a.type);
|
|
332
|
+
const ib = priorityOrder.indexOf(b.type);
|
|
333
|
+
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
|
|
334
|
+
});
|
|
335
|
+
nodes = nodes.slice(0, max);
|
|
336
|
+
}
|
|
337
|
+
const truncatedNodeIds = new Set(nodes.map((n) => n.id));
|
|
338
|
+
links = links.filter((l) => truncatedNodeIds.has(l.source) && truncatedNodeIds.has(l.target));
|
|
339
|
+
// Remove duplicate links
|
|
340
|
+
const linkSet = new Set();
|
|
341
|
+
links = links.filter((l) => {
|
|
342
|
+
const key = `${l.source}|${l.target}|${l.type}`;
|
|
343
|
+
if (linkSet.has(key))
|
|
344
|
+
return false;
|
|
345
|
+
linkSet.add(key);
|
|
346
|
+
return true;
|
|
347
|
+
});
|
|
348
|
+
// Build Mermaid diagram
|
|
349
|
+
const nodeIdMap = new Map();
|
|
350
|
+
let counter = 0;
|
|
351
|
+
const getMermaidId = (nodeId) => {
|
|
352
|
+
if (!nodeIdMap.has(nodeId)) {
|
|
353
|
+
nodeIdMap.set(nodeId, `n${counter++}`);
|
|
354
|
+
}
|
|
355
|
+
return nodeIdMap.get(nodeId);
|
|
356
|
+
};
|
|
357
|
+
const lines = ["graph TD"];
|
|
358
|
+
for (const node of nodes) {
|
|
359
|
+
const mid = getMermaidId(node.id);
|
|
360
|
+
const label = node.label.replace(/"/g, "'");
|
|
361
|
+
const typeIcon = node.type === "module" ? "📄" : node.type === "class" ? "🏗️" : node.type === "function" ? "⚡" : "📦";
|
|
362
|
+
if (node.type === "module") {
|
|
363
|
+
lines.push(` ${mid}["${typeIcon} ${label}"]`);
|
|
364
|
+
}
|
|
365
|
+
else if (node.type === "class") {
|
|
366
|
+
lines.push(` ${mid}[["${typeIcon} ${label}"]]`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
lines.push(` ${mid}("${typeIcon} ${label}")`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const arrowMap = { import: "-->", call: "-.->", contains: "-->", implements: "-.->|implements|" };
|
|
373
|
+
const labelMap = { import: "imports", call: "calls", contains: "contains", implements: "implements" };
|
|
374
|
+
for (const link of links) {
|
|
375
|
+
const src = getMermaidId(link.source);
|
|
376
|
+
const tgt = getMermaidId(link.target);
|
|
377
|
+
if (src && tgt) {
|
|
378
|
+
const arrow = arrowMap[link.type] || "-->";
|
|
379
|
+
if (link.type === "contains") {
|
|
380
|
+
lines.push(` ${src} ${arrow} ${tgt}`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
lines.push(` ${src} ${arrow}|${labelMap[link.type] || link.type}| ${tgt}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const mermaid = lines.join("\n");
|
|
388
|
+
const result = {
|
|
389
|
+
project: loaded.projectName,
|
|
390
|
+
scope: diagramScope,
|
|
391
|
+
feature: feature || null,
|
|
392
|
+
nodeCount: nodes.length,
|
|
393
|
+
linkCount: links.length,
|
|
394
|
+
truncated: loaded.analysis.graph.nodes.length > max,
|
|
395
|
+
mermaidDiagram: mermaid,
|
|
396
|
+
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.`,
|
|
397
|
+
};
|
|
398
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
399
|
+
});
|
|
400
|
+
// Tool 7: Sync System Memory
|
|
401
|
+
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.", {
|
|
402
|
+
project: z.string().optional().describe("Project name or path"),
|
|
403
|
+
businessRule: z.string().optional().describe("Optional: A new business rule to add to the memory (e.g. 'VIP users get free shipping')"),
|
|
404
|
+
changeDescription: z.string().optional().describe("Optional: Description of what was just changed (for the changelog)"),
|
|
405
|
+
enableEnterpriseSync: z.boolean().optional().default(true).describe("If true, syncs data to Oracle 26ai Knowledge Graph (Pro/Plus feature). Default is true."),
|
|
406
|
+
}, async ({ project, businessRule, changeDescription, enableEnterpriseSync }) => {
|
|
407
|
+
const auth = await checkAuth();
|
|
408
|
+
await logActivity(auth, "sync_system_memory", { project, businessRule, changeDescription, enableEnterpriseSync });
|
|
409
|
+
const loaded = await loadAnalysisAsync(project);
|
|
410
|
+
if (!loaded) {
|
|
411
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
412
|
+
}
|
|
413
|
+
const nodes = loaded.analysis.graph.nodes;
|
|
414
|
+
const links = loaded.analysis.graph.links;
|
|
415
|
+
let syncSuccess = false;
|
|
416
|
+
let syncError;
|
|
417
|
+
// Sync local analysis to CodeAtlas Cloud
|
|
418
|
+
if (enableEnterpriseSync !== false) {
|
|
419
|
+
try {
|
|
420
|
+
console.error(`Syncing Knowledge Graph for ${loaded.projectName} to CodeAtlas Cloud...`);
|
|
421
|
+
await syncAnalysisToServer(loaded.projectName, loaded.analysis, businessRule, changeDescription);
|
|
422
|
+
syncSuccess = true;
|
|
423
|
+
}
|
|
424
|
+
catch (syncErr) {
|
|
425
|
+
syncError = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
426
|
+
console.error("Failed to sync memory to CodeAtlas Cloud:", syncErr);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
if (businessRule || changeDescription) {
|
|
431
|
+
syncError = "Sync skipped (enableEnterpriseSync is false), cannot save episodic memory.";
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
syncSuccess = true; // No episodic memory requested, so no-op is considered success
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const result = {
|
|
438
|
+
success: syncSuccess,
|
|
439
|
+
project: loaded.projectName,
|
|
440
|
+
stats: {
|
|
441
|
+
modules: nodes.filter((n) => n.type === "module").length,
|
|
442
|
+
totalEntities: nodes.length,
|
|
443
|
+
totalLinks: links.length,
|
|
444
|
+
businessRuleSaved: syncSuccess && !!businessRule,
|
|
445
|
+
changeDescriptionSaved: syncSuccess && !!changeDescription,
|
|
446
|
+
},
|
|
447
|
+
error: syncError,
|
|
448
|
+
message: syncSuccess
|
|
449
|
+
? (enableEnterpriseSync !== false
|
|
450
|
+
? `System memory synced to CodeAtlas Cloud for ${loaded.projectName}. Local file writing deprecated.`
|
|
451
|
+
: `System memory sync skipped (no-op success).`)
|
|
452
|
+
: `System memory sync failed or skipped: ${syncError}`,
|
|
453
|
+
};
|
|
454
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
455
|
+
});
|
|
456
|
+
// Tool 7.5: Get System Memory (Episodic memories like business rules and change logs)
|
|
457
|
+
server.tool("get_system_memory", "Retrieve the auto-generated system documentation and episodic memories (business rules and change logs) for a project from CodeAtlas Cloud / Oracle 26ai.", {
|
|
458
|
+
project: z.string().optional().describe("Project name or path"),
|
|
459
|
+
eventType: z.enum(["all", "BUSINESS_RULE", "CHANGE_LOG"]).optional().default("all").describe("Filter by event type"),
|
|
460
|
+
}, async ({ project, eventType }) => {
|
|
461
|
+
const auth = await checkAuth();
|
|
462
|
+
await logActivity(auth, "get_system_memory", { project, eventType });
|
|
463
|
+
const loaded = await loadAnalysisAsync(project);
|
|
464
|
+
if (!loaded) {
|
|
465
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
const filterType = eventType === "all" ? undefined : eventType;
|
|
469
|
+
const memories = await getEpisodicMemoriesFromServer(loaded.projectName, filterType);
|
|
470
|
+
const result = {
|
|
471
|
+
success: true,
|
|
472
|
+
project: loaded.projectName,
|
|
473
|
+
count: memories.length,
|
|
474
|
+
memories: memories
|
|
475
|
+
};
|
|
476
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
return { content: [{ type: "text", text: `Failed to retrieve system memory: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
// Tool 8a: Save Dream Memory
|
|
483
|
+
server.tool("save_dream_memory", "Save a dream memory (mistake, preference, knowledge, or pattern) to CodeAtlas Cloud for long-term AI recall. The AI uses this to persist learnings across conversations.", {
|
|
484
|
+
memory_type: z.enum(["MISTAKE", "PREFERENCE", "KNOWLEDGE", "PATTERN"]).describe("Category of the memory"),
|
|
485
|
+
content: z.string().describe("The actual memory content or insight"),
|
|
486
|
+
importance: z.number().min(1).max(10).optional().describe("Importance level from 1 (low) to 10 (critical). Defaults to 5."),
|
|
487
|
+
session_id: z.string().optional().describe("Optional session identifier for grouping related memories"),
|
|
488
|
+
project: z.string().optional().describe("Optional project name to associate this memory with"),
|
|
489
|
+
}, async ({ memory_type, content, importance, session_id, project }) => {
|
|
490
|
+
const auth = await checkAuth();
|
|
491
|
+
await logActivity(auth, "save_dream_memory", { memory_type, content: content.substring(0, 100), importance, session_id, project });
|
|
492
|
+
try {
|
|
493
|
+
const result = await saveDreamMemory({
|
|
494
|
+
memory_type,
|
|
495
|
+
content,
|
|
496
|
+
importance: importance || 5,
|
|
497
|
+
session_id,
|
|
498
|
+
project,
|
|
499
|
+
});
|
|
500
|
+
return {
|
|
501
|
+
content: [{
|
|
502
|
+
type: "text",
|
|
503
|
+
text: JSON.stringify({
|
|
504
|
+
success: result.success,
|
|
505
|
+
id: result.id,
|
|
506
|
+
memory_type,
|
|
507
|
+
message: `Dream memory saved successfully with id: ${result.id}`,
|
|
508
|
+
}, null, 2),
|
|
509
|
+
}],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
return {
|
|
514
|
+
content: [{
|
|
515
|
+
type: "text",
|
|
516
|
+
text: `Failed to save dream memory: ${err instanceof Error ? err.message : String(err)}`,
|
|
517
|
+
}],
|
|
518
|
+
isError: true,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
// Tool 8b: Query Dream Memories
|
|
523
|
+
server.tool("query_dream_memories", "Query previously saved dream memories from CodeAtlas Cloud. Uses semantic search to find relevant memories based on the query text. Returns memories with relevance scores.", {
|
|
524
|
+
query: z.string().describe("Natural language query to search for relevant memories"),
|
|
525
|
+
project: z.string().optional().describe("Optional project name filter to scope the search"),
|
|
526
|
+
limit: z.number().min(1).max(100).optional().default(10).describe("Maximum number of results to return (default: 10, max: 100)"),
|
|
527
|
+
}, async ({ query, project, limit }) => {
|
|
528
|
+
const auth = await checkAuth();
|
|
529
|
+
await logActivity(auth, "query_dream_memories", { query: query.substring(0, 100), project, limit });
|
|
530
|
+
try {
|
|
531
|
+
const memories = await queryDreamMemories({
|
|
532
|
+
query,
|
|
533
|
+
project,
|
|
534
|
+
limit: limit || 10,
|
|
535
|
+
});
|
|
536
|
+
return {
|
|
537
|
+
content: [{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: JSON.stringify({
|
|
540
|
+
success: true,
|
|
541
|
+
count: memories.length,
|
|
542
|
+
query,
|
|
543
|
+
memories,
|
|
544
|
+
}, null, 2),
|
|
545
|
+
}],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
return {
|
|
550
|
+
content: [{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: `Failed to query dream memories: ${err instanceof Error ? err.message : String(err)}`,
|
|
553
|
+
}],
|
|
554
|
+
isError: true,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
// Tool 8c: Trace Feature Flow
|
|
559
|
+
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.", {
|
|
560
|
+
project: z.string().optional().describe("Project name or path"),
|
|
561
|
+
keyword: z.string().describe("Feature keyword to trace (e.g. 'auth', 'crawl', 'payment', 'upload')"),
|
|
562
|
+
depth: z.number().optional().describe("How many hops to follow from matching nodes (default: 2)"),
|
|
563
|
+
}, async ({ keyword, project, depth }) => {
|
|
564
|
+
const auth = await checkAuth();
|
|
565
|
+
await logActivity(auth, "trace_feature_flow", { keyword, project, depth });
|
|
566
|
+
const loaded = await loadAnalysisAsync(project);
|
|
567
|
+
if (!loaded) {
|
|
568
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
569
|
+
}
|
|
570
|
+
const maxDepth = depth || 2;
|
|
571
|
+
const q = keyword.toLowerCase();
|
|
572
|
+
const nodes = loaded.analysis.graph.nodes;
|
|
573
|
+
const links = loaded.analysis.graph.links;
|
|
574
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
575
|
+
const seedNodes = new Set();
|
|
576
|
+
for (const node of nodes) {
|
|
577
|
+
if (node.id.startsWith('external:'))
|
|
578
|
+
continue;
|
|
579
|
+
if (node.filePath && (node.filePath.includes('/venv/') ||
|
|
580
|
+
node.filePath.includes('/.venv/') ||
|
|
581
|
+
node.filePath.includes('/node_modules/') ||
|
|
582
|
+
node.filePath.includes('/vendor/') ||
|
|
583
|
+
node.filePath.includes('/site-packages/')))
|
|
584
|
+
continue;
|
|
585
|
+
if (node.label.toLowerCase().includes(q) ||
|
|
586
|
+
(node.filePath && node.filePath.toLowerCase().includes(q)) ||
|
|
587
|
+
node.id.toLowerCase().includes(q)) {
|
|
588
|
+
seedNodes.add(node.id);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (seedNodes.size === 0) {
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: "text",
|
|
596
|
+
text: JSON.stringify({
|
|
597
|
+
keyword,
|
|
598
|
+
matchCount: 0,
|
|
599
|
+
message: `No entities found matching '${keyword}'. Try a broader keyword.`,
|
|
600
|
+
suggestions: nodes
|
|
601
|
+
.filter((n) => n.type === "module" && n.filePath)
|
|
602
|
+
.map((n) => n.label)
|
|
603
|
+
.slice(0, 10),
|
|
604
|
+
}, null, 2),
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const visited = new Set(seedNodes);
|
|
610
|
+
let frontier = new Set(seedNodes);
|
|
611
|
+
for (let d = 0; d < maxDepth; d++) {
|
|
612
|
+
const nextFrontier = new Set();
|
|
613
|
+
for (const link of links) {
|
|
614
|
+
if (frontier.has(link.source) && !visited.has(link.target)) {
|
|
615
|
+
nextFrontier.add(link.target);
|
|
616
|
+
visited.add(link.target);
|
|
617
|
+
}
|
|
618
|
+
if (frontier.has(link.target) && !visited.has(link.source)) {
|
|
619
|
+
nextFrontier.add(link.source);
|
|
620
|
+
visited.add(link.source);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
frontier = nextFrontier;
|
|
624
|
+
}
|
|
625
|
+
const traceNodes = nodes.filter((n) => visited.has(n.id));
|
|
626
|
+
const traceLinks = links.filter((l) => visited.has(l.source) && visited.has(l.target));
|
|
627
|
+
const byFile = new Map();
|
|
628
|
+
for (const node of traceNodes) {
|
|
629
|
+
const filePath = node.filePath || "external";
|
|
630
|
+
if (!byFile.has(filePath))
|
|
631
|
+
byFile.set(filePath, []);
|
|
632
|
+
byFile.get(filePath).push({
|
|
633
|
+
name: node.label,
|
|
634
|
+
type: node.type,
|
|
635
|
+
isSeed: seedNodes.has(node.id),
|
|
636
|
+
line: node.line || null,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const filesArray = Array.from(byFile.entries())
|
|
640
|
+
.map(([filePath, entities]) => {
|
|
641
|
+
const isExt = filePath === "external";
|
|
642
|
+
const relPath = isExt ? "external" : (path.isAbsolute(filePath) ? path.relative(loaded.projectDir, filePath) : filePath);
|
|
643
|
+
const absPath = isExt ? "external" : (path.isAbsolute(filePath) ? filePath : path.resolve(loaded.projectDir, filePath));
|
|
644
|
+
return {
|
|
645
|
+
filePath: relPath,
|
|
646
|
+
absolutePath: absPath,
|
|
647
|
+
entities,
|
|
648
|
+
hasSeedMatch: entities.some((e) => e.isSeed),
|
|
649
|
+
entityCount: entities.length,
|
|
650
|
+
};
|
|
651
|
+
})
|
|
652
|
+
.sort((a, b) => {
|
|
653
|
+
if (a.hasSeedMatch && !b.hasSeedMatch)
|
|
654
|
+
return -1;
|
|
655
|
+
if (!a.hasSeedMatch && b.hasSeedMatch)
|
|
656
|
+
return 1;
|
|
657
|
+
return b.entityCount - a.entityCount;
|
|
658
|
+
});
|
|
659
|
+
const result = {
|
|
660
|
+
keyword,
|
|
661
|
+
project: loaded.projectName,
|
|
662
|
+
seedMatches: seedNodes.size,
|
|
663
|
+
totalConnected: visited.size,
|
|
664
|
+
depth: maxDepth,
|
|
665
|
+
files: filesArray.filter((f) => f.filePath !== "external").slice(0, 30),
|
|
666
|
+
externalDeps: filesArray.find((f) => f.filePath === "external")?.entities.map((e) => e.name) || [],
|
|
667
|
+
relationships: traceLinks.slice(0, 50).map((l) => ({
|
|
668
|
+
from: nodeMap.get(l.source)?.label || l.source,
|
|
669
|
+
to: nodeMap.get(l.target)?.label || l.target,
|
|
670
|
+
type: l.type,
|
|
671
|
+
})),
|
|
672
|
+
readingOrder: filesArray
|
|
673
|
+
.filter((f) => f.hasSeedMatch && f.filePath !== "external")
|
|
674
|
+
.map((f) => f.filePath),
|
|
675
|
+
message: `Found ${seedNodes.size} direct matches and ${visited.size - seedNodes.size} connected entities for '${keyword}'. Start reading from the files in 'readingOrder'.`,
|
|
676
|
+
};
|
|
677
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
678
|
+
});
|
|
679
|
+
// Tool 9: Generate Feature Flow Diagram
|
|
680
|
+
server.tool("generate_feature_flow_diagram", "Generate a Mermaid diagram showing the EXECUTION FLOW of a feature. Unlike generate_system_flow (which shows module imports), this traces the actual call chain: entry point → controller → service → model → database. Given a keyword, it finds all related functions and classes, then builds a flowchart or sequence diagram showing how they call each other at runtime. This is the best tool for understanding HOW a feature works step-by-step.", {
|
|
681
|
+
project: z.string().optional().describe("Project name or path"),
|
|
682
|
+
keyword: z.string().describe("Feature keyword to trace (e.g. 'login', 'payment', 'upload', 'auth')"),
|
|
683
|
+
diagramType: z.enum(["flowchart", "sequence"]).optional().describe("Type of Mermaid diagram: 'flowchart' (default) shows call graph, 'sequence' shows step-by-step execution order"),
|
|
684
|
+
depth: z.number().optional().describe("How many call hops to follow (default: 3)"),
|
|
685
|
+
maxNodes: z.number().optional().describe("Maximum nodes in diagram (default: 40)"),
|
|
686
|
+
}, async ({ project, keyword, diagramType, depth, maxNodes }) => {
|
|
687
|
+
const auth = await checkAuth();
|
|
688
|
+
await logActivity(auth, "generate_feature_flow_diagram", { project, keyword, diagramType, depth, maxNodes });
|
|
689
|
+
const loaded = await loadAnalysisAsync(project);
|
|
690
|
+
if (!loaded) {
|
|
691
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
692
|
+
}
|
|
693
|
+
const q = keyword.toLowerCase();
|
|
694
|
+
const maxDepth = depth || 3;
|
|
695
|
+
const maxN = maxNodes || 40;
|
|
696
|
+
const dType = diagramType || "flowchart";
|
|
697
|
+
const nodes = loaded.analysis.graph.nodes;
|
|
698
|
+
const links = loaded.analysis.graph.links;
|
|
699
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
700
|
+
const nodeNameMap = new Map(nodes.map((n) => [n.id, n.label]));
|
|
701
|
+
const seedNodes = new Set();
|
|
702
|
+
for (const node of nodes) {
|
|
703
|
+
if (node.id.startsWith('external:'))
|
|
704
|
+
continue;
|
|
705
|
+
if (node.filePath && (node.filePath.includes('/venv/') ||
|
|
706
|
+
node.filePath.includes('/.venv/') ||
|
|
707
|
+
node.filePath.includes('/node_modules/') ||
|
|
708
|
+
node.filePath.includes('/vendor/') ||
|
|
709
|
+
node.filePath.includes('/site-packages/')))
|
|
710
|
+
continue;
|
|
711
|
+
if (node.label.toLowerCase().includes(q) ||
|
|
712
|
+
(node.filePath && node.filePath.toLowerCase().includes(q)) ||
|
|
713
|
+
node.id.toLowerCase().includes(q)) {
|
|
714
|
+
seedNodes.add(node.id);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (seedNodes.size === 0) {
|
|
718
|
+
return {
|
|
719
|
+
content: [{
|
|
720
|
+
type: "text",
|
|
721
|
+
text: JSON.stringify({
|
|
722
|
+
keyword,
|
|
723
|
+
matchCount: 0,
|
|
724
|
+
message: `No entities found matching '${keyword}'. Try a broader keyword.`,
|
|
725
|
+
suggestions: nodes
|
|
726
|
+
.filter((n) => n.type === "function" || n.type === "class")
|
|
727
|
+
.map((n) => n.label)
|
|
728
|
+
.filter((l, i, arr) => arr.indexOf(l) === i)
|
|
729
|
+
.slice(0, 15),
|
|
730
|
+
}, null, 2),
|
|
731
|
+
}],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const visited = new Set(seedNodes);
|
|
735
|
+
let frontier = new Set(seedNodes);
|
|
736
|
+
const callAndContainsLinks = links.filter((l) => l.type === "call" || l.type === "contains");
|
|
737
|
+
for (let d = 0; d < maxDepth; d++) {
|
|
738
|
+
const nextFrontier = new Set();
|
|
739
|
+
for (const link of callAndContainsLinks) {
|
|
740
|
+
if (frontier.has(link.source) && !visited.has(link.target)) {
|
|
741
|
+
nextFrontier.add(link.target);
|
|
742
|
+
visited.add(link.target);
|
|
743
|
+
}
|
|
744
|
+
if (frontier.has(link.target) && !visited.has(link.source)) {
|
|
745
|
+
nextFrontier.add(link.source);
|
|
746
|
+
visited.add(link.source);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
frontier = nextFrontier;
|
|
750
|
+
if (nextFrontier.size === 0)
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
let traceNodes = nodes.filter((n) => visited.has(n.id) && (n.type === "function" || n.type === "class"));
|
|
754
|
+
if (traceNodes.length > maxN) {
|
|
755
|
+
const callConnections = new Map();
|
|
756
|
+
for (const link of links) {
|
|
757
|
+
if (link.type === "call") {
|
|
758
|
+
callConnections.set(link.source, (callConnections.get(link.source) || 0) + 1);
|
|
759
|
+
callConnections.set(link.target, (callConnections.get(link.target) || 0) + 1);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
traceNodes.sort((a, b) => {
|
|
763
|
+
if (seedNodes.has(a.id) && !seedNodes.has(b.id))
|
|
764
|
+
return -1;
|
|
765
|
+
if (!seedNodes.has(a.id) && seedNodes.has(b.id))
|
|
766
|
+
return 1;
|
|
767
|
+
return (callConnections.get(b.id) || 0) - (callConnections.get(a.id) || 0);
|
|
768
|
+
});
|
|
769
|
+
traceNodes = traceNodes.slice(0, maxN);
|
|
770
|
+
}
|
|
771
|
+
const traceNodeIds = new Set(traceNodes.map((n) => n.id));
|
|
772
|
+
const traceLinks = links.filter((l) => traceNodeIds.has(l.source) && traceNodeIds.has(l.target) && l.type === "call");
|
|
773
|
+
const linkSet = new Set();
|
|
774
|
+
const dedupLinks = traceLinks.filter((l) => {
|
|
775
|
+
const key = `${l.source}|${l.target}`;
|
|
776
|
+
if (linkSet.has(key))
|
|
777
|
+
return false;
|
|
778
|
+
linkSet.add(key);
|
|
779
|
+
return true;
|
|
780
|
+
});
|
|
781
|
+
const hasIncoming = new Set();
|
|
782
|
+
for (const link of dedupLinks) {
|
|
783
|
+
hasIncoming.add(link.target);
|
|
784
|
+
}
|
|
785
|
+
const entryPoints = traceNodes.filter((n) => !hasIncoming.has(n.id) || seedNodes.has(n.id));
|
|
786
|
+
let mermaid = "";
|
|
787
|
+
const sanitizeLabel = (s) => s.replace(/"/g, "'").replace(/[<>]/g, "");
|
|
788
|
+
if (dType === "sequence") {
|
|
789
|
+
const seqLines = ["sequenceDiagram"];
|
|
790
|
+
const participantMap = new Map();
|
|
791
|
+
let pCounter = 0;
|
|
792
|
+
for (const node of traceNodes) {
|
|
793
|
+
const pid = `P${pCounter++}`;
|
|
794
|
+
participantMap.set(node.id, pid);
|
|
795
|
+
const icon = node.type === "class" ? "🏗️" : "⚡";
|
|
796
|
+
const fileSuffix = node.filePath ? ` (${path.basename(node.filePath)})` : "";
|
|
797
|
+
seqLines.push(` participant ${pid} as ${icon} ${sanitizeLabel(node.label)}${fileSuffix}`);
|
|
798
|
+
}
|
|
799
|
+
for (const link of dedupLinks) {
|
|
800
|
+
const src = participantMap.get(link.source);
|
|
801
|
+
const tgt = participantMap.get(link.target);
|
|
802
|
+
if (src && tgt && src !== tgt) {
|
|
803
|
+
seqLines.push(` ${src}->>+${tgt}: calls`);
|
|
804
|
+
seqLines.push(` ${tgt}-->>-${src}: returns`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
mermaid = seqLines.join("\n");
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
const flowLines = ["graph TD"];
|
|
811
|
+
flowLines.push(" classDef entry fill:#4CAF50,stroke:#388E3C,color:#fff,stroke-width:2px");
|
|
812
|
+
flowLines.push(" classDef seed fill:#2196F3,stroke:#1565C0,color:#fff,stroke-width:2px");
|
|
813
|
+
flowLines.push(" classDef cls fill:#FF9800,stroke:#E65100,color:#fff");
|
|
814
|
+
flowLines.push(" classDef func fill:#607D8B,stroke:#37474F,color:#fff");
|
|
815
|
+
const mermaidIdMap = new Map();
|
|
816
|
+
let nCounter = 0;
|
|
817
|
+
for (const node of traceNodes) {
|
|
818
|
+
const mid = `f${nCounter++}`;
|
|
819
|
+
mermaidIdMap.set(node.id, mid);
|
|
820
|
+
const label = sanitizeLabel(node.label);
|
|
821
|
+
const fileSuffix = node.filePath ? `<br/>${path.basename(node.filePath)}` : "";
|
|
822
|
+
if (node.type === "class") {
|
|
823
|
+
flowLines.push(` ${mid}[["🏗️ ${label}${fileSuffix}"]]`);
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
flowLines.push(` ${mid}("⚡ ${label}${fileSuffix}")`);
|
|
827
|
+
}
|
|
828
|
+
if (entryPoints.includes(node) && !hasIncoming.has(node.id)) {
|
|
829
|
+
flowLines.push(` class ${mid} entry`);
|
|
830
|
+
}
|
|
831
|
+
else if (seedNodes.has(node.id)) {
|
|
832
|
+
flowLines.push(` class ${mid} seed`);
|
|
833
|
+
}
|
|
834
|
+
else if (node.type === "class") {
|
|
835
|
+
flowLines.push(` class ${mid} cls`);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
flowLines.push(` class ${mid} func`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
for (const link of dedupLinks) {
|
|
842
|
+
const src = mermaidIdMap.get(link.source);
|
|
843
|
+
const tgt = mermaidIdMap.get(link.target);
|
|
844
|
+
if (src && tgt) {
|
|
845
|
+
flowLines.push(` ${src} -->|calls| ${tgt}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
flowLines.push("");
|
|
849
|
+
flowLines.push(` subgraph Legend`);
|
|
850
|
+
flowLines.push(` L1("🟢 Entry Point"):::entry`);
|
|
851
|
+
flowLines.push(` L2("🔵 Keyword Match"):::seed`);
|
|
852
|
+
flowLines.push(` L3("🟠 Class"):::cls`);
|
|
853
|
+
flowLines.push(` L4("⬜ Function"):::func`);
|
|
854
|
+
flowLines.push(` end`);
|
|
855
|
+
mermaid = flowLines.join("\n");
|
|
856
|
+
}
|
|
857
|
+
const executionOrder = [];
|
|
858
|
+
const inDegree = new Map();
|
|
859
|
+
for (const node of traceNodes) {
|
|
860
|
+
inDegree.set(node.id, 0);
|
|
861
|
+
}
|
|
862
|
+
for (const link of dedupLinks) {
|
|
863
|
+
inDegree.set(link.target, (inDegree.get(link.target) || 0) + 1);
|
|
864
|
+
}
|
|
865
|
+
const queue = [];
|
|
866
|
+
for (const [id, deg] of inDegree) {
|
|
867
|
+
if (deg === 0)
|
|
868
|
+
queue.push(id);
|
|
869
|
+
}
|
|
870
|
+
let step = 1;
|
|
871
|
+
const ordered = new Set();
|
|
872
|
+
while (queue.length > 0 && step <= maxN) {
|
|
873
|
+
const current = queue.shift();
|
|
874
|
+
if (ordered.has(current))
|
|
875
|
+
continue;
|
|
876
|
+
ordered.add(current);
|
|
877
|
+
const node = nodeMap.get(current);
|
|
878
|
+
if (node) {
|
|
879
|
+
const callsTo = dedupLinks
|
|
880
|
+
.filter((l) => l.source === current)
|
|
881
|
+
.map((l) => nodeNameMap.get(l.target) || l.target);
|
|
882
|
+
const calledBy = dedupLinks
|
|
883
|
+
.filter((l) => l.target === current)
|
|
884
|
+
.map((l) => nodeNameMap.get(l.source) || l.source);
|
|
885
|
+
executionOrder.push({
|
|
886
|
+
step: step++,
|
|
887
|
+
name: node.label,
|
|
888
|
+
type: node.type,
|
|
889
|
+
file: node.filePath ? (path.isAbsolute(node.filePath) ? path.relative(loaded.projectDir, node.filePath) : node.filePath) : null,
|
|
890
|
+
line: node.line || null,
|
|
891
|
+
callsTo,
|
|
892
|
+
calledBy,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
for (const link of dedupLinks) {
|
|
896
|
+
if (link.source === current) {
|
|
897
|
+
const newDeg = (inDegree.get(link.target) || 1) - 1;
|
|
898
|
+
inDegree.set(link.target, newDeg);
|
|
899
|
+
if (newDeg <= 0 && !ordered.has(link.target)) {
|
|
900
|
+
queue.push(link.target);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
for (const node of traceNodes) {
|
|
906
|
+
if (!ordered.has(node.id)) {
|
|
907
|
+
const callsTo = dedupLinks
|
|
908
|
+
.filter((l) => l.source === node.id)
|
|
909
|
+
.map((l) => nodeNameMap.get(l.target) || l.target);
|
|
910
|
+
const calledBy = dedupLinks
|
|
911
|
+
.filter((l) => l.target === node.id)
|
|
912
|
+
.map((l) => nodeNameMap.get(l.source) || l.source);
|
|
913
|
+
executionOrder.push({
|
|
914
|
+
step: step++,
|
|
915
|
+
name: node.label,
|
|
916
|
+
type: node.type,
|
|
917
|
+
file: node.filePath ? (path.isAbsolute(node.filePath) ? path.relative(loaded.projectDir, node.filePath) : node.filePath) : null,
|
|
918
|
+
line: node.line || null,
|
|
919
|
+
callsTo,
|
|
920
|
+
calledBy,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const result = {
|
|
925
|
+
keyword,
|
|
926
|
+
project: loaded.projectName,
|
|
927
|
+
diagramType: dType,
|
|
928
|
+
seedMatches: seedNodes.size,
|
|
929
|
+
nodesInDiagram: traceNodes.length,
|
|
930
|
+
callRelationships: dedupLinks.length,
|
|
931
|
+
entryPoints: entryPoints.map((n) => ({
|
|
932
|
+
name: n.label,
|
|
933
|
+
type: n.type,
|
|
934
|
+
file: n.filePath ? (path.isAbsolute(n.filePath) ? path.relative(loaded.projectDir, n.filePath) : n.filePath) : null,
|
|
935
|
+
})),
|
|
936
|
+
mermaidDiagram: mermaid,
|
|
937
|
+
executionOrder,
|
|
938
|
+
readingOrder: executionOrder
|
|
939
|
+
.filter((e) => e.file)
|
|
940
|
+
.map((e) => e.file)
|
|
941
|
+
.filter((f, i, arr) => arr.indexOf(f) === i),
|
|
942
|
+
message: `Generated ${dType} diagram for '${keyword}': ${traceNodes.length} nodes, ${dedupLinks.length} call relationships. Entry points: ${entryPoints.map((n) => n.label).join(", ")}`,
|
|
943
|
+
};
|
|
944
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
945
|
+
});
|
|
946
|
+
// Tool 11: Detect Architectural Smells
|
|
947
|
+
server.tool("detect_architectural_smells", "Knowledge Graph Reasoning: Use Oracle 26ai Graph features to automatically detect architectural weaknesses, circular dependencies, God objects, and dead code.", {
|
|
948
|
+
project: z.string().optional().describe("Project name or path"),
|
|
949
|
+
}, async ({ project }) => {
|
|
950
|
+
const auth = await checkAuth();
|
|
951
|
+
await logActivity(auth, "detect_architectural_smells", { project });
|
|
952
|
+
const loaded = await loadAnalysisAsync(project);
|
|
953
|
+
if (!loaded) {
|
|
954
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const nodes = loaded.analysis.graph?.nodes || [];
|
|
958
|
+
const links = loaded.analysis.graph?.links || [];
|
|
959
|
+
// Find circular dependencies locally (from analyzer insights or simple check)
|
|
960
|
+
const circularDependencies = loaded.analysis.insights?.circularDependencies || [];
|
|
961
|
+
// God objects: classes with more than 15 outgoing/incoming connections
|
|
962
|
+
const nodeConnections = new Map();
|
|
963
|
+
links.forEach((l) => {
|
|
964
|
+
nodeConnections.set(l.source, (nodeConnections.get(l.source) || 0) + 1);
|
|
965
|
+
nodeConnections.set(l.target, (nodeConnections.get(l.target) || 0) + 1);
|
|
966
|
+
});
|
|
967
|
+
const godObjects = nodes
|
|
968
|
+
.filter((n) => n.type === 'class' && (nodeConnections.get(n.id) || 0) > 15)
|
|
969
|
+
.map((n) => ({ name: n.label, filePath: n.filePath, connections: nodeConnections.get(n.id) }));
|
|
970
|
+
// Dead code: non-external entities with 0 incoming connections
|
|
971
|
+
const incomingCount = new Map();
|
|
972
|
+
links.forEach((l) => {
|
|
973
|
+
incomingCount.set(l.target, (incomingCount.get(l.target) || 0) + 1);
|
|
974
|
+
});
|
|
975
|
+
const deadCode = nodes
|
|
976
|
+
.filter((n) => n.type === 'function' && !n.id.startsWith('external:') && (incomingCount.get(n.id) || 0) === 0 && !n.label.includes('main') && !n.label.includes('index'))
|
|
977
|
+
.slice(0, 10)
|
|
978
|
+
.map((n) => ({ name: n.label, filePath: n.filePath, line: n.line }));
|
|
979
|
+
const result = {
|
|
980
|
+
project: loaded.projectName,
|
|
981
|
+
timestamp: new Date().toISOString(),
|
|
982
|
+
findings: {
|
|
983
|
+
circularDependencies: {
|
|
984
|
+
count: circularDependencies.length,
|
|
985
|
+
details: circularDependencies,
|
|
986
|
+
impact: "High - Causes tight coupling and build issues."
|
|
987
|
+
},
|
|
988
|
+
godObjects: {
|
|
989
|
+
count: godObjects.length,
|
|
990
|
+
details: godObjects,
|
|
991
|
+
impact: "Medium - Violates Single Responsibility Principle, hard to maintain."
|
|
992
|
+
},
|
|
993
|
+
deadCode: {
|
|
994
|
+
count: deadCode.length,
|
|
995
|
+
details: deadCode,
|
|
996
|
+
impact: "Low - Increases codebase size and cognitive load."
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
recommendation: "Review high-impact findings (Circular Dependencies) first. Refactor God Objects into smaller services."
|
|
1000
|
+
};
|
|
1001
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
return { content: [{ type: "text", text: `Local Static Smells reasoning failed: ${(err instanceof Error ? err.message : String(err))}` }] };
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
// Tool 12: Scan Enterprise Vulnerabilities
|
|
1008
|
+
server.tool("scan_enterprise_vulnerabilities", "Enterprise Scanner: Automatically scan all analyzed projects for bugs, security vulnerabilities (hardcoded secrets, unsafe functions), and architectural problems. Features Admin Insights and Security Scoring.", {
|
|
1009
|
+
maxProjects: z.number().optional().describe("Maximum number of projects to scan (default: all)"),
|
|
1010
|
+
}, async ({ maxProjects }) => {
|
|
1011
|
+
const auth = await checkAuth();
|
|
1012
|
+
await logActivity(auth, "scan_enterprise_vulnerabilities", { maxProjects });
|
|
1013
|
+
const projects = await discoverProjectsAsync(auth.uid);
|
|
1014
|
+
if (projects.length === 0) {
|
|
1015
|
+
return { content: [{ type: "text", text: "No analyzed projects found. Run 'analyze' tool first." }] };
|
|
1016
|
+
}
|
|
1017
|
+
const isEnterprise = auth.tier === 'enterprise';
|
|
1018
|
+
const scanResults = [];
|
|
1019
|
+
const limit = maxProjects || (isEnterprise ? projects.length : 3);
|
|
1020
|
+
const projectsToScan = projects.slice(0, limit);
|
|
1021
|
+
for (const p of projectsToScan) {
|
|
1022
|
+
try {
|
|
1023
|
+
const loaded = await loadAnalysisAsync(p.name);
|
|
1024
|
+
if (!loaded)
|
|
1025
|
+
continue;
|
|
1026
|
+
const vulnerabilities = SecurityScanner.scan(loaded.analysis);
|
|
1027
|
+
const stats = getStats(loaded.analysis);
|
|
1028
|
+
const circularDeps = stats.circularDeps || 0;
|
|
1029
|
+
const deadCode = stats.deadCode || 0;
|
|
1030
|
+
const riskLevel = vulnerabilities.length > 10 ? "CRITICAL" : (vulnerabilities.length > 0 ? "HIGH" : "LOW");
|
|
1031
|
+
const securityScore = Math.max(0, 100 - (vulnerabilities.length * 5) - (circularDeps * 2));
|
|
1032
|
+
scanResults.push({
|
|
1033
|
+
project: p.name,
|
|
1034
|
+
riskLevel,
|
|
1035
|
+
securityScore: isEnterprise ? securityScore : "Upgrade to view",
|
|
1036
|
+
vulnerabilities: vulnerabilities.length,
|
|
1037
|
+
circularDependencies: circularDeps,
|
|
1038
|
+
deadCode: deadCode,
|
|
1039
|
+
adminInsights: isEnterprise ? `Project health is ${securityScore > 80 ? 'EXCELLENT' : 'NEEDS ATTENTION'}. Priority: ${riskLevel}.` : null,
|
|
1040
|
+
details: { vulnerabilities }
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
catch (err) {
|
|
1044
|
+
scanResults.push({
|
|
1045
|
+
project: p.name,
|
|
1046
|
+
error: `Scan failed: ${(err instanceof Error ? err.message : String(err))}`
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const finalReport = {
|
|
1051
|
+
timestamp: new Date().toISOString(),
|
|
1052
|
+
tier: auth.tier,
|
|
1053
|
+
projectsScanned: projectsToScan.length,
|
|
1054
|
+
totalProjectsDiscovered: projects.length,
|
|
1055
|
+
results: scanResults,
|
|
1056
|
+
enterpriseStatus: isEnterprise ? "ACTIVE (Admin Enabled)" : "INACTIVE"
|
|
1057
|
+
};
|
|
1058
|
+
return {
|
|
1059
|
+
content: [{
|
|
1060
|
+
type: "text",
|
|
1061
|
+
text: JSON.stringify(finalReport, null, 2)
|
|
1062
|
+
}]
|
|
1063
|
+
};
|
|
1064
|
+
});
|
|
1065
|
+
// Tool 13: code_search — Full-text search across source files
|
|
1066
|
+
server.tool("code_search", "Search source FILE CONTENTS across the entire project for any text string. Unlike 'search_entities' (which only searches entity names), this searches the actual code — comments, strings, variable names, function bodies, etc.", {
|
|
1067
|
+
project: z.string().optional().describe("Project name or path (auto-detects if omitted)"),
|
|
1068
|
+
query: z.string().describe("Text to search for in source file contents (case-insensitive)"),
|
|
1069
|
+
filePattern: z.string().optional().describe("Optional file glob pattern to narrow search (e.g. '*.ts', '*.py'). Default: all supported files"),
|
|
1070
|
+
maxResults: z.number().optional().describe("Maximum results to return (default: 30, max: 100)"),
|
|
1071
|
+
contextLines: z.number().optional().describe("Number of context lines around each match (default: 2)"),
|
|
1072
|
+
}, async ({ project, query, filePattern, maxResults, contextLines }) => {
|
|
1073
|
+
const auth = await checkAuth();
|
|
1074
|
+
await logActivity(auth, "code_search", { project, query: query.substring(0, 100), filePattern, maxResults, contextLines });
|
|
1075
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1076
|
+
if (!loaded) {
|
|
1077
|
+
return { content: [{ type: "text", text: "No analysis data found. Run 'analyze' tool first." }] };
|
|
1078
|
+
}
|
|
1079
|
+
const maxRes = Math.min(maxResults || 30, 100);
|
|
1080
|
+
const ctx = contextLines || 2;
|
|
1081
|
+
const q = query.toLowerCase();
|
|
1082
|
+
const allFiles = [];
|
|
1083
|
+
const extSet = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".php", ".json", ".yaml", ".yml", ".md", ".css", ".scss", ".html"]);
|
|
1084
|
+
try {
|
|
1085
|
+
const walkDir = (dir, depth) => {
|
|
1086
|
+
if (depth > 8)
|
|
1087
|
+
return;
|
|
1088
|
+
try {
|
|
1089
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1090
|
+
for (const entry of entries) {
|
|
1091
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "venv" || entry.name === ".venv")
|
|
1092
|
+
continue;
|
|
1093
|
+
const fullPath = path.join(dir, entry.name);
|
|
1094
|
+
if (entry.isDirectory())
|
|
1095
|
+
walkDir(fullPath, depth + 1);
|
|
1096
|
+
else if (entry.isFile()) {
|
|
1097
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1098
|
+
if (extSet.has(ext))
|
|
1099
|
+
allFiles.push(fullPath);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch { /* skip */ }
|
|
1104
|
+
};
|
|
1105
|
+
walkDir(loaded.projectDir, 0);
|
|
1106
|
+
}
|
|
1107
|
+
catch { /* fallback */ }
|
|
1108
|
+
const results = [];
|
|
1109
|
+
for (const filePath of allFiles) {
|
|
1110
|
+
if (results.length >= maxRes)
|
|
1111
|
+
break;
|
|
1112
|
+
try {
|
|
1113
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1114
|
+
const lines = content.split("\n");
|
|
1115
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1116
|
+
if (results.length >= maxRes)
|
|
1117
|
+
break;
|
|
1118
|
+
if (lines[i].toLowerCase().includes(q)) {
|
|
1119
|
+
results.push({
|
|
1120
|
+
file: path.relative(loaded.projectDir, filePath),
|
|
1121
|
+
line: i + 1,
|
|
1122
|
+
content: lines[i].trim(),
|
|
1123
|
+
contextBefore: lines.slice(Math.max(0, i - ctx), i).map(l => l.trim()).filter(Boolean),
|
|
1124
|
+
contextAfter: lines.slice(i + 1, i + 1 + ctx).map(l => l.trim()).filter(Boolean),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
catch { /* skip */ }
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{ type: "text", text: JSON.stringify({ query, project: loaded.projectName, matchCount: results.length, truncated: results.length >= maxRes, files: [...new Set(results.map(r => r.file))], results: results.slice(0, maxRes) }, null, 2) }],
|
|
1133
|
+
};
|
|
1134
|
+
});
|
|
1135
|
+
// Tool 14: get_callers — Find all callers of a function/class
|
|
1136
|
+
server.tool("get_callers", "Find ALL functions, methods, or classes that call or reference a specific symbol. The 'reverse dependency' view — given a function/class name, trace everything that depends on it. Use before refactoring or deleting code.", {
|
|
1137
|
+
project: z.string().optional().describe("Project name or path"),
|
|
1138
|
+
symbol: z.string().describe("Function or class name to find callers (case-insensitive, partial match)"),
|
|
1139
|
+
maxResults: z.number().optional().describe("Maximum callers to return (default: 30)"),
|
|
1140
|
+
depth: z.number().optional().describe("How many levels deep (default: 1, max: 5)"),
|
|
1141
|
+
}, async ({ project, symbol, maxResults, depth }) => {
|
|
1142
|
+
const auth = await checkAuth();
|
|
1143
|
+
await logActivity(auth, "get_callers", { project, symbol, maxResults, depth });
|
|
1144
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1145
|
+
if (!loaded)
|
|
1146
|
+
return { content: [{ type: "text", text: "No analysis found. Run 'analyze' first." }] };
|
|
1147
|
+
const q = symbol.toLowerCase();
|
|
1148
|
+
const maxD = Math.min(depth || 1, 5);
|
|
1149
|
+
const nodes = loaded.analysis.graph.nodes;
|
|
1150
|
+
const links = loaded.analysis.graph.links;
|
|
1151
|
+
const targetIds = new Set();
|
|
1152
|
+
const targetNames = new Map();
|
|
1153
|
+
for (const node of nodes) {
|
|
1154
|
+
if (node.label.toLowerCase().includes(q) && !node.id.startsWith("external:")) {
|
|
1155
|
+
targetIds.add(node.id);
|
|
1156
|
+
targetNames.set(node.id, node.label);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (targetIds.size === 0)
|
|
1160
|
+
return { content: [{ type: "text", text: JSON.stringify({ symbol, matchCount: 0, message: `No symbol '${symbol}' found.` }) }] };
|
|
1161
|
+
const callers = new Map();
|
|
1162
|
+
let frontier = new Set(targetIds);
|
|
1163
|
+
const visited = new Set(targetIds);
|
|
1164
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
1165
|
+
for (let d = 1; d <= maxD; d++) {
|
|
1166
|
+
const next = new Set();
|
|
1167
|
+
for (const link of links) {
|
|
1168
|
+
if ((link.type === "call" || link.type === "import") && frontier.has(link.target) && !visited.has(link.source)) {
|
|
1169
|
+
visited.add(link.source);
|
|
1170
|
+
next.add(link.source);
|
|
1171
|
+
const srcNode = nodeMap.get(link.source);
|
|
1172
|
+
const tgtName = targetNames.get(link.target) || nodeMap.get(link.target)?.label || link.target;
|
|
1173
|
+
if (srcNode) {
|
|
1174
|
+
if (!callers.has(link.source)) {
|
|
1175
|
+
callers.set(link.source, { name: srcNode.label, type: srcNode.type, filePath: srcNode.filePath || null, line: srcNode.line || null, depth: d, via: [tgtName] });
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
callers.get(link.source).via.push(tgtName);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
frontier = next;
|
|
1184
|
+
}
|
|
1185
|
+
const maxRes = maxResults || 30;
|
|
1186
|
+
const targetDetails = Array.from(targetIds).map(id => { const n = nodeMap.get(id); return n ? { name: n.label, type: n.type, filePath: n.filePath || null, line: n.line || null } : { name: id, type: "unknown", filePath: null, line: null }; });
|
|
1187
|
+
return { content: [{ type: "text", text: JSON.stringify({ symbol, project: loaded.projectName, targets: targetDetails, totalCallers: callers.size, maxDepth: maxD, callers: Array.from(callers.values()).slice(0, maxRes) }, null, 2) }] };
|
|
1188
|
+
});
|
|
1189
|
+
// Tool 15: get_callees — Find all functions called by a symbol
|
|
1190
|
+
server.tool("get_callees", "Find everything a function, method, or class calls or depends on. The 'forward dependency' view — given a function name, trace what it imports and calls. Use to understand function dependencies before modifying.", {
|
|
1191
|
+
project: z.string().optional().describe("Project name or path"),
|
|
1192
|
+
symbol: z.string().describe("Function or class name to find callees (case-insensitive, partial match)"),
|
|
1193
|
+
maxResults: z.number().optional().describe("Maximum callees (default: 30)"),
|
|
1194
|
+
depth: z.number().optional().describe("How many levels deep (default: 1, max: 5)"),
|
|
1195
|
+
}, async ({ project, symbol, maxResults, depth }) => {
|
|
1196
|
+
const auth = await checkAuth();
|
|
1197
|
+
await logActivity(auth, "get_callees", { project, symbol, maxResults, depth });
|
|
1198
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1199
|
+
if (!loaded)
|
|
1200
|
+
return { content: [{ type: "text", text: "No analysis found. Run 'analyze' first." }] };
|
|
1201
|
+
const q = symbol.toLowerCase();
|
|
1202
|
+
const maxD = Math.min(depth || 1, 5);
|
|
1203
|
+
const nodes = loaded.analysis.graph.nodes;
|
|
1204
|
+
const links = loaded.analysis.graph.links;
|
|
1205
|
+
const sourceIds = new Set();
|
|
1206
|
+
for (const node of nodes) {
|
|
1207
|
+
if (node.label.toLowerCase().includes(q) && !node.id.startsWith("external:"))
|
|
1208
|
+
sourceIds.add(node.id);
|
|
1209
|
+
}
|
|
1210
|
+
if (sourceIds.size === 0)
|
|
1211
|
+
return { content: [{ type: "text", text: JSON.stringify({ symbol, matchCount: 0, message: `No symbol '${symbol}' found.` }) }] };
|
|
1212
|
+
const callees = new Map();
|
|
1213
|
+
let frontier = new Set(sourceIds);
|
|
1214
|
+
const visited = new Set(sourceIds);
|
|
1215
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
1216
|
+
for (let d = 1; d <= maxD; d++) {
|
|
1217
|
+
const next = new Set();
|
|
1218
|
+
for (const link of links) {
|
|
1219
|
+
if ((link.type === "call" || link.type === "import") && frontier.has(link.source) && !visited.has(link.target)) {
|
|
1220
|
+
visited.add(link.target);
|
|
1221
|
+
next.add(link.target);
|
|
1222
|
+
const n = nodeMap.get(link.target);
|
|
1223
|
+
if (n && !n.id.startsWith("external:"))
|
|
1224
|
+
callees.set(link.target, { name: n.label, type: n.type, filePath: n.filePath || null, line: n.line || null, depth: d });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
frontier = next;
|
|
1228
|
+
}
|
|
1229
|
+
const maxRes = maxResults || 30;
|
|
1230
|
+
const nodeMap2 = new Map(nodes.map((n) => [n.id, n]));
|
|
1231
|
+
const sourceDetails = Array.from(sourceIds).map(id => { const n = nodeMap2.get(id); return n ? { name: n.label, type: n.type, filePath: n.filePath || null, line: n.line || null } : { name: id, type: "unknown", filePath: null, line: null }; });
|
|
1232
|
+
return { content: [{ type: "text", text: JSON.stringify({ symbol, project: loaded.projectName, sources: sourceDetails, totalCallees: callees.size, maxDepth: maxD, callees: Array.from(callees.values()).slice(0, maxRes) }, null, 2) }] };
|
|
1233
|
+
});
|
|
1234
|
+
// Tool 16: impact_analysis — Blast radius analysis
|
|
1235
|
+
server.tool("impact_analysis", "Full BLAST RADIUS analysis for changing a symbol. Traces BOTH callers (what depends on this) AND callees (what this depends on) in one view. Also finds related test files. Use BEFORE any significant code change.", {
|
|
1236
|
+
project: z.string().optional().describe("Project name or path"),
|
|
1237
|
+
symbol: z.string().describe("Function, class, or module name (case-insensitive, partial match)"),
|
|
1238
|
+
depth: z.number().optional().describe("How many levels deep (default: 2, max: 5)"),
|
|
1239
|
+
}, async ({ project, symbol, depth }) => {
|
|
1240
|
+
const auth = await checkAuth();
|
|
1241
|
+
await logActivity(auth, "impact_analysis", { project, symbol, depth });
|
|
1242
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1243
|
+
if (!loaded)
|
|
1244
|
+
return { content: [{ type: "text", text: "No analysis found. Run 'analyze' first." }] };
|
|
1245
|
+
const q = symbol.toLowerCase();
|
|
1246
|
+
const maxD = Math.min(depth || 2, 5);
|
|
1247
|
+
const nodes = loaded.analysis.graph.nodes;
|
|
1248
|
+
const links = loaded.analysis.graph.links;
|
|
1249
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
1250
|
+
const symbolIds = new Set();
|
|
1251
|
+
for (const node of nodes) {
|
|
1252
|
+
if (node.label.toLowerCase().includes(q) && !node.id.startsWith("external:"))
|
|
1253
|
+
symbolIds.add(node.id);
|
|
1254
|
+
}
|
|
1255
|
+
if (symbolIds.size === 0)
|
|
1256
|
+
return { content: [{ type: "text", text: JSON.stringify({ symbol, matchCount: 0, message: `No symbol '${symbol}' found.` }) }] };
|
|
1257
|
+
const callers = new Map();
|
|
1258
|
+
const callees = new Map();
|
|
1259
|
+
// Forward (callees)
|
|
1260
|
+
let fF = new Set(symbolIds);
|
|
1261
|
+
const fV = new Set(symbolIds);
|
|
1262
|
+
for (let d = 1; d <= maxD; d++) {
|
|
1263
|
+
const n = new Set();
|
|
1264
|
+
for (const l of links)
|
|
1265
|
+
if ((l.type === "call" || l.type === "import") && fF.has(l.source) && !fV.has(l.target)) {
|
|
1266
|
+
fV.add(l.target);
|
|
1267
|
+
n.add(l.target);
|
|
1268
|
+
const nd = nodeMap.get(l.target);
|
|
1269
|
+
if (nd && !nd.id.startsWith("external:"))
|
|
1270
|
+
callees.set(l.target, { name: nd.label, type: nd.type, filePath: nd.filePath || null, depth: d });
|
|
1271
|
+
}
|
|
1272
|
+
fF = n;
|
|
1273
|
+
}
|
|
1274
|
+
// Reverse (callers)
|
|
1275
|
+
let rF = new Set(symbolIds);
|
|
1276
|
+
const rV = new Set(symbolIds);
|
|
1277
|
+
for (let d = 1; d <= maxD; d++) {
|
|
1278
|
+
const n = new Set();
|
|
1279
|
+
for (const l of links)
|
|
1280
|
+
if ((l.type === "call" || l.type === "import") && rF.has(l.target) && !rV.has(l.source)) {
|
|
1281
|
+
rV.add(l.source);
|
|
1282
|
+
n.add(l.source);
|
|
1283
|
+
const nd = nodeMap.get(l.source);
|
|
1284
|
+
if (nd && !nd.id.startsWith("external:"))
|
|
1285
|
+
callers.set(l.source, { name: nd.label, type: nd.type, filePath: nd.filePath || null, depth: d });
|
|
1286
|
+
}
|
|
1287
|
+
rF = n;
|
|
1288
|
+
}
|
|
1289
|
+
// Find test files
|
|
1290
|
+
const testFiles = new Set();
|
|
1291
|
+
for (const id of [...symbolIds]) {
|
|
1292
|
+
const n = nodeMap.get(id);
|
|
1293
|
+
if (n?.filePath) {
|
|
1294
|
+
const absPath = path.isAbsolute(n.filePath) ? n.filePath : path.resolve(loaded.projectDir, n.filePath);
|
|
1295
|
+
try {
|
|
1296
|
+
const entries = fs.readdirSync(path.dirname(absPath));
|
|
1297
|
+
const base = path.basename(absPath).replace(path.extname(absPath), "");
|
|
1298
|
+
for (const e of entries)
|
|
1299
|
+
if ((e.includes(".test.") || e.includes(".spec.")) && e.toLowerCase().includes(base.toLowerCase()))
|
|
1300
|
+
testFiles.add(path.join(path.dirname(absPath), e));
|
|
1301
|
+
}
|
|
1302
|
+
catch { /* skip */ }
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const affectedFiles = new Set();
|
|
1306
|
+
for (const c of [...Array.from(callers.values()), ...Array.from(callees.values())])
|
|
1307
|
+
if (c.filePath)
|
|
1308
|
+
affectedFiles.add(c.filePath);
|
|
1309
|
+
return {
|
|
1310
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1311
|
+
symbol, project: loaded.projectName,
|
|
1312
|
+
impact: { incomingDependents: callers.size, outgoingDependencies: callees.size, totalAffectedFiles: affectedFiles.size, affectedFiles: Array.from(affectedFiles), testFiles: Array.from(testFiles) },
|
|
1313
|
+
callers: Array.from(callers.values()).slice(0, 20),
|
|
1314
|
+
callees: Array.from(callees.values()).slice(0, 20),
|
|
1315
|
+
recommendation: callers.size > 10 ? "HIGH IMPACT" : callers.size > 0 ? "MEDIUM IMPACT" : "LOW IMPACT",
|
|
1316
|
+
}, null, 2) }],
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
// Tool 17: project_context — One-shot comprehensive project overview
|
|
1320
|
+
server.tool("project_context", "Get a comprehensive overview of a project in ONE call: package.json (name, version, scripts, deps, devDeps), config files detected, README summary, test framework, git branch. Saves 5-10 individual read_file calls when starting work.", {
|
|
1321
|
+
project: z.string().optional().describe("Project name or path (auto-detects if omitted)"),
|
|
1322
|
+
}, async ({ project }) => {
|
|
1323
|
+
const auth = await checkAuth();
|
|
1324
|
+
await logActivity(auth, "project_context", { project });
|
|
1325
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1326
|
+
if (!loaded)
|
|
1327
|
+
return { content: [{ type: "text", text: "No analysis found. Run 'analyze' first." }] };
|
|
1328
|
+
const projectDir = loaded.projectDir;
|
|
1329
|
+
const ctx = { name: loaded.projectName, path: projectDir };
|
|
1330
|
+
// package.json
|
|
1331
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
1332
|
+
if (fs.existsSync(pkgPath)) {
|
|
1333
|
+
try {
|
|
1334
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1335
|
+
ctx.version = pkg.version;
|
|
1336
|
+
ctx.description = pkg.description;
|
|
1337
|
+
ctx.scripts = pkg.scripts || {};
|
|
1338
|
+
ctx.scriptCount = Object.keys(ctx.scripts).length;
|
|
1339
|
+
ctx.dependencies = pkg.dependencies ? Object.keys(pkg.dependencies) : [];
|
|
1340
|
+
ctx.devDependencies = pkg.devDependencies ? Object.keys(pkg.devDependencies) : [];
|
|
1341
|
+
ctx.main = pkg.main;
|
|
1342
|
+
ctx.bin = pkg.bin;
|
|
1343
|
+
}
|
|
1344
|
+
catch { /* skip */ }
|
|
1345
|
+
}
|
|
1346
|
+
// Config files
|
|
1347
|
+
ctx.configFiles = {};
|
|
1348
|
+
for (const [key, f] of Object.entries({ tsconfig: "tsconfig.json", eslint: ".eslintrc.js", prettier: ".prettierrc", jest: "jest.config.js", vitest: "vitest.config.ts", playwright: "playwright.config.ts", docker: "Dockerfile" })) {
|
|
1349
|
+
ctx.configFiles[key] = fs.existsSync(path.join(projectDir, f));
|
|
1350
|
+
}
|
|
1351
|
+
// README
|
|
1352
|
+
for (const r of ["README.md", "README"]) {
|
|
1353
|
+
const rp = path.join(projectDir, r);
|
|
1354
|
+
if (fs.existsSync(rp)) {
|
|
1355
|
+
ctx.readme = { file: r, length: fs.statSync(rp).size };
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// Git branch
|
|
1360
|
+
const gh = path.join(projectDir, ".git", "HEAD");
|
|
1361
|
+
if (fs.existsSync(gh)) {
|
|
1362
|
+
try {
|
|
1363
|
+
const h = fs.readFileSync(gh, "utf-8").trim();
|
|
1364
|
+
const m = h.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
1365
|
+
ctx.gitBranch = m ? m[1] : "(detached)";
|
|
1366
|
+
}
|
|
1367
|
+
catch { /* skip */ }
|
|
1368
|
+
}
|
|
1369
|
+
// Stats
|
|
1370
|
+
const st = getStats(loaded.analysis);
|
|
1371
|
+
ctx.stats = { files: st.files, functions: st.functions, classes: st.classes, deps: st.dependencies, circularDeps: st.circularDeps, deadCode: st.deadCode };
|
|
1372
|
+
// Top files
|
|
1373
|
+
const fc = new Map();
|
|
1374
|
+
for (const n of loaded.analysis.graph.nodes)
|
|
1375
|
+
if (n.filePath && !n.id.startsWith("external:"))
|
|
1376
|
+
fc.set(n.filePath, (fc.get(n.filePath) || 0) + 1);
|
|
1377
|
+
ctx.topFiles = Array.from(fc.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([f, c]) => ({ file: f, entities: c }));
|
|
1378
|
+
ctx.testFramework = ctx.configFiles.vitest ? "vitest" : ctx.configFiles.jest ? "jest" : ctx.configFiles.playwright ? "playwright" : "unknown";
|
|
1379
|
+
return { content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }] };
|
|
1380
|
+
});
|
|
1381
|
+
// Tool 18: run_script — Run npm scripts
|
|
1382
|
+
server.tool("run_script", "Run an npm/pnpm/yarn script from package.json. Returns exit code, stdout/stderr, and duration. Handles cd to project dir automatically.", {
|
|
1383
|
+
project: z.string().optional().describe("Project name or path"),
|
|
1384
|
+
script: z.string().describe("Script name from package.json (e.g. 'build', 'test', 'lint')"),
|
|
1385
|
+
args: z.string().optional().describe("Optional args (e.g. '-- --watch')"),
|
|
1386
|
+
timeout: z.number().optional().describe("Timeout in seconds (default: 60, max: 300)"),
|
|
1387
|
+
}, async ({ project, script, args, timeout }) => {
|
|
1388
|
+
const auth = await checkAuth();
|
|
1389
|
+
await logActivity(auth, "run_script", { project, script, args, timeout });
|
|
1390
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1391
|
+
if (!loaded)
|
|
1392
|
+
return { content: [{ type: "text", text: "No analysis found. Run 'analyze' first." }] };
|
|
1393
|
+
const projectDir = loaded.projectDir;
|
|
1394
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
1395
|
+
if (fs.existsSync(pkgPath)) {
|
|
1396
|
+
try {
|
|
1397
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1398
|
+
if (!pkg.scripts?.[script])
|
|
1399
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Script '${script}' not found`, available: pkg.scripts ? Object.keys(pkg.scripts) : [] }) }] };
|
|
1400
|
+
}
|
|
1401
|
+
catch { /* skip */ }
|
|
1402
|
+
}
|
|
1403
|
+
const cmd = `cd ${JSON.stringify(projectDir)} && npm run ${script}${args ? " " + args : ""}`;
|
|
1404
|
+
const maxTime = Math.min(timeout || 60, 300);
|
|
1405
|
+
const startTime = Date.now();
|
|
1406
|
+
try {
|
|
1407
|
+
const cp = require("child_process");
|
|
1408
|
+
const result = cp.execSync(cmd, { timeout: maxTime * 1000, shell: "/bin/bash", maxBuffer: 1024 * 1024, cwd: projectDir });
|
|
1409
|
+
const dur = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1410
|
+
return { content: [{ type: "text", text: JSON.stringify({ script, project: loaded.projectName, exitCode: 0, duration: `${dur}s`, stdout: result.stdout.toString().substring(0, 10000), stderr: (result.stderr || "").toString().substring(0, 5000) }, null, 2) }] };
|
|
1411
|
+
}
|
|
1412
|
+
catch (err) {
|
|
1413
|
+
const dur = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1414
|
+
return { content: [{ type: "text", text: JSON.stringify({ script, project: loaded.projectName, exitCode: err.status || 1, duration: `${dur}s`, stdout: (err.stdout || "").toString().substring(0, 10000), stderr: (err.stderr || "").toString().substring(0, 5000), error: err.killed ? "TIMEOUT" : err.message?.substring(0, 300) }, null, 2) }] };
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
// Tool 19: git_changes — Recent git activity
|
|
1418
|
+
server.tool("git_changes", "Get recent git changes: last N commits (hash, author, date, message, files changed), uncommitted changes (modified/added/deleted), branch status (ahead/behind). Saves multiple git commands.", {
|
|
1419
|
+
project: z.string().optional().describe("Project name or path"),
|
|
1420
|
+
commits: z.number().optional().describe("Number of recent commits (default: 5, max: 20)"),
|
|
1421
|
+
}, async ({ project, commits }) => {
|
|
1422
|
+
const auth = await checkAuth();
|
|
1423
|
+
await logActivity(auth, "git_changes", { project, commits });
|
|
1424
|
+
const loaded = await loadAnalysisAsync(project);
|
|
1425
|
+
if (!loaded)
|
|
1426
|
+
return { content: [{ type: "text", text: "No analysis found. Run 'analyze' first." }] };
|
|
1427
|
+
const projectDir = loaded.projectDir;
|
|
1428
|
+
if (!fs.existsSync(path.join(projectDir, ".git")))
|
|
1429
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Not a git repository" }) }] };
|
|
1430
|
+
const maxC = Math.min(commits || 5, 20);
|
|
1431
|
+
const result = { project: loaded.projectName };
|
|
1432
|
+
const cp = require("child_process");
|
|
1433
|
+
try {
|
|
1434
|
+
result.branch = cp.execSync("git rev-parse --abbrev-ref HEAD", { cwd: projectDir, encoding: "utf-8" }).toString().trim();
|
|
1435
|
+
const st = cp.execSync("git status --porcelain", { cwd: projectDir, encoding: "utf-8" }).toString();
|
|
1436
|
+
const mod = [], add = [], del = [];
|
|
1437
|
+
for (const line of st.split("\n").map((x) => x.trim()).filter(Boolean)) {
|
|
1438
|
+
const s = line.substring(0, 2), f = line.substring(3);
|
|
1439
|
+
if (s.includes("M"))
|
|
1440
|
+
mod.push(f);
|
|
1441
|
+
if (s.includes("A"))
|
|
1442
|
+
add.push(f);
|
|
1443
|
+
if (s.includes("D"))
|
|
1444
|
+
del.push(f);
|
|
1445
|
+
}
|
|
1446
|
+
result.uncommitted = { modified: mod.slice(0, 20), added: add.slice(0, 10), deleted: del.slice(0, 10), hasChanges: st.trim().length > 0 };
|
|
1447
|
+
try {
|
|
1448
|
+
const [behind, ahead] = cp.execSync("git rev-list --left-right --count HEAD...@{upstream}", { cwd: projectDir, encoding: "utf-8" }).toString().trim().split("\t").map(Number);
|
|
1449
|
+
result.ahead = ahead || 0;
|
|
1450
|
+
result.behind = behind || 0;
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
result.ahead = null;
|
|
1454
|
+
result.behind = null;
|
|
1455
|
+
}
|
|
1456
|
+
const logRaw = cp.execSync(`git log -${maxC} --format="COMMIT%n%H%n%an%n%ai%n%s%nFILES:" --name-only`, { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024 }).toString();
|
|
1457
|
+
result.recentCommits = [];
|
|
1458
|
+
for (const block of logRaw.split("COMMIT\n").filter(Boolean)) {
|
|
1459
|
+
const ls = block.trim().split("\n");
|
|
1460
|
+
if (ls.length < 4)
|
|
1461
|
+
continue;
|
|
1462
|
+
const ci = { hash: ls[0]?.substring(0, 12), author: ls[1], date: ls[2], message: ls[3] };
|
|
1463
|
+
const fi = ls.findIndex((x) => x === "FILES:");
|
|
1464
|
+
if (fi !== -1)
|
|
1465
|
+
ci.files = ls.slice(fi + 1).filter((x) => x.trim()).slice(0, 15);
|
|
1466
|
+
result.recentCommits.push(ci);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
catch (err) {
|
|
1470
|
+
result.error = err.message?.substring(0, 300);
|
|
1471
|
+
}
|
|
1472
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
// Create the global MCP server instance
|
|
1476
|
+
export const server = new McpServer({
|
|
1477
|
+
name: "CodeAtlas",
|
|
1478
|
+
version: "2.2.3",
|
|
1479
|
+
}, {
|
|
1480
|
+
capabilities: {
|
|
1481
|
+
resources: {},
|
|
1482
|
+
tools: {},
|
|
1483
|
+
logging: {},
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
registerTools(server);
|
|
1487
|
+
//# sourceMappingURL=mcpServer.js.map
|