@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.
- package/.agents/rules/antigravity-rtk-rules.md +32 -0
- package/README.md +116 -0
- package/dist/graph.d.ts +32 -0
- package/dist/graph.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +227 -0
- package/dist/mcp_server.d.ts +48 -0
- package/dist/mcp_server.js +433 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +529 -0
- package/dist/stats.d.ts +43 -0
- package/dist/stats.js +146 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +1003 -0
- package/dist/vector.d.ts +66 -0
- package/dist/vector.js +144 -0
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/dist/watcher.d.ts +36 -0
- package/dist/watcher.js +166 -0
- package/mcp-config-example.json +12 -0
- package/mcp_smart_indexer_implementation_spec.md +366 -0
- package/package.json +35 -0
- package/src/graph.ts +93 -0
- package/src/index.ts +216 -0
- package/src/mcp_server.ts +454 -0
- package/src/parser.ts +484 -0
- package/src/stats.ts +156 -0
- package/src/ui.ts +956 -0
- package/src/vector.ts +166 -0
- package/src/watcher.ts +144 -0
- package/test_project/App.java +16 -0
- package/test_project/BillingService.cs +31 -0
- package/test_project/auth.ts +18 -0
- package/test_project/config.ts +11 -0
- package/test_project/database.ts +21 -0
- package/test_project/index.ts +13 -0
- package/test_project/main.go +11 -0
- package/test_project/main.py +21 -0
- package/test_project/processor.py +12 -0
- package/test_project/utils.py +13 -0
- package/tsconfig.json +16 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- 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
|
+
}
|