@rpcajr/smart-graph-indexer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.agents/rules/antigravity-rtk-rules.md +32 -0
  2. package/README.md +116 -0
  3. package/dist/graph.d.ts +32 -0
  4. package/dist/graph.js +86 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +227 -0
  7. package/dist/mcp_server.d.ts +48 -0
  8. package/dist/mcp_server.js +433 -0
  9. package/dist/parser.d.ts +42 -0
  10. package/dist/parser.js +529 -0
  11. package/dist/stats.d.ts +43 -0
  12. package/dist/stats.js +146 -0
  13. package/dist/ui.d.ts +2 -0
  14. package/dist/ui.js +1003 -0
  15. package/dist/vector.d.ts +66 -0
  16. package/dist/vector.js +144 -0
  17. package/dist/wasm/tree-sitter-c.wasm +0 -0
  18. package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
  19. package/dist/wasm/tree-sitter-cpp.wasm +0 -0
  20. package/dist/wasm/tree-sitter-go.wasm +0 -0
  21. package/dist/wasm/tree-sitter-java.wasm +0 -0
  22. package/dist/wasm/tree-sitter-python.wasm +0 -0
  23. package/dist/wasm/tree-sitter-ruby.wasm +0 -0
  24. package/dist/wasm/tree-sitter-rust.wasm +0 -0
  25. package/dist/wasm/tree-sitter-typescript.wasm +0 -0
  26. package/dist/wasm/tree-sitter.wasm +0 -0
  27. package/dist/watcher.d.ts +36 -0
  28. package/dist/watcher.js +166 -0
  29. package/mcp-config-example.json +12 -0
  30. package/mcp_smart_indexer_implementation_spec.md +366 -0
  31. package/package.json +35 -0
  32. package/src/graph.ts +93 -0
  33. package/src/index.ts +216 -0
  34. package/src/mcp_server.ts +454 -0
  35. package/src/parser.ts +484 -0
  36. package/src/stats.ts +156 -0
  37. package/src/ui.ts +956 -0
  38. package/src/vector.ts +166 -0
  39. package/src/watcher.ts +144 -0
  40. package/test_project/App.java +16 -0
  41. package/test_project/BillingService.cs +31 -0
  42. package/test_project/auth.ts +18 -0
  43. package/test_project/config.ts +11 -0
  44. package/test_project/database.ts +21 -0
  45. package/test_project/index.ts +13 -0
  46. package/test_project/main.go +11 -0
  47. package/test_project/main.py +21 -0
  48. package/test_project/processor.py +12 -0
  49. package/test_project/utils.py +13 -0
  50. package/tsconfig.json +16 -0
  51. package/wasm/tree-sitter-c.wasm +0 -0
  52. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  53. package/wasm/tree-sitter-cpp.wasm +0 -0
  54. package/wasm/tree-sitter-go.wasm +0 -0
  55. package/wasm/tree-sitter-java.wasm +0 -0
  56. package/wasm/tree-sitter-python.wasm +0 -0
  57. package/wasm/tree-sitter-ruby.wasm +0 -0
  58. package/wasm/tree-sitter-rust.wasm +0 -0
  59. package/wasm/tree-sitter-typescript.wasm +0 -0
  60. package/wasm/tree-sitter.wasm +0 -0
package/src/index.ts ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { IndexerEngine, createMCPServer } from './mcp_server.js';
4
+ import { generateGraphHtml } from './ui.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { ListRootsResultSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs';
9
+ import { exec } from 'child_process';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('graph-indexer')
15
+ .description('Smart Graph Code Indexer MCP CLI')
16
+ .version('1.0.0');
17
+
18
+ program
19
+ .command('start')
20
+ .description('Starts the MCP server via stdio')
21
+ .option('-p, --path <directory>', 'Target project directory to index', '.')
22
+ .action(async (options) => {
23
+ try {
24
+ const projectPath = path.resolve(options.path);
25
+
26
+
27
+ console.error(`[Start] Initializing SmartGraphIndexerMCP for: ${projectPath}`);
28
+
29
+ const engine = new IndexerEngine(projectPath);
30
+ const server = createMCPServer(engine);
31
+ const transport = new StdioServerTransport();
32
+
33
+ await server.connect(transport);
34
+ console.error('[Start] MCP Server successfully connected via Stdio.');
35
+
36
+ // Dynamic workspace path discovery via client roots list
37
+ let resolvedPath = projectPath;
38
+ let rootsResult = "none";
39
+ let rootsError = "none";
40
+ try {
41
+ const response = await server.request({ method: 'roots/list' }, ListRootsResultSchema);
42
+ rootsResult = JSON.stringify(response);
43
+ if (response && response.roots && response.roots.length > 0) {
44
+ const rootUri = response.roots[0].uri;
45
+ if (rootUri.startsWith('file:///')) {
46
+ // Remove file:/// prefix
47
+ let pathFromUri = rootUri.substring(8);
48
+ pathFromUri = decodeURIComponent(pathFromUri);
49
+
50
+ // Normalize Windows/Unix path
51
+ resolvedPath = path.resolve(pathFromUri);
52
+ engine.updateProjectPath(resolvedPath);
53
+ }
54
+ }
55
+ } catch (err: any) {
56
+ rootsError = err?.message || String(err);
57
+ }
58
+
59
+
60
+ await engine.startIndexing();
61
+
62
+ // Handle termination gracefully
63
+ process.on('SIGINT', async () => {
64
+ console.error('[Shutdown] Closing watcher...');
65
+ await engine.shutdown();
66
+ process.exit(0);
67
+ });
68
+ process.on('SIGTERM', async () => {
69
+ console.error('[Shutdown] Closing watcher...');
70
+ await engine.shutdown();
71
+ process.exit(0);
72
+ });
73
+ } catch (err) {
74
+ console.error('Critical error starting the MCP server:', err);
75
+ process.exit(1);
76
+ }
77
+ });
78
+
79
+ program
80
+ .command('index')
81
+ .description('Performs only the initial indexing of the project and exports a JSON structural report')
82
+ .option('-p, --path <directory>', 'Target project directory to index', '.')
83
+ .action(async (options) => {
84
+ try {
85
+ const projectPath = path.resolve(options.path);
86
+ console.log(`[Index] Indexing project at: ${projectPath}`);
87
+
88
+ const engine = new IndexerEngine(projectPath);
89
+ await engine.initialize();
90
+
91
+ await (engine as any).scanDir(projectPath);
92
+
93
+ const report = engine.getReport();
94
+ console.log(JSON.stringify(report, null, 2));
95
+ process.exit(0);
96
+ } catch (err) {
97
+ console.error('Critical error during indexing:', err);
98
+ process.exit(1);
99
+ }
100
+ });
101
+
102
+ program
103
+ .command('view')
104
+ .description('Maps the project and opens an interactive web graph of dependencies')
105
+ .option('-p, --path <directory>', 'Target project directory to map', '.')
106
+ .option('-w, --watch', 'Monitors filesystem changes in real time and regenerates the graph automatically')
107
+ .action(async (options) => {
108
+ try {
109
+ const projectPath = path.resolve(options.path);
110
+ console.log(`[View] Mapping project dependencies at: ${projectPath}`);
111
+
112
+ let engine: IndexerEngine;
113
+
114
+ const writeHtml = () => {
115
+ const htmlContent = generateGraphHtml(engine);
116
+ const mcpDir = path.join(projectPath, '.graph-indexer');
117
+ if (!fs.existsSync(mcpDir)) {
118
+ fs.mkdirSync(mcpDir, { recursive: true });
119
+ }
120
+ const htmlPath = path.join(mcpDir, 'graph.html');
121
+ fs.writeFileSync(htmlPath, htmlContent, 'utf-8');
122
+ console.log(`[View] Interactive graph updated at: ${htmlPath}`);
123
+ };
124
+
125
+ if (options.watch) {
126
+ engine = new IndexerEngine(projectPath, () => {
127
+ writeHtml();
128
+ });
129
+ } else {
130
+ engine = new IndexerEngine(projectPath);
131
+ }
132
+
133
+ await engine.initialize();
134
+ await (engine as any).scanDir(projectPath);
135
+
136
+ // Write initial HTML
137
+ writeHtml();
138
+
139
+ const mcpDir = path.join(projectPath, '.graph-indexer');
140
+ const htmlPath = path.join(mcpDir, 'graph.html');
141
+
142
+ // Open in the default browser based on platform
143
+ console.log('[View] Opening interactive graph in default browser...');
144
+ const platform = process.platform;
145
+ const fileUrl = `file:///${htmlPath.replace(/\\/g, '/')}`;
146
+
147
+ if (platform === 'win32') {
148
+ exec(`start "" "${fileUrl}"`);
149
+ } else if (platform === 'darwin') {
150
+ exec(`open "${fileUrl}"`);
151
+ } else {
152
+ exec(`xdg-open "${fileUrl}"`);
153
+ }
154
+
155
+ if (options.watch) {
156
+ console.log('[View] Real-time monitoring (--watch) active. Press Ctrl+C to terminate.');
157
+ engine.startWatcher();
158
+
159
+ process.on('SIGINT', async () => {
160
+ console.log('[Shutdown] Closing watcher...');
161
+ await engine.shutdown();
162
+ process.exit(0);
163
+ });
164
+ } else {
165
+ process.exit(0);
166
+ }
167
+ } catch (err) {
168
+ console.error('Critical error generating graph view:', err);
169
+ process.exit(1);
170
+ }
171
+ });
172
+
173
+ program
174
+ .command('init')
175
+ .description('Initializes the current project directory for SmartGraphIndexer by creating the .graph-indexer config folder')
176
+ .option('-p, --path <directory>', 'Target directory to initialize', '.')
177
+ .action(async (options) => {
178
+ try {
179
+ const projectPath = path.resolve(options.path);
180
+ const mcpDir = path.join(projectPath, '.graph-indexer');
181
+
182
+ if (fs.existsSync(mcpDir)) {
183
+ console.log(`[Init] Project is already initialized at: ${projectPath} (.graph-indexer directory exists).`);
184
+ process.exit(0);
185
+ }
186
+
187
+ fs.mkdirSync(mcpDir, { recursive: true });
188
+
189
+ // Create initial empty stats file
190
+ const statsPath = path.join(mcpDir, 'stats.json');
191
+ const initialStats = {
192
+ totalCalls: 0,
193
+ totalTokensSaved: 0,
194
+ totalTokensActual: 0,
195
+ totalTokensAlternative: 0,
196
+ efficiencyPercentage: 0,
197
+ callsByType: {
198
+ search_symbols: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 },
199
+ get_file_skeleton: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 },
200
+ get_dependencies: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 },
201
+ read_symbol_body: { calls: 0, saved: 0, actual: 0, alternative: 0, totalTimeMs: 0 }
202
+ },
203
+ history: []
204
+ };
205
+ fs.writeFileSync(statsPath, JSON.stringify(initialStats, null, 2), 'utf-8');
206
+
207
+ console.log(`[Init] Successfully initialized SmartGraphIndexer workspace at: ${projectPath}`);
208
+ console.log('[Init] The MCP server will now index and analyze this project.');
209
+ process.exit(0);
210
+ } catch (err) {
211
+ console.error('Critical error during initialization:', err);
212
+ process.exit(1);
213
+ }
214
+ });
215
+
216
+ program.parse(process.argv);
@@ -0,0 +1,454 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import { ASTParser, FileSkeleton, SymbolSkeleton } from './parser.js';
10
+ import { DependencyGraph } from './graph.js';
11
+ import { VectorStore } from './vector.js';
12
+ import { GitAwareWatcher } from './watcher.js';
13
+ import { StatsManager } from './stats.js';
14
+
15
+ export class IndexerEngine {
16
+ public projectPath: string;
17
+ private parser: ASTParser;
18
+ private graph: DependencyGraph;
19
+ private vectorStore: VectorStore;
20
+ private watcher: GitAwareWatcher;
21
+ private fileSkeletons: Map<string, FileSkeleton> = new Map();
22
+ private isLoaded = false;
23
+ private stats: StatsManager;
24
+
25
+ constructor(projectPath: string, onUpdate?: () => void) {
26
+ this.projectPath = path.resolve(projectPath);
27
+ this.stats = new StatsManager(this.projectPath);
28
+ this.parser = new ASTParser();
29
+ this.graph = new DependencyGraph();
30
+ this.vectorStore = new VectorStore();
31
+ this.watcher = new GitAwareWatcher({
32
+ projectPath: this.projectPath,
33
+ onChange: async (changedFiles) => {
34
+ console.error(`[Watcher] Files modified, re-indexing: ${changedFiles.join(', ')}`);
35
+ for (const file of changedFiles) {
36
+ await this.indexFile(file);
37
+ }
38
+ if (onUpdate) {
39
+ onUpdate();
40
+ }
41
+ },
42
+ });
43
+ }
44
+
45
+ public updateProjectPath(newPath: string, onUpdate?: () => void) {
46
+ this.projectPath = path.resolve(newPath);
47
+ this.stats = new StatsManager(this.projectPath);
48
+ this.watcher = new GitAwareWatcher({
49
+ projectPath: this.projectPath,
50
+ onChange: async (changedFiles) => {
51
+ console.error(`[Watcher] Files modified, re-indexing: ${changedFiles.join(', ')}`);
52
+ for (const file of changedFiles) {
53
+ await this.indexFile(file);
54
+ }
55
+ if (onUpdate) {
56
+ onUpdate();
57
+ }
58
+ },
59
+ });
60
+ }
61
+
62
+ public startWatcher() {
63
+ this.watcher.start();
64
+ }
65
+
66
+ public async initialize() {
67
+ const wasmDir = path.join(__dirname, 'wasm');
68
+ await this.parser.init(wasmDir);
69
+ await this.vectorStore.init();
70
+ }
71
+
72
+ /**
73
+ * Scans and indexes the entire directory recursively.
74
+ */
75
+ public async startIndexing() {
76
+ await this.initialize();
77
+
78
+ // Check if the workspace is initialized for SmartGraphIndexer (.graph-indexer folder exists)
79
+ const mcpDir = path.join(this.projectPath, '.graph-indexer');
80
+ if (!fs.existsSync(mcpDir)) {
81
+ console.error(`[Indexer] .graph-indexer directory not found at: ${this.projectPath}. SmartGraphIndexerMCP is dormant for this workspace.`);
82
+ this.isLoaded = true;
83
+ return; // Do not scan or watch
84
+ }
85
+
86
+ const deltas = await this.watcher.getGitDeltas();
87
+ console.error(`[Indexer] Git status deltas detected: ${deltas.length} files.`);
88
+
89
+ // Recursive scan
90
+ await this.scanDir(this.projectPath);
91
+ this.isLoaded = true;
92
+
93
+ // Start watching filesystem
94
+ this.watcher.start();
95
+ console.error('[Indexer] Full initial indexing completed. File watcher active.');
96
+ }
97
+
98
+ private async scanDir(dir: string) {
99
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
100
+
101
+ for (const entry of entries) {
102
+ const fullPath = path.join(dir, entry.name);
103
+ const relativePath = path.relative(this.projectPath, fullPath).replace(/\\/g, '/');
104
+
105
+ if (this.watcher.isIgnored(relativePath)) {
106
+ continue;
107
+ }
108
+
109
+ if (entry.isDirectory()) {
110
+ await this.scanDir(fullPath);
111
+ } else if (entry.isFile()) {
112
+ const ext = path.extname(entry.name).toLowerCase();
113
+ const supported = [
114
+ '.cs', '.ts', '.tsx', '.js', '.jsx', '.py',
115
+ '.java', '.go', '.rs', '.rb',
116
+ '.cpp', '.hpp', '.cc', '.cxx', '.h', '.c'
117
+ ];
118
+ if (supported.includes(ext)) {
119
+ await this.indexFile(relativePath);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Indexes a single file.
127
+ */
128
+ public async indexFile(relativeFilePath: string) {
129
+ const normalizedPath = relativeFilePath.replace(/\\/g, '/');
130
+ const fullPath = path.resolve(this.projectPath, normalizedPath);
131
+ if (!fs.existsSync(fullPath)) {
132
+ this.fileSkeletons.delete(normalizedPath);
133
+ this.graph.removeFile(normalizedPath);
134
+ this.vectorStore.removeFileSymbols(normalizedPath);
135
+ return;
136
+ }
137
+
138
+ try {
139
+ const content = fs.readFileSync(fullPath, 'utf-8');
140
+ const hash = this.parser.getHash(content);
141
+
142
+ const cached = this.fileSkeletons.get(normalizedPath);
143
+ if (cached && cached.file_hash === hash) {
144
+ return;
145
+ }
146
+
147
+ const skeleton = this.parser.parseFile(normalizedPath, content);
148
+ this.fileSkeletons.set(normalizedPath, skeleton);
149
+
150
+ this.graph.updateFileDependencies(normalizedPath, skeleton.dependencies);
151
+
152
+ this.vectorStore.removeFileSymbols(normalizedPath);
153
+ for (const symbol of skeleton.symbols) {
154
+ await this.vectorStore.addSymbol(normalizedPath, symbol);
155
+ }
156
+ } catch (err) {
157
+ console.error(`Error indexing file ${normalizedPath}:`, err);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Gracefully updates a file on demand (for JIT read operations).
163
+ */
164
+ public async ensureFileUpToDate(relativeFilePath: string): Promise<FileSkeleton | undefined> {
165
+ await this.indexFile(relativeFilePath);
166
+ return this.fileSkeletons.get(relativeFilePath);
167
+ }
168
+
169
+ public getFileSkeleton(relativeFilePath: string): FileSkeleton | undefined {
170
+ return this.fileSkeletons.get(relativeFilePath);
171
+ }
172
+
173
+ public getGraph() {
174
+ return this.graph;
175
+ }
176
+
177
+ public getVectorStore() {
178
+ return this.vectorStore;
179
+ }
180
+
181
+ public getStatsManager(): StatsManager {
182
+ return this.stats;
183
+ }
184
+
185
+ public async shutdown() {
186
+ await this.watcher.stop();
187
+ }
188
+
189
+ public getReport() {
190
+ return {
191
+ totalFiles: this.fileSkeletons.size,
192
+ files: Array.from(this.fileSkeletons.keys()),
193
+ symbolsCount: this.vectorStore.getAllSymbols().length,
194
+ dependencyGraph: this.graph.serialize(),
195
+ tokenStats: this.stats.getStats(),
196
+ };
197
+ }
198
+ }
199
+
200
+ export function createMCPServer(engine: IndexerEngine): Server {
201
+ const server = new Server(
202
+ {
203
+ name: 'SmartGraphIndexerMCP',
204
+ version: '1.0.0',
205
+ },
206
+ {
207
+ capabilities: {
208
+ tools: {},
209
+ },
210
+ }
211
+ );
212
+
213
+ // 1. List available tools
214
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
215
+ return {
216
+ tools: [
217
+ {
218
+ name: 'search_symbols',
219
+ description: 'Performs hybrid search (semantic via vectors + lexical via text) over the indexed signatures database.',
220
+ inputSchema: {
221
+ type: 'object',
222
+ properties: {
223
+ query: {
224
+ type: 'string',
225
+ description: 'Search term or functional description of the logic you want to find.',
226
+ },
227
+ limit: {
228
+ type: 'integer',
229
+ description: 'Maximum number of matches to return.',
230
+ default: 5,
231
+ },
232
+ },
233
+ required: ['query'],
234
+ },
235
+ },
236
+ {
237
+ name: 'get_file_skeleton',
238
+ description: 'Returns the complete structure (skeleton) of a specific file without inner implementations.',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ file_path: {
243
+ type: 'string',
244
+ description: 'Relative path of the file in the project.',
245
+ },
246
+ },
247
+ required: ['file_path'],
248
+ },
249
+ },
250
+ {
251
+ name: 'get_dependencies',
252
+ description: 'Queries the in-memory graph and returns imports/dependants of a specific file.',
253
+ inputSchema: {
254
+ type: 'object',
255
+ properties: {
256
+ file_path: {
257
+ type: 'string',
258
+ description: 'Relative path of the source file.',
259
+ },
260
+ direction: {
261
+ type: 'string',
262
+ enum: ['incoming', 'outgoing', 'both'],
263
+ description: 'Direction of dependencies to query.',
264
+ default: 'both',
265
+ },
266
+ },
267
+ required: ['file_path'],
268
+ },
269
+ },
270
+ {
271
+ name: 'read_symbol_body',
272
+ description: 'JIT Extraction. Returns the actual, complete source code of a specific method or class based on coordinates.',
273
+ inputSchema: {
274
+ type: 'object',
275
+ properties: {
276
+ file_path: {
277
+ type: 'string',
278
+ description: 'Relative path of the file.',
279
+ },
280
+ symbol_name: {
281
+ type: 'string',
282
+ description: 'Exact name of the class or method to read.',
283
+ },
284
+ },
285
+ required: ['file_path', 'symbol_name'],
286
+ },
287
+ },
288
+ {
289
+ name: 'get_token_savings',
290
+ description: 'Returns token savings statistics accumulated in the project by using this MCP.',
291
+ inputSchema: {
292
+ type: 'object',
293
+ properties: {},
294
+ },
295
+ },
296
+ ],
297
+ };
298
+ });
299
+
300
+ // 2. Handle tool calls
301
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
302
+ const { name, arguments: args } = request.params;
303
+ const startTime = Date.now();
304
+
305
+ try {
306
+ if (name === 'search_symbols') {
307
+ const query = String(args?.query);
308
+ const limit = Number(args?.limit ?? 5);
309
+ const results = await engine.getVectorStore().search(query, limit);
310
+
311
+ const actualPayload = JSON.stringify(results, null, 2);
312
+ let alternativeText = '';
313
+ for (const res of results) {
314
+ const fullPath = path.resolve(engine.projectPath, res.symbol.filePath);
315
+ if (fs.existsSync(fullPath)) {
316
+ alternativeText += fs.readFileSync(fullPath, 'utf-8');
317
+ }
318
+ }
319
+
320
+ const execTimeMs = Date.now() - startTime;
321
+ engine.getStatsManager().recordCall('search_symbols', actualPayload, alternativeText, `Query: "${query}"`, execTimeMs);
322
+
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: actualPayload,
328
+ },
329
+ ],
330
+ };
331
+ }
332
+
333
+ if (name === 'get_file_skeleton') {
334
+ const filePath = String(args?.file_path).replace(/\\/g, '/');
335
+ const skeleton = await engine.ensureFileUpToDate(filePath);
336
+
337
+ if (!skeleton) {
338
+ throw new Error(`File ${filePath} not found or could not be parsed.`);
339
+ }
340
+
341
+ const actualPayload = JSON.stringify(skeleton, null, 2);
342
+ const fullPath = path.resolve(engine.projectPath, filePath);
343
+ const alternativeText = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf-8') : '';
344
+
345
+ const execTimeMs = Date.now() - startTime;
346
+ engine.getStatsManager().recordCall('get_file_skeleton', actualPayload, alternativeText, filePath, execTimeMs);
347
+
348
+ return {
349
+ content: [
350
+ {
351
+ type: 'text',
352
+ text: actualPayload,
353
+ },
354
+ ],
355
+ };
356
+ }
357
+
358
+ if (name === 'get_dependencies') {
359
+ const filePath = String(args?.file_path).replace(/\\/g, '/');
360
+ const direction = String(args?.direction ?? 'both');
361
+ const graph = engine.getGraph();
362
+
363
+ const result: Record<string, string[]> = {};
364
+ if (direction === 'outgoing' || direction === 'both') {
365
+ result.outgoing = graph.getOutgoing(filePath);
366
+ }
367
+ if (direction === 'incoming' || direction === 'both') {
368
+ result.incoming = graph.getIncoming(filePath);
369
+ }
370
+
371
+ const actualPayload = JSON.stringify(result, null, 2);
372
+ const fullPath = path.resolve(engine.projectPath, filePath);
373
+ const alternativeText = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf-8') : '';
374
+
375
+ const execTimeMs = Date.now() - startTime;
376
+ engine.getStatsManager().recordCall('get_dependencies', actualPayload, alternativeText, filePath, execTimeMs);
377
+
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text: actualPayload,
383
+ },
384
+ ],
385
+ };
386
+ }
387
+
388
+ if (name === 'read_symbol_body') {
389
+ const filePath = String(args?.file_path).replace(/\\/g, '/');
390
+ const symbolName = String(args?.symbol_name);
391
+
392
+ const skeleton = await engine.ensureFileUpToDate(filePath);
393
+ if (!skeleton) {
394
+ throw new Error(`File ${filePath} not found or could not be parsed.`);
395
+ }
396
+
397
+ const symbol = skeleton.symbols.find(s => s.name === symbolName);
398
+ if (!symbol) {
399
+ throw new Error(`Symbol ${symbolName} not found in ${filePath}.`);
400
+ }
401
+
402
+ const fullPath = path.resolve(engine.projectPath, filePath);
403
+ if (!fs.existsSync(fullPath)) {
404
+ throw new Error(`File ${filePath} does not exist on disk.`);
405
+ }
406
+
407
+ const fileLines = fs.readFileSync(fullPath, 'utf-8').split('\n');
408
+ const start = Math.max(0, symbol.start_line - 1);
409
+ const end = Math.min(fileLines.length, symbol.end_line);
410
+ const codeSlice = fileLines.slice(start, end).join('\n');
411
+
412
+ const fullContent = fileLines.join('\n');
413
+
414
+ const execTimeMs = Date.now() - startTime;
415
+ engine.getStatsManager().recordCall('read_symbol_body', codeSlice, fullContent, `${filePath} -> ${symbolName}`, execTimeMs);
416
+
417
+ return {
418
+ content: [
419
+ {
420
+ type: 'text',
421
+ text: codeSlice,
422
+ },
423
+ ],
424
+ };
425
+ }
426
+
427
+ if (name === 'get_token_savings') {
428
+ const stats = engine.getStatsManager().getStats();
429
+ return {
430
+ content: [
431
+ {
432
+ type: 'text',
433
+ text: JSON.stringify(stats, null, 2),
434
+ },
435
+ ],
436
+ };
437
+ }
438
+
439
+ throw new Error(`Unknown tool: ${name}`);
440
+ } catch (err: any) {
441
+ return {
442
+ isError: true,
443
+ content: [
444
+ {
445
+ type: 'text',
446
+ text: err?.message || String(err),
447
+ },
448
+ ],
449
+ };
450
+ }
451
+ });
452
+
453
+ return server;
454
+ }