@optave/codegraph 2.1.0 → 2.1.1-dev.0e15f12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -20
- package/package.json +5 -5
- package/src/builder.js +238 -33
- package/src/cli.js +73 -0
- package/src/db.js +4 -0
- package/src/extractors/csharp.js +6 -1
- package/src/extractors/go.js +6 -1
- package/src/extractors/java.js +4 -1
- package/src/extractors/javascript.js +145 -5
- package/src/extractors/php.js +8 -2
- package/src/extractors/python.js +8 -1
- package/src/extractors/ruby.js +4 -1
- package/src/extractors/rust.js +12 -2
- package/src/index.js +6 -0
- package/src/journal.js +109 -0
- package/src/mcp.js +121 -3
- package/src/parser.js +1 -0
- package/src/queries.js +1069 -22
- package/src/structure.js +14 -4
- package/src/watcher.js +25 -0
package/src/db.js
CHANGED
package/src/extractors/csharp.js
CHANGED
|
@@ -186,7 +186,12 @@ export function extractCSharpSymbols(tree, _filePath) {
|
|
|
186
186
|
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
187
187
|
} else if (fn.type === 'member_access_expression') {
|
|
188
188
|
const name = fn.childForFieldName('name');
|
|
189
|
-
if (name)
|
|
189
|
+
if (name) {
|
|
190
|
+
const expr = fn.childForFieldName('expression');
|
|
191
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
192
|
+
if (expr) call.receiver = expr.text;
|
|
193
|
+
calls.push(call);
|
|
194
|
+
}
|
|
190
195
|
} else if (fn.type === 'generic_name' || fn.type === 'member_binding_expression') {
|
|
191
196
|
const name = fn.childForFieldName('name') || fn.child(0);
|
|
192
197
|
if (name) calls.push({ name: name.text, line: node.startPosition.row + 1 });
|
package/src/extractors/go.js
CHANGED
|
@@ -152,7 +152,12 @@ export function extractGoSymbols(tree, _filePath) {
|
|
|
152
152
|
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
153
153
|
} else if (fn.type === 'selector_expression') {
|
|
154
154
|
const field = fn.childForFieldName('field');
|
|
155
|
-
if (field)
|
|
155
|
+
if (field) {
|
|
156
|
+
const operand = fn.childForFieldName('operand');
|
|
157
|
+
const call = { name: field.text, line: node.startPosition.row + 1 };
|
|
158
|
+
if (operand) call.receiver = operand.text;
|
|
159
|
+
calls.push(call);
|
|
160
|
+
}
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
break;
|
package/src/extractors/java.js
CHANGED
|
@@ -203,7 +203,10 @@ export function extractJavaSymbols(tree, _filePath) {
|
|
|
203
203
|
case 'method_invocation': {
|
|
204
204
|
const nameNode = node.childForFieldName('name');
|
|
205
205
|
if (nameNode) {
|
|
206
|
-
|
|
206
|
+
const obj = node.childForFieldName('object');
|
|
207
|
+
const call = { name: nameNode.text, line: node.startPosition.row + 1 };
|
|
208
|
+
if (obj) call.receiver = obj.text;
|
|
209
|
+
calls.push(call);
|
|
207
210
|
}
|
|
208
211
|
break;
|
|
209
212
|
}
|
|
@@ -140,6 +140,8 @@ export function extractSymbols(tree, _filePath) {
|
|
|
140
140
|
calls.push(callInfo);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
const cbDef = extractCallbackDefinition(node);
|
|
144
|
+
if (cbDef) definitions.push(cbDef);
|
|
143
145
|
break;
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -313,6 +315,18 @@ function extractImplementsFromNode(node) {
|
|
|
313
315
|
return result;
|
|
314
316
|
}
|
|
315
317
|
|
|
318
|
+
function extractReceiverName(objNode) {
|
|
319
|
+
if (!objNode) return undefined;
|
|
320
|
+
if (objNode.type === 'identifier') return objNode.text;
|
|
321
|
+
if (objNode.type === 'this') return 'this';
|
|
322
|
+
if (objNode.type === 'super') return 'super';
|
|
323
|
+
if (objNode.type === 'member_expression') {
|
|
324
|
+
const prop = objNode.childForFieldName('property');
|
|
325
|
+
if (prop) return objNode.text;
|
|
326
|
+
}
|
|
327
|
+
return objNode.text;
|
|
328
|
+
}
|
|
329
|
+
|
|
316
330
|
function extractCallInfo(fn, callNode) {
|
|
317
331
|
if (fn.type === 'identifier') {
|
|
318
332
|
return { name: fn.text, line: callNode.startPosition.row + 1 };
|
|
@@ -335,21 +349,147 @@ function extractCallInfo(fn, callNode) {
|
|
|
335
349
|
|
|
336
350
|
if (prop.type === 'string' || prop.type === 'string_fragment') {
|
|
337
351
|
const methodName = prop.text.replace(/['"]/g, '');
|
|
338
|
-
if (methodName)
|
|
339
|
-
|
|
352
|
+
if (methodName) {
|
|
353
|
+
const receiver = extractReceiverName(obj);
|
|
354
|
+
return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
|
|
355
|
+
}
|
|
340
356
|
}
|
|
341
357
|
|
|
342
|
-
|
|
358
|
+
const receiver = extractReceiverName(obj);
|
|
359
|
+
return { name: prop.text, line: callNode.startPosition.row + 1, receiver };
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
if (fn.type === 'subscript_expression') {
|
|
363
|
+
const obj = fn.childForFieldName('object');
|
|
346
364
|
const index = fn.childForFieldName('index');
|
|
347
365
|
if (index && (index.type === 'string' || index.type === 'template_string')) {
|
|
348
366
|
const methodName = index.text.replace(/['"`]/g, '');
|
|
349
|
-
if (methodName && !methodName.includes('$'))
|
|
350
|
-
|
|
367
|
+
if (methodName && !methodName.includes('$')) {
|
|
368
|
+
const receiver = extractReceiverName(obj);
|
|
369
|
+
return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function findAnonymousCallback(argsNode) {
|
|
378
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
379
|
+
const child = argsNode.child(i);
|
|
380
|
+
if (child && (child.type === 'arrow_function' || child.type === 'function_expression')) {
|
|
381
|
+
return child;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function findFirstStringArg(argsNode) {
|
|
388
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
389
|
+
const child = argsNode.child(i);
|
|
390
|
+
if (child && child.type === 'string') {
|
|
391
|
+
return child.text.replace(/['"]/g, '');
|
|
351
392
|
}
|
|
352
393
|
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function walkCallChain(startNode, methodName) {
|
|
398
|
+
let current = startNode;
|
|
399
|
+
while (current) {
|
|
400
|
+
if (current.type === 'call_expression') {
|
|
401
|
+
const fn = current.childForFieldName('function');
|
|
402
|
+
if (fn && fn.type === 'member_expression') {
|
|
403
|
+
const prop = fn.childForFieldName('property');
|
|
404
|
+
if (prop && prop.text === methodName) {
|
|
405
|
+
return current;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (current.type === 'member_expression') {
|
|
410
|
+
const obj = current.childForFieldName('object');
|
|
411
|
+
current = obj;
|
|
412
|
+
} else if (current.type === 'call_expression') {
|
|
413
|
+
const fn = current.childForFieldName('function');
|
|
414
|
+
current = fn;
|
|
415
|
+
} else {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const EXPRESS_METHODS = new Set([
|
|
423
|
+
'get',
|
|
424
|
+
'post',
|
|
425
|
+
'put',
|
|
426
|
+
'delete',
|
|
427
|
+
'patch',
|
|
428
|
+
'options',
|
|
429
|
+
'head',
|
|
430
|
+
'all',
|
|
431
|
+
'use',
|
|
432
|
+
]);
|
|
433
|
+
const EVENT_METHODS = new Set(['on', 'once', 'addEventListener', 'addListener']);
|
|
434
|
+
|
|
435
|
+
function extractCallbackDefinition(callNode) {
|
|
436
|
+
const fn = callNode.childForFieldName('function');
|
|
437
|
+
if (!fn || fn.type !== 'member_expression') return null;
|
|
438
|
+
|
|
439
|
+
const prop = fn.childForFieldName('property');
|
|
440
|
+
if (!prop) return null;
|
|
441
|
+
const method = prop.text;
|
|
442
|
+
|
|
443
|
+
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
|
|
444
|
+
if (!args) return null;
|
|
445
|
+
|
|
446
|
+
// Commander: .action(callback) with .command('name') in chain
|
|
447
|
+
if (method === 'action') {
|
|
448
|
+
const cb = findAnonymousCallback(args);
|
|
449
|
+
if (!cb) return null;
|
|
450
|
+
const commandCall = walkCallChain(fn.childForFieldName('object'), 'command');
|
|
451
|
+
if (!commandCall) return null;
|
|
452
|
+
const cmdArgs =
|
|
453
|
+
commandCall.childForFieldName('arguments') || findChild(commandCall, 'arguments');
|
|
454
|
+
if (!cmdArgs) return null;
|
|
455
|
+
const cmdName = findFirstStringArg(cmdArgs);
|
|
456
|
+
if (!cmdName) return null;
|
|
457
|
+
const firstWord = cmdName.split(/\s/)[0];
|
|
458
|
+
return {
|
|
459
|
+
name: `command:${firstWord}`,
|
|
460
|
+
kind: 'function',
|
|
461
|
+
line: cb.startPosition.row + 1,
|
|
462
|
+
endLine: nodeEndLine(cb),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Express: app.get('/path', callback)
|
|
467
|
+
if (EXPRESS_METHODS.has(method)) {
|
|
468
|
+
const strArg = findFirstStringArg(args);
|
|
469
|
+
if (!strArg || !strArg.startsWith('/')) return null;
|
|
470
|
+
const cb = findAnonymousCallback(args);
|
|
471
|
+
if (!cb) return null;
|
|
472
|
+
return {
|
|
473
|
+
name: `route:${method.toUpperCase()} ${strArg}`,
|
|
474
|
+
kind: 'function',
|
|
475
|
+
line: cb.startPosition.row + 1,
|
|
476
|
+
endLine: nodeEndLine(cb),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Events: emitter.on('event', callback)
|
|
481
|
+
if (EVENT_METHODS.has(method)) {
|
|
482
|
+
const eventName = findFirstStringArg(args);
|
|
483
|
+
if (!eventName) return null;
|
|
484
|
+
const cb = findAnonymousCallback(args);
|
|
485
|
+
if (!cb) return null;
|
|
486
|
+
return {
|
|
487
|
+
name: `event:${eventName}`,
|
|
488
|
+
kind: 'function',
|
|
489
|
+
line: cb.startPosition.row + 1,
|
|
490
|
+
endLine: nodeEndLine(cb),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
353
493
|
|
|
354
494
|
return null;
|
|
355
495
|
}
|
package/src/extractors/php.js
CHANGED
|
@@ -206,7 +206,10 @@ export function extractPHPSymbols(tree, _filePath) {
|
|
|
206
206
|
case 'member_call_expression': {
|
|
207
207
|
const name = node.childForFieldName('name');
|
|
208
208
|
if (name) {
|
|
209
|
-
|
|
209
|
+
const obj = node.childForFieldName('object');
|
|
210
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
211
|
+
if (obj) call.receiver = obj.text;
|
|
212
|
+
calls.push(call);
|
|
210
213
|
}
|
|
211
214
|
break;
|
|
212
215
|
}
|
|
@@ -214,7 +217,10 @@ export function extractPHPSymbols(tree, _filePath) {
|
|
|
214
217
|
case 'scoped_call_expression': {
|
|
215
218
|
const name = node.childForFieldName('name');
|
|
216
219
|
if (name) {
|
|
217
|
-
|
|
220
|
+
const scope = node.childForFieldName('scope');
|
|
221
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
222
|
+
if (scope) call.receiver = scope.text;
|
|
223
|
+
calls.push(call);
|
|
218
224
|
}
|
|
219
225
|
break;
|
|
220
226
|
}
|
package/src/extractors/python.js
CHANGED
|
@@ -69,12 +69,19 @@ export function extractPythonSymbols(tree, _filePath) {
|
|
|
69
69
|
const fn = node.childForFieldName('function');
|
|
70
70
|
if (fn) {
|
|
71
71
|
let callName = null;
|
|
72
|
+
let receiver;
|
|
72
73
|
if (fn.type === 'identifier') callName = fn.text;
|
|
73
74
|
else if (fn.type === 'attribute') {
|
|
74
75
|
const attr = fn.childForFieldName('attribute');
|
|
75
76
|
if (attr) callName = attr.text;
|
|
77
|
+
const obj = fn.childForFieldName('object');
|
|
78
|
+
if (obj) receiver = obj.text;
|
|
79
|
+
}
|
|
80
|
+
if (callName) {
|
|
81
|
+
const call = { name: callName, line: node.startPosition.row + 1 };
|
|
82
|
+
if (receiver) call.receiver = receiver;
|
|
83
|
+
calls.push(call);
|
|
76
84
|
}
|
|
77
|
-
if (callName) calls.push({ name: callName, line: node.startPosition.row + 1 });
|
|
78
85
|
}
|
|
79
86
|
break;
|
|
80
87
|
}
|
package/src/extractors/ruby.js
CHANGED
|
@@ -170,7 +170,10 @@ export function extractRubySymbols(tree, _filePath) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
} else {
|
|
173
|
-
|
|
173
|
+
const recv = node.childForFieldName('receiver');
|
|
174
|
+
const call = { name: methodNode.text, line: node.startPosition.row + 1 };
|
|
175
|
+
if (recv) call.receiver = recv.text;
|
|
176
|
+
calls.push(call);
|
|
174
177
|
}
|
|
175
178
|
}
|
|
176
179
|
break;
|
package/src/extractors/rust.js
CHANGED
|
@@ -135,10 +135,20 @@ export function extractRustSymbols(tree, _filePath) {
|
|
|
135
135
|
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
|
|
136
136
|
} else if (fn.type === 'field_expression') {
|
|
137
137
|
const field = fn.childForFieldName('field');
|
|
138
|
-
if (field)
|
|
138
|
+
if (field) {
|
|
139
|
+
const value = fn.childForFieldName('value');
|
|
140
|
+
const call = { name: field.text, line: node.startPosition.row + 1 };
|
|
141
|
+
if (value) call.receiver = value.text;
|
|
142
|
+
calls.push(call);
|
|
143
|
+
}
|
|
139
144
|
} else if (fn.type === 'scoped_identifier') {
|
|
140
145
|
const name = fn.childForFieldName('name');
|
|
141
|
-
if (name)
|
|
146
|
+
if (name) {
|
|
147
|
+
const path = fn.childForFieldName('path');
|
|
148
|
+
const call = { name: name.text, line: node.startPosition.row + 1 };
|
|
149
|
+
if (path) call.receiver = path.text;
|
|
150
|
+
calls.push(call);
|
|
151
|
+
}
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
break;
|
package/src/index.js
CHANGED
|
@@ -38,7 +38,12 @@ export { isNativeAvailable } from './native.js';
|
|
|
38
38
|
export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
|
|
39
39
|
// Query functions (data-returning)
|
|
40
40
|
export {
|
|
41
|
+
ALL_SYMBOL_KINDS,
|
|
42
|
+
contextData,
|
|
41
43
|
diffImpactData,
|
|
44
|
+
explainData,
|
|
45
|
+
FALSE_POSITIVE_CALLER_THRESHOLD,
|
|
46
|
+
FALSE_POSITIVE_NAMES,
|
|
42
47
|
fileDepsData,
|
|
43
48
|
fnDepsData,
|
|
44
49
|
fnImpactData,
|
|
@@ -46,6 +51,7 @@ export {
|
|
|
46
51
|
moduleMapData,
|
|
47
52
|
queryNameData,
|
|
48
53
|
statsData,
|
|
54
|
+
whereData,
|
|
49
55
|
} from './queries.js';
|
|
50
56
|
// Registry (multi-repo)
|
|
51
57
|
export {
|
package/src/journal.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { debug, warn } from './logger.js';
|
|
4
|
+
|
|
5
|
+
export const JOURNAL_FILENAME = 'changes.journal';
|
|
6
|
+
const HEADER_PREFIX = '# codegraph-journal v1 ';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read and validate the change journal.
|
|
10
|
+
* Returns { valid, timestamp, changed[], removed[] } or { valid: false }.
|
|
11
|
+
*/
|
|
12
|
+
export function readJournal(rootDir) {
|
|
13
|
+
const journalPath = path.join(rootDir, '.codegraph', JOURNAL_FILENAME);
|
|
14
|
+
let content;
|
|
15
|
+
try {
|
|
16
|
+
content = fs.readFileSync(journalPath, 'utf-8');
|
|
17
|
+
} catch {
|
|
18
|
+
return { valid: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
if (lines.length === 0 || !lines[0].startsWith(HEADER_PREFIX)) {
|
|
23
|
+
debug('Journal has malformed or missing header');
|
|
24
|
+
return { valid: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const timestamp = Number(lines[0].slice(HEADER_PREFIX.length).trim());
|
|
28
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
29
|
+
debug('Journal has invalid timestamp');
|
|
30
|
+
return { valid: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const changed = [];
|
|
34
|
+
const removed = [];
|
|
35
|
+
const seenChanged = new Set();
|
|
36
|
+
const seenRemoved = new Set();
|
|
37
|
+
|
|
38
|
+
for (let i = 1; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i].trim();
|
|
40
|
+
if (!line || line.startsWith('#')) continue;
|
|
41
|
+
|
|
42
|
+
if (line.startsWith('DELETED ')) {
|
|
43
|
+
const filePath = line.slice(8);
|
|
44
|
+
if (filePath && !seenRemoved.has(filePath)) {
|
|
45
|
+
seenRemoved.add(filePath);
|
|
46
|
+
removed.push(filePath);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
if (!seenChanged.has(line)) {
|
|
50
|
+
seenChanged.add(line);
|
|
51
|
+
changed.push(line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { valid: true, timestamp, changed, removed };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Append changed/deleted paths to the journal.
|
|
61
|
+
* Creates the journal with a header if it doesn't exist.
|
|
62
|
+
*/
|
|
63
|
+
export function appendJournalEntries(rootDir, entries) {
|
|
64
|
+
const dir = path.join(rootDir, '.codegraph');
|
|
65
|
+
const journalPath = path.join(dir, JOURNAL_FILENAME);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(dir)) {
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If journal doesn't exist, create with a placeholder header
|
|
72
|
+
if (!fs.existsSync(journalPath)) {
|
|
73
|
+
fs.writeFileSync(journalPath, `${HEADER_PREFIX}0\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = entries.map((e) => {
|
|
77
|
+
if (e.deleted) return `DELETED ${e.file}`;
|
|
78
|
+
return e.file;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
fs.appendFileSync(journalPath, `${lines.join('\n')}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Write a fresh journal header after a successful build.
|
|
86
|
+
* Atomic: write to temp file then rename.
|
|
87
|
+
*/
|
|
88
|
+
export function writeJournalHeader(rootDir, timestamp) {
|
|
89
|
+
const dir = path.join(rootDir, '.codegraph');
|
|
90
|
+
const journalPath = path.join(dir, JOURNAL_FILENAME);
|
|
91
|
+
const tmpPath = `${journalPath}.tmp`;
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(dir)) {
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
fs.writeFileSync(tmpPath, `${HEADER_PREFIX}${timestamp}\n`);
|
|
99
|
+
fs.renameSync(tmpPath, journalPath);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
warn(`Failed to write journal header: ${err.message}`);
|
|
102
|
+
// Clean up temp file if rename failed
|
|
103
|
+
try {
|
|
104
|
+
fs.unlinkSync(tmpPath);
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { findCycles } from './cycles.js';
|
|
10
10
|
import { findDbPath } from './db.js';
|
|
11
|
+
import { ALL_SYMBOL_KINDS } from './queries.js';
|
|
11
12
|
|
|
12
13
|
const REPO_PROP = {
|
|
13
14
|
repo: {
|
|
@@ -81,6 +82,15 @@ const BASE_TOOLS = [
|
|
|
81
82
|
properties: {
|
|
82
83
|
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
83
84
|
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
85
|
+
file: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
88
|
+
},
|
|
89
|
+
kind: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
enum: ALL_SYMBOL_KINDS,
|
|
92
|
+
description: 'Filter to a specific symbol kind',
|
|
93
|
+
},
|
|
84
94
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
85
95
|
},
|
|
86
96
|
required: ['name'],
|
|
@@ -95,11 +105,88 @@ const BASE_TOOLS = [
|
|
|
95
105
|
properties: {
|
|
96
106
|
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
97
107
|
depth: { type: 'number', description: 'Max traversal depth', default: 5 },
|
|
108
|
+
file: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
111
|
+
},
|
|
112
|
+
kind: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
enum: ALL_SYMBOL_KINDS,
|
|
115
|
+
description: 'Filter to a specific symbol kind',
|
|
116
|
+
},
|
|
98
117
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
99
118
|
},
|
|
100
119
|
required: ['name'],
|
|
101
120
|
},
|
|
102
121
|
},
|
|
122
|
+
{
|
|
123
|
+
name: 'context',
|
|
124
|
+
description:
|
|
125
|
+
'Full context for a function: source code, dependencies with summaries, callers, signature, and related tests — everything needed to understand or modify a function in one call',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
130
|
+
depth: {
|
|
131
|
+
type: 'number',
|
|
132
|
+
description: 'Include callee source up to N levels deep (0=no source, 1=direct)',
|
|
133
|
+
default: 0,
|
|
134
|
+
},
|
|
135
|
+
file: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
138
|
+
},
|
|
139
|
+
kind: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
enum: ALL_SYMBOL_KINDS,
|
|
142
|
+
description: 'Filter to a specific symbol kind',
|
|
143
|
+
},
|
|
144
|
+
no_source: {
|
|
145
|
+
type: 'boolean',
|
|
146
|
+
description: 'Skip source extraction (metadata only)',
|
|
147
|
+
default: false,
|
|
148
|
+
},
|
|
149
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
150
|
+
include_tests: {
|
|
151
|
+
type: 'boolean',
|
|
152
|
+
description: 'Include test file source code',
|
|
153
|
+
default: false,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ['name'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'explain',
|
|
161
|
+
description:
|
|
162
|
+
'Structural summary of a file or function: public/internal API, data flow, dependencies. No LLM needed.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
target: { type: 'string', description: 'File path or function name' },
|
|
167
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
168
|
+
},
|
|
169
|
+
required: ['target'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'where',
|
|
174
|
+
description:
|
|
175
|
+
'Find where a symbol is defined and used, or list symbols/imports/exports for a file. Minimal, fast lookup.',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
target: { type: 'string', description: 'Symbol name or file path' },
|
|
180
|
+
file_mode: {
|
|
181
|
+
type: 'boolean',
|
|
182
|
+
description: 'Treat target as file path (list symbols/imports/exports)',
|
|
183
|
+
default: false,
|
|
184
|
+
},
|
|
185
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
186
|
+
},
|
|
187
|
+
required: ['target'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
103
190
|
{
|
|
104
191
|
name: 'diff_impact',
|
|
105
192
|
description: 'Analyze git diff to find which functions changed and their transitive callers',
|
|
@@ -245,12 +332,15 @@ export { TOOLS, buildToolList };
|
|
|
245
332
|
export async function startMCPServer(customDbPath, options = {}) {
|
|
246
333
|
const { allowedRepos } = options;
|
|
247
334
|
const multiRepo = options.multiRepo || !!allowedRepos;
|
|
248
|
-
let Server, StdioServerTransport;
|
|
335
|
+
let Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema;
|
|
249
336
|
try {
|
|
250
337
|
const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
251
338
|
Server = sdk.Server;
|
|
252
339
|
const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
253
340
|
StdioServerTransport = transport.StdioServerTransport;
|
|
341
|
+
const types = await import('@modelcontextprotocol/sdk/types.js');
|
|
342
|
+
ListToolsRequestSchema = types.ListToolsRequestSchema;
|
|
343
|
+
CallToolRequestSchema = types.CallToolRequestSchema;
|
|
254
344
|
} catch {
|
|
255
345
|
console.error(
|
|
256
346
|
'MCP server requires @modelcontextprotocol/sdk.\n' +
|
|
@@ -267,6 +357,9 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
267
357
|
fileDepsData,
|
|
268
358
|
fnDepsData,
|
|
269
359
|
fnImpactData,
|
|
360
|
+
contextData,
|
|
361
|
+
explainData,
|
|
362
|
+
whereData,
|
|
270
363
|
diffImpactData,
|
|
271
364
|
listFunctionsData,
|
|
272
365
|
} = await import('./queries.js');
|
|
@@ -279,9 +372,11 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
279
372
|
{ capabilities: { tools: {} } },
|
|
280
373
|
);
|
|
281
374
|
|
|
282
|
-
server.setRequestHandler(
|
|
375
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
376
|
+
tools: buildToolList(multiRepo),
|
|
377
|
+
}));
|
|
283
378
|
|
|
284
|
-
server.setRequestHandler(
|
|
379
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
285
380
|
const { name, arguments: args } = request.params;
|
|
286
381
|
|
|
287
382
|
try {
|
|
@@ -334,12 +429,35 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
334
429
|
case 'fn_deps':
|
|
335
430
|
result = fnDepsData(args.name, dbPath, {
|
|
336
431
|
depth: args.depth,
|
|
432
|
+
file: args.file,
|
|
433
|
+
kind: args.kind,
|
|
337
434
|
noTests: args.no_tests,
|
|
338
435
|
});
|
|
339
436
|
break;
|
|
340
437
|
case 'fn_impact':
|
|
341
438
|
result = fnImpactData(args.name, dbPath, {
|
|
342
439
|
depth: args.depth,
|
|
440
|
+
file: args.file,
|
|
441
|
+
kind: args.kind,
|
|
442
|
+
noTests: args.no_tests,
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
case 'context':
|
|
446
|
+
result = contextData(args.name, dbPath, {
|
|
447
|
+
depth: args.depth,
|
|
448
|
+
file: args.file,
|
|
449
|
+
kind: args.kind,
|
|
450
|
+
noSource: args.no_source,
|
|
451
|
+
noTests: args.no_tests,
|
|
452
|
+
includeTests: args.include_tests,
|
|
453
|
+
});
|
|
454
|
+
break;
|
|
455
|
+
case 'explain':
|
|
456
|
+
result = explainData(args.target, dbPath, { noTests: args.no_tests });
|
|
457
|
+
break;
|
|
458
|
+
case 'where':
|
|
459
|
+
result = whereData(args.target, dbPath, {
|
|
460
|
+
file: args.file_mode,
|
|
343
461
|
noTests: args.no_tests,
|
|
344
462
|
});
|
|
345
463
|
break;
|