@optave/codegraph 2.5.1 → 3.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/src/mcp.js CHANGED
@@ -6,10 +6,11 @@
6
6
  */
7
7
 
8
8
  import { createRequire } from 'node:module';
9
+ import { AST_NODE_KINDS } from './ast.js';
9
10
  import { findCycles } from './cycles.js';
10
11
  import { findDbPath } from './db.js';
11
12
  import { MCP_DEFAULTS, MCP_MAX_LIMIT } from './paginate.js';
12
- import { ALL_SYMBOL_KINDS, diffImpactMermaid, VALID_ROLES } from './queries.js';
13
+ import { diffImpactMermaid, EVERY_EDGE_KIND, EVERY_SYMBOL_KIND, VALID_ROLES } from './queries.js';
13
14
 
14
15
  const REPO_PROP = {
15
16
  repo: {
@@ -25,23 +26,71 @@ const PAGINATION_PROPS = {
25
26
 
26
27
  const BASE_TOOLS = [
27
28
  {
28
- name: 'query_function',
29
- description: 'Find callers and callees of a function by name',
29
+ name: 'query',
30
+ description:
31
+ 'Query the call graph: find callers/callees with transitive chain, or find shortest path between two symbols',
30
32
  inputSchema: {
31
33
  type: 'object',
32
34
  properties: {
33
- name: { type: 'string', description: 'Function name to query (supports partial match)' },
35
+ name: { type: 'string', description: 'Function/method/class name (partial match)' },
36
+ mode: {
37
+ type: 'string',
38
+ enum: ['deps', 'path'],
39
+ description: 'deps (default): dependency chain. path: shortest path to target',
40
+ },
34
41
  depth: {
35
42
  type: 'number',
36
- description: 'Traversal depth for transitive callers',
37
- default: 2,
43
+ description: 'Transitive depth (deps default: 3, path default: 10)',
44
+ },
45
+ file: {
46
+ type: 'string',
47
+ description: 'Scope search to functions in this file (partial match)',
48
+ },
49
+ kind: {
50
+ type: 'string',
51
+ enum: EVERY_SYMBOL_KIND,
52
+ description: 'Filter by symbol kind',
53
+ },
54
+ to: { type: 'string', description: 'Target symbol for path mode (required in path mode)' },
55
+ edge_kinds: {
56
+ type: 'array',
57
+ items: { type: 'string', enum: EVERY_EDGE_KIND },
58
+ description: 'Edge kinds to follow in path mode (default: ["calls"])',
59
+ },
60
+ reverse: {
61
+ type: 'boolean',
62
+ description: 'Follow edges backward in path mode',
63
+ default: false,
38
64
  },
65
+ from_file: { type: 'string', description: 'Disambiguate source by file in path mode' },
66
+ to_file: { type: 'string', description: 'Disambiguate target by file in path mode' },
39
67
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
40
68
  ...PAGINATION_PROPS,
41
69
  },
42
70
  required: ['name'],
43
71
  },
44
72
  },
73
+ {
74
+ name: 'path',
75
+ description: 'Find shortest path between two symbols in the dependency graph',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ from: { type: 'string', description: 'Source symbol name' },
80
+ to: { type: 'string', description: 'Target symbol name' },
81
+ depth: { type: 'number', description: 'Max traversal depth (default: 10)' },
82
+ edge_kinds: {
83
+ type: 'array',
84
+ items: { type: 'string', enum: EVERY_EDGE_KIND },
85
+ description: 'Edge kinds to follow (default: ["calls"])',
86
+ },
87
+ from_file: { type: 'string', description: 'Disambiguate source by file' },
88
+ to_file: { type: 'string', description: 'Disambiguate target by file' },
89
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
90
+ },
91
+ required: ['from', 'to'],
92
+ },
93
+ },
45
94
  {
46
95
  name: 'file_deps',
47
96
  description: 'Show what a file imports and what imports it',
@@ -50,6 +99,21 @@ const BASE_TOOLS = [
50
99
  properties: {
51
100
  file: { type: 'string', description: 'File path (partial match supported)' },
52
101
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
102
+ ...PAGINATION_PROPS,
103
+ },
104
+ required: ['file'],
105
+ },
106
+ },
107
+ {
108
+ name: 'file_exports',
109
+ description:
110
+ 'Show exported symbols of a file with per-symbol consumers — who calls each export and from where',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ file: { type: 'string', description: 'File path (partial match supported)' },
115
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
116
+ ...PAGINATION_PROPS,
53
117
  },
54
118
  required: ['file'],
55
119
  },
@@ -62,6 +126,7 @@ const BASE_TOOLS = [
62
126
  properties: {
63
127
  file: { type: 'string', description: 'File path to analyze' },
64
128
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
129
+ ...PAGINATION_PROPS,
65
130
  },
66
131
  required: ['file'],
67
132
  },
@@ -85,28 +150,6 @@ const BASE_TOOLS = [
85
150
  },
86
151
  },
87
152
  },
88
- {
89
- name: 'fn_deps',
90
- description: 'Show function-level dependency chain: what a function calls and what calls it',
91
- inputSchema: {
92
- type: 'object',
93
- properties: {
94
- name: { type: 'string', description: 'Function/method/class name (partial match)' },
95
- depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
96
- file: {
97
- type: 'string',
98
- description: 'Scope search to functions in this file (partial match)',
99
- },
100
- kind: {
101
- type: 'string',
102
- enum: ALL_SYMBOL_KINDS,
103
- description: 'Filter to a specific symbol kind',
104
- },
105
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
106
- },
107
- required: ['name'],
108
- },
109
- },
110
153
  {
111
154
  name: 'fn_impact',
112
155
  description:
@@ -122,41 +165,15 @@ const BASE_TOOLS = [
122
165
  },
123
166
  kind: {
124
167
  type: 'string',
125
- enum: ALL_SYMBOL_KINDS,
168
+ enum: EVERY_SYMBOL_KIND,
126
169
  description: 'Filter to a specific symbol kind',
127
170
  },
128
171
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
172
+ ...PAGINATION_PROPS,
129
173
  },
130
174
  required: ['name'],
131
175
  },
132
176
  },
133
- {
134
- name: 'symbol_path',
135
- description: 'Find the shortest path between two symbols in the call graph (A calls...calls B)',
136
- inputSchema: {
137
- type: 'object',
138
- properties: {
139
- from: { type: 'string', description: 'Source symbol name (partial match)' },
140
- to: { type: 'string', description: 'Target symbol name (partial match)' },
141
- max_depth: { type: 'number', description: 'Maximum BFS depth', default: 10 },
142
- edge_kinds: {
143
- type: 'array',
144
- items: { type: 'string' },
145
- description: 'Edge kinds to follow (default: ["calls"])',
146
- },
147
- reverse: { type: 'boolean', description: 'Follow edges backward', default: false },
148
- from_file: { type: 'string', description: 'Disambiguate source by file (partial match)' },
149
- to_file: { type: 'string', description: 'Disambiguate target by file (partial match)' },
150
- kind: {
151
- type: 'string',
152
- enum: ALL_SYMBOL_KINDS,
153
- description: 'Filter both symbols by kind',
154
- },
155
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
156
- },
157
- required: ['from', 'to'],
158
- },
159
- },
160
177
  {
161
178
  name: 'context',
162
179
  description:
@@ -176,7 +193,7 @@ const BASE_TOOLS = [
176
193
  },
177
194
  kind: {
178
195
  type: 'string',
179
- enum: ALL_SYMBOL_KINDS,
196
+ enum: EVERY_SYMBOL_KIND,
180
197
  description: 'Filter to a specific symbol kind',
181
198
  },
182
199
  no_source: {
@@ -190,21 +207,25 @@ const BASE_TOOLS = [
190
207
  description: 'Include test file source code',
191
208
  default: false,
192
209
  },
210
+ ...PAGINATION_PROPS,
193
211
  },
194
212
  required: ['name'],
195
213
  },
196
214
  },
197
215
  {
198
- name: 'explain',
216
+ name: 'symbol_children',
199
217
  description:
200
- 'Structural summary of a file or function: public/internal API, data flow, dependencies. No LLM needed.',
218
+ 'List sub-declaration children of a symbol: parameters, properties, constants. Answers "what fields does this class have?" without reading source.',
201
219
  inputSchema: {
202
220
  type: 'object',
203
221
  properties: {
204
- target: { type: 'string', description: 'File path or function name' },
222
+ name: { type: 'string', description: 'Function/method/class name (partial match)' },
223
+ file: { type: 'string', description: 'Scope to file (partial match)' },
224
+ kind: { type: 'string', enum: EVERY_SYMBOL_KIND, description: 'Filter by symbol kind' },
205
225
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
226
+ ...PAGINATION_PROPS,
206
227
  },
207
- required: ['target'],
228
+ required: ['name'],
208
229
  },
209
230
  },
210
231
  {
@@ -241,32 +262,41 @@ const BASE_TOOLS = [
241
262
  enum: ['json', 'mermaid'],
242
263
  description: 'Output format (default: json)',
243
264
  },
265
+ ...PAGINATION_PROPS,
244
266
  },
245
267
  },
246
268
  },
247
269
  {
248
270
  name: 'semantic_search',
249
271
  description:
250
- 'Search code symbols by meaning using embeddings (requires prior `codegraph embed`)',
272
+ 'Search code symbols by meaning using embeddings and/or keyword matching (requires prior `codegraph embed`). Default hybrid mode combines BM25 keyword + semantic search for best results.',
251
273
  inputSchema: {
252
274
  type: 'object',
253
275
  properties: {
254
276
  query: { type: 'string', description: 'Natural language search query' },
255
277
  limit: { type: 'number', description: 'Max results to return', default: 15 },
256
278
  min_score: { type: 'number', description: 'Minimum similarity score (0-1)', default: 0.2 },
279
+ mode: {
280
+ type: 'string',
281
+ enum: ['hybrid', 'semantic', 'keyword'],
282
+ description:
283
+ 'Search mode: hybrid (BM25 + semantic, default), semantic (embeddings only), keyword (BM25 only)',
284
+ },
285
+ ...PAGINATION_PROPS,
257
286
  },
258
287
  required: ['query'],
259
288
  },
260
289
  },
261
290
  {
262
291
  name: 'export_graph',
263
- description: 'Export the dependency graph in DOT (Graphviz), Mermaid, or JSON format',
292
+ description:
293
+ 'Export the dependency graph in DOT, Mermaid, JSON, GraphML, GraphSON, or Neo4j CSV format',
264
294
  inputSchema: {
265
295
  type: 'object',
266
296
  properties: {
267
297
  format: {
268
298
  type: 'string',
269
- enum: ['dot', 'mermaid', 'json'],
299
+ enum: ['dot', 'mermaid', 'json', 'graphml', 'graphson', 'neo4j'],
270
300
  description: 'Export format',
271
301
  },
272
302
  file_level: {
@@ -312,6 +342,7 @@ const BASE_TOOLS = [
312
342
  description: 'Return all files without limit',
313
343
  default: false,
314
344
  },
345
+ ...PAGINATION_PROPS,
315
346
  },
316
347
  },
317
348
  },
@@ -333,28 +364,6 @@ const BASE_TOOLS = [
333
364
  },
334
365
  },
335
366
  },
336
- {
337
- name: 'hotspots',
338
- description:
339
- 'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
340
- inputSchema: {
341
- type: 'object',
342
- properties: {
343
- metric: {
344
- type: 'string',
345
- enum: ['fan-in', 'fan-out', 'density', 'coupling'],
346
- description: 'Metric to rank by',
347
- },
348
- level: {
349
- type: 'string',
350
- enum: ['file', 'directory'],
351
- description: 'Rank files or directories',
352
- },
353
- limit: { type: 'number', description: 'Number of results to return', default: 10 },
354
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
355
- },
356
- },
357
- },
358
367
  {
359
368
  name: 'co_changes',
360
369
  description:
@@ -373,20 +382,26 @@ const BASE_TOOLS = [
373
382
  default: 0.3,
374
383
  },
375
384
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
385
+ offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
376
386
  },
377
387
  },
378
388
  },
379
389
  {
380
390
  name: 'execution_flow',
381
391
  description:
382
- 'Trace execution flow forward from an entry point (route, command, event) through callees to leaf functions. Answers "what happens when X is called?"',
392
+ 'Trace execution flow forward from an entry point through callees to leaves, or list all entry points with list=true',
383
393
  inputSchema: {
384
394
  type: 'object',
385
395
  properties: {
386
396
  name: {
387
397
  type: 'string',
388
398
  description:
389
- 'Entry point or function name (e.g. "POST /login", "build"). Supports prefix-stripped matching.',
399
+ 'Entry point or function name (required unless list=true). Supports prefix-stripped matching.',
400
+ },
401
+ list: {
402
+ type: 'boolean',
403
+ description: 'List all entry points grouped by type',
404
+ default: false,
390
405
  },
391
406
  depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
392
407
  file: {
@@ -395,22 +410,10 @@ const BASE_TOOLS = [
395
410
  },
396
411
  kind: {
397
412
  type: 'string',
398
- enum: ALL_SYMBOL_KINDS,
413
+ enum: EVERY_SYMBOL_KIND,
399
414
  description: 'Filter to a specific symbol kind',
400
415
  },
401
416
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
402
- },
403
- required: ['name'],
404
- },
405
- },
406
- {
407
- name: 'list_entry_points',
408
- description:
409
- 'List all framework entry points (routes, commands, events) in the codebase, grouped by type',
410
- inputSchema: {
411
- type: 'object',
412
- properties: {
413
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
414
417
  ...PAGINATION_PROPS,
415
418
  },
416
419
  },
@@ -446,48 +449,284 @@ const BASE_TOOLS = [
446
449
  type: 'string',
447
450
  description: 'Filter by symbol kind (function, method, class, etc.)',
448
451
  },
452
+ offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
449
453
  },
450
454
  },
451
455
  },
452
456
  {
453
- name: 'manifesto',
457
+ name: 'communities',
454
458
  description:
455
- 'Evaluate manifesto rules and return pass/fail verdicts for code health. Checks function complexity, file metrics, and cycle rules against configured thresholds.',
459
+ 'Detect natural module boundaries using Louvain community detection. Compares discovered communities against directory structure and surfaces architectural drift.',
456
460
  inputSchema: {
457
461
  type: 'object',
458
462
  properties: {
459
- file: { type: 'string', description: 'Scope to file (partial match)' },
463
+ functions: {
464
+ type: 'boolean',
465
+ description: 'Function-level instead of file-level',
466
+ default: false,
467
+ },
468
+ resolution: {
469
+ type: 'number',
470
+ description: 'Louvain resolution parameter (higher = more communities)',
471
+ default: 1.0,
472
+ },
473
+ drift: {
474
+ type: 'boolean',
475
+ description: 'Show only drift analysis (omit community member lists)',
476
+ default: false,
477
+ },
460
478
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
479
+ ...PAGINATION_PROPS,
480
+ },
481
+ },
482
+ },
483
+ {
484
+ name: 'code_owners',
485
+ description:
486
+ 'Show CODEOWNERS mapping for files and functions. Shows ownership coverage, per-owner breakdown, and cross-owner boundary edges.',
487
+ inputSchema: {
488
+ type: 'object',
489
+ properties: {
490
+ file: { type: 'string', description: 'Scope to a specific file (partial match)' },
491
+ owner: { type: 'string', description: 'Filter to a specific owner (e.g. @team-name)' },
492
+ boundary: {
493
+ type: 'boolean',
494
+ description: 'Show cross-owner boundary edges',
495
+ default: false,
496
+ },
461
497
  kind: {
462
498
  type: 'string',
463
499
  description: 'Filter by symbol kind (function, method, class, etc.)',
464
500
  },
501
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
465
502
  },
466
503
  },
467
504
  },
468
505
  {
469
- name: 'communities',
506
+ name: 'audit',
470
507
  description:
471
- 'Detect natural module boundaries using Louvain community detection. Compares discovered communities against directory structure and surfaces architectural drift.',
508
+ 'Composite report combining explain, fn-impact, and health metrics for a file or function. Returns structure, blast radius, complexity, and threshold breaches in one call.',
472
509
  inputSchema: {
473
510
  type: 'object',
474
511
  properties: {
475
- functions: {
512
+ target: { type: 'string', description: 'File path or function name' },
513
+ quick: {
476
514
  type: 'boolean',
477
- description: 'Function-level instead of file-level',
515
+ description: 'Structural summary only (skip impact + health)',
478
516
  default: false,
479
517
  },
480
- resolution: {
518
+ depth: { type: 'number', description: 'Impact analysis depth (default: 3)', default: 3 },
519
+ file: { type: 'string', description: 'Scope to file (partial match)' },
520
+ kind: {
521
+ type: 'string',
522
+ description: 'Filter by symbol kind (function, method, class, etc.)',
523
+ },
524
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
525
+ ...PAGINATION_PROPS,
526
+ },
527
+ required: ['target'],
528
+ },
529
+ },
530
+ {
531
+ name: 'batch_query',
532
+ description:
533
+ 'Run a query command against multiple targets in one call. Returns all results in a single JSON payload — ideal for multi-agent dispatch.',
534
+ inputSchema: {
535
+ type: 'object',
536
+ properties: {
537
+ command: {
538
+ type: 'string',
539
+ enum: [
540
+ 'fn-impact',
541
+ 'context',
542
+ 'explain',
543
+ 'where',
544
+ 'query',
545
+ 'impact',
546
+ 'deps',
547
+ 'flow',
548
+ 'dataflow',
549
+ 'complexity',
550
+ ],
551
+ description: 'The query command to run for each target',
552
+ },
553
+ targets: {
554
+ type: 'array',
555
+ items: { type: 'string' },
556
+ description: 'List of target names (symbol names or file paths depending on command)',
557
+ },
558
+ depth: {
481
559
  type: 'number',
482
- description: 'Louvain resolution parameter (higher = more communities)',
483
- default: 1.0,
560
+ description: 'Traversal depth (for fn-impact, context, fn, flow)',
484
561
  },
485
- drift: {
562
+ file: {
563
+ type: 'string',
564
+ description: 'Scope to file (partial match)',
565
+ },
566
+ kind: {
567
+ type: 'string',
568
+ enum: EVERY_SYMBOL_KIND,
569
+ description: 'Filter symbol kind',
570
+ },
571
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
572
+ },
573
+ required: ['command', 'targets'],
574
+ },
575
+ },
576
+ {
577
+ name: 'triage',
578
+ description:
579
+ 'Ranked audit queue by composite risk score. Merges connectivity (fan-in), complexity (cognitive), churn (commit count), role classification, and maintainability index into a single weighted score.',
580
+ inputSchema: {
581
+ type: 'object',
582
+ properties: {
583
+ level: {
584
+ type: 'string',
585
+ enum: ['function', 'file', 'directory'],
586
+ description:
587
+ 'Granularity: function (default) | file | directory. File/directory shows hotspots',
588
+ },
589
+ sort: {
590
+ type: 'string',
591
+ enum: ['risk', 'complexity', 'churn', 'fan-in', 'mi'],
592
+ description: 'Sort metric (default: risk)',
593
+ },
594
+ min_score: {
595
+ type: 'number',
596
+ description: 'Only return symbols with risk score >= this threshold (0-1)',
597
+ },
598
+ role: {
599
+ type: 'string',
600
+ enum: VALID_ROLES,
601
+ description: 'Filter by role classification',
602
+ },
603
+ file: { type: 'string', description: 'Scope to file (partial match)' },
604
+ kind: {
605
+ type: 'string',
606
+ enum: ['function', 'method', 'class'],
607
+ description: 'Filter by symbol kind',
608
+ },
609
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
610
+ weights: {
611
+ type: 'object',
612
+ description:
613
+ 'Custom scoring weights (e.g. {"fanIn":1,"complexity":0,"churn":0,"role":0,"mi":0})',
614
+ },
615
+ ...PAGINATION_PROPS,
616
+ },
617
+ },
618
+ },
619
+ {
620
+ name: 'branch_compare',
621
+ description:
622
+ 'Compare code structure between two git refs (branches, tags, commits). Shows added/removed/changed symbols and transitive caller impact using temporary git worktrees.',
623
+ inputSchema: {
624
+ type: 'object',
625
+ properties: {
626
+ base: { type: 'string', description: 'Base git ref (branch, tag, or commit SHA)' },
627
+ target: { type: 'string', description: 'Target git ref to compare against base' },
628
+ depth: { type: 'number', description: 'Max transitive caller depth', default: 3 },
629
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
630
+ format: {
631
+ type: 'string',
632
+ enum: ['json', 'mermaid'],
633
+ description: 'Output format (default: json)',
634
+ },
635
+ },
636
+ required: ['base', 'target'],
637
+ },
638
+ },
639
+ {
640
+ name: 'cfg',
641
+ description: 'Show intraprocedural control flow graph for a function. Requires build --cfg.',
642
+ inputSchema: {
643
+ type: 'object',
644
+ properties: {
645
+ name: { type: 'string', description: 'Function/method name (partial match)' },
646
+ format: {
647
+ type: 'string',
648
+ enum: ['json', 'dot', 'mermaid'],
649
+ description: 'Output format (default: json)',
650
+ },
651
+ file: { type: 'string', description: 'Scope to file (partial match)' },
652
+ kind: { type: 'string', enum: EVERY_SYMBOL_KIND, description: 'Filter by symbol kind' },
653
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
654
+ ...PAGINATION_PROPS,
655
+ },
656
+ required: ['name'],
657
+ },
658
+ },
659
+ {
660
+ name: 'dataflow',
661
+ description: 'Show data flow edges or data-dependent blast radius. Requires build --dataflow.',
662
+ inputSchema: {
663
+ type: 'object',
664
+ properties: {
665
+ name: { type: 'string', description: 'Function/method name (partial match)' },
666
+ mode: {
667
+ type: 'string',
668
+ enum: ['edges', 'impact'],
669
+ description: 'edges (default) or impact',
670
+ },
671
+ depth: { type: 'number', description: 'Max depth for impact mode', default: 5 },
672
+ file: { type: 'string', description: 'Scope to file (partial match)' },
673
+ kind: { type: 'string', enum: EVERY_SYMBOL_KIND, description: 'Filter by symbol kind' },
674
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
675
+ ...PAGINATION_PROPS,
676
+ },
677
+ required: ['name'],
678
+ },
679
+ },
680
+ {
681
+ name: 'check',
682
+ description:
683
+ 'CI gate: run manifesto rules (no args), diff predicates (with ref/staged), or both (with rules flag). Returns pass/fail verdicts.',
684
+ inputSchema: {
685
+ type: 'object',
686
+ properties: {
687
+ ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
688
+ staged: { type: 'boolean', description: 'Analyze staged changes instead of unstaged' },
689
+ rules: {
486
690
  type: 'boolean',
487
- description: 'Show only drift analysis (omit community member lists)',
488
- default: false,
691
+ description: 'Also run manifesto rules alongside diff predicates',
692
+ },
693
+ cycles: { type: 'boolean', description: 'Enable cycles predicate (default: true)' },
694
+ blast_radius: {
695
+ type: 'number',
696
+ description: 'Max transitive callers threshold (null = disabled)',
697
+ },
698
+ signatures: { type: 'boolean', description: 'Enable signatures predicate (default: true)' },
699
+ boundaries: { type: 'boolean', description: 'Enable boundaries predicate (default: true)' },
700
+ depth: { type: 'number', description: 'Max BFS depth for blast radius (default: 3)' },
701
+ file: { type: 'string', description: 'Scope to file (partial match, manifesto mode)' },
702
+ kind: {
703
+ type: 'string',
704
+ description: 'Filter by symbol kind (manifesto mode)',
705
+ },
706
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
707
+ ...PAGINATION_PROPS,
708
+ },
709
+ },
710
+ },
711
+ {
712
+ name: 'ast_query',
713
+ description:
714
+ 'Search stored AST nodes (calls, literals, new, throw, await) by pattern. Requires a prior build.',
715
+ inputSchema: {
716
+ type: 'object',
717
+ properties: {
718
+ pattern: {
719
+ type: 'string',
720
+ description: 'GLOB pattern for node name (auto-wrapped in *..* for substring match)',
489
721
  },
722
+ kind: {
723
+ type: 'string',
724
+ enum: AST_NODE_KINDS,
725
+ description: 'Filter by AST node kind',
726
+ },
727
+ file: { type: 'string', description: 'Scope to file (partial match)' },
490
728
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
729
+ ...PAGINATION_PROPS,
491
730
  },
492
731
  },
493
732
  },
@@ -557,14 +796,15 @@ export async function startMCPServer(customDbPath, options = {}) {
557
796
 
558
797
  // Lazy import query functions to avoid circular deps at module load
559
798
  const {
560
- queryNameData,
561
799
  impactAnalysisData,
562
800
  moduleMapData,
563
801
  fileDepsData,
802
+ exportsData,
564
803
  fnDepsData,
565
804
  fnImpactData,
566
805
  pathData,
567
806
  contextData,
807
+ childrenData,
568
808
  explainData,
569
809
  whereData,
570
810
  diffImpactData,
@@ -615,18 +855,63 @@ export async function startMCPServer(customDbPath, options = {}) {
615
855
 
616
856
  let result;
617
857
  switch (name) {
618
- case 'query_function':
619
- result = queryNameData(args.name, dbPath, {
858
+ case 'query': {
859
+ const qMode = args.mode || 'deps';
860
+ if (qMode === 'path') {
861
+ if (!args.to) {
862
+ result = { error: 'path mode requires a "to" argument' };
863
+ break;
864
+ }
865
+ result = pathData(args.name, args.to, dbPath, {
866
+ maxDepth: args.depth ?? 10,
867
+ edgeKinds: args.edge_kinds,
868
+ reverse: args.reverse,
869
+ fromFile: args.from_file,
870
+ toFile: args.to_file,
871
+ kind: args.kind,
872
+ noTests: args.no_tests,
873
+ });
874
+ } else {
875
+ result = fnDepsData(args.name, dbPath, {
876
+ depth: args.depth,
877
+ file: args.file,
878
+ kind: args.kind,
879
+ noTests: args.no_tests,
880
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.query, MCP_MAX_LIMIT),
881
+ offset: args.offset ?? 0,
882
+ });
883
+ }
884
+ break;
885
+ }
886
+ case 'path':
887
+ result = pathData(args.from, args.to, dbPath, {
888
+ maxDepth: args.depth ?? 10,
889
+ edgeKinds: args.edge_kinds,
890
+ fromFile: args.from_file,
891
+ toFile: args.to_file,
620
892
  noTests: args.no_tests,
621
- limit: Math.min(args.limit ?? MCP_DEFAULTS.query_function, MCP_MAX_LIMIT),
622
- offset: args.offset ?? 0,
623
893
  });
624
894
  break;
625
895
  case 'file_deps':
626
- result = fileDepsData(args.file, dbPath, { noTests: args.no_tests });
896
+ result = fileDepsData(args.file, dbPath, {
897
+ noTests: args.no_tests,
898
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.file_deps, MCP_MAX_LIMIT),
899
+ offset: args.offset ?? 0,
900
+ });
901
+ break;
902
+ case 'file_exports':
903
+ result = exportsData(args.file, dbPath, {
904
+ noTests: args.no_tests,
905
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.file_exports, MCP_MAX_LIMIT),
906
+ offset: args.offset ?? 0,
907
+ });
627
908
  break;
628
909
  case 'impact_analysis':
629
- result = impactAnalysisData(args.file, dbPath, { noTests: args.no_tests });
910
+ result = impactAnalysisData(args.file, dbPath, {
911
+ noTests: args.no_tests,
912
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.impact_analysis, MCP_MAX_LIMIT),
913
+ offset: args.offset ?? 0,
914
+ });
630
915
  break;
631
916
  case 'find_cycles': {
632
917
  const db = new Database(findDbPath(dbPath), { readonly: true });
@@ -638,31 +923,14 @@ export async function startMCPServer(customDbPath, options = {}) {
638
923
  case 'module_map':
639
924
  result = moduleMapData(dbPath, args.limit || 20, { noTests: args.no_tests });
640
925
  break;
641
- case 'fn_deps':
642
- result = fnDepsData(args.name, dbPath, {
643
- depth: args.depth,
644
- file: args.file,
645
- kind: args.kind,
646
- noTests: args.no_tests,
647
- });
648
- break;
649
926
  case 'fn_impact':
650
927
  result = fnImpactData(args.name, dbPath, {
651
928
  depth: args.depth,
652
929
  file: args.file,
653
930
  kind: args.kind,
654
931
  noTests: args.no_tests,
655
- });
656
- break;
657
- case 'symbol_path':
658
- result = pathData(args.from, args.to, dbPath, {
659
- maxDepth: args.max_depth,
660
- edgeKinds: args.edge_kinds,
661
- reverse: args.reverse,
662
- fromFile: args.from_file,
663
- toFile: args.to_file,
664
- kind: args.kind,
665
- noTests: args.no_tests,
932
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_impact, MCP_MAX_LIMIT),
933
+ offset: args.offset ?? 0,
666
934
  });
667
935
  break;
668
936
  case 'context':
@@ -673,10 +941,18 @@ export async function startMCPServer(customDbPath, options = {}) {
673
941
  noSource: args.no_source,
674
942
  noTests: args.no_tests,
675
943
  includeTests: args.include_tests,
944
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.context, MCP_MAX_LIMIT),
945
+ offset: args.offset ?? 0,
676
946
  });
677
947
  break;
678
- case 'explain':
679
- result = explainData(args.target, dbPath, { noTests: args.no_tests });
948
+ case 'symbol_children':
949
+ result = childrenData(args.name, dbPath, {
950
+ file: args.file,
951
+ kind: args.kind,
952
+ noTests: args.no_tests,
953
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.context, MCP_MAX_LIMIT),
954
+ offset: args.offset ?? 0,
955
+ });
680
956
  break;
681
957
  case 'where':
682
958
  result = whereData(args.target, dbPath, {
@@ -700,27 +976,77 @@ export async function startMCPServer(customDbPath, options = {}) {
700
976
  ref: args.ref,
701
977
  depth: args.depth,
702
978
  noTests: args.no_tests,
979
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.diff_impact, MCP_MAX_LIMIT),
980
+ offset: args.offset ?? 0,
703
981
  });
704
982
  }
705
983
  break;
706
984
  case 'semantic_search': {
707
- const { searchData } = await import('./embedder.js');
708
- result = await searchData(args.query, dbPath, {
709
- limit: args.limit,
985
+ const mode = args.mode || 'hybrid';
986
+ const searchOpts = {
987
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.semantic_search, MCP_MAX_LIMIT),
988
+ offset: args.offset ?? 0,
710
989
  minScore: args.min_score,
711
- });
712
- if (result === null) {
713
- return {
714
- content: [
715
- { type: 'text', text: 'Semantic search unavailable. Run `codegraph embed` first.' },
716
- ],
717
- isError: true,
718
- };
990
+ };
991
+
992
+ if (mode === 'keyword') {
993
+ const { ftsSearchData } = await import('./embedder.js');
994
+ result = ftsSearchData(args.query, dbPath, searchOpts);
995
+ if (result === null) {
996
+ return {
997
+ content: [
998
+ {
999
+ type: 'text',
1000
+ text: 'No FTS5 index found. Run `codegraph embed` to build the keyword index.',
1001
+ },
1002
+ ],
1003
+ isError: true,
1004
+ };
1005
+ }
1006
+ } else if (mode === 'semantic') {
1007
+ const { searchData } = await import('./embedder.js');
1008
+ result = await searchData(args.query, dbPath, searchOpts);
1009
+ if (result === null) {
1010
+ return {
1011
+ content: [
1012
+ {
1013
+ type: 'text',
1014
+ text: 'Semantic search unavailable. Run `codegraph embed` first.',
1015
+ },
1016
+ ],
1017
+ isError: true,
1018
+ };
1019
+ }
1020
+ } else {
1021
+ // hybrid (default) — falls back to semantic if no FTS5
1022
+ const { hybridSearchData, searchData } = await import('./embedder.js');
1023
+ result = await hybridSearchData(args.query, dbPath, searchOpts);
1024
+ if (result === null) {
1025
+ result = await searchData(args.query, dbPath, searchOpts);
1026
+ if (result === null) {
1027
+ return {
1028
+ content: [
1029
+ {
1030
+ type: 'text',
1031
+ text: 'Semantic search unavailable. Run `codegraph embed` first.',
1032
+ },
1033
+ ],
1034
+ isError: true,
1035
+ };
1036
+ }
1037
+ }
719
1038
  }
720
1039
  break;
721
1040
  }
722
1041
  case 'export_graph': {
723
- const { exportDOT, exportMermaid, exportJSON } = await import('./export.js');
1042
+ const {
1043
+ exportDOT,
1044
+ exportGraphML,
1045
+ exportGraphSON,
1046
+ exportJSON,
1047
+ exportMermaid,
1048
+ exportNeo4jCSV,
1049
+ } = await import('./export.js');
724
1050
  const db = new Database(findDbPath(dbPath), { readonly: true });
725
1051
  const fileLevel = args.file_level !== false;
726
1052
  const exportLimit = args.limit
@@ -739,13 +1065,26 @@ export async function startMCPServer(customDbPath, options = {}) {
739
1065
  offset: args.offset ?? 0,
740
1066
  });
741
1067
  break;
1068
+ case 'graphml':
1069
+ result = exportGraphML(db, { fileLevel, limit: exportLimit });
1070
+ break;
1071
+ case 'graphson':
1072
+ result = exportGraphSON(db, {
1073
+ fileLevel,
1074
+ limit: exportLimit,
1075
+ offset: args.offset ?? 0,
1076
+ });
1077
+ break;
1078
+ case 'neo4j':
1079
+ result = exportNeo4jCSV(db, { fileLevel, limit: exportLimit });
1080
+ break;
742
1081
  default:
743
1082
  db.close();
744
1083
  return {
745
1084
  content: [
746
1085
  {
747
1086
  type: 'text',
748
- text: `Unknown format: ${args.format}. Use dot, mermaid, or json.`,
1087
+ text: `Unknown format: ${args.format}. Use dot, mermaid, json, graphml, graphson, or neo4j.`,
749
1088
  },
750
1089
  ],
751
1090
  isError: true,
@@ -779,16 +1118,8 @@ export async function startMCPServer(customDbPath, options = {}) {
779
1118
  depth: args.depth,
780
1119
  sort: args.sort,
781
1120
  full: args.full,
782
- });
783
- break;
784
- }
785
- case 'hotspots': {
786
- const { hotspotsData } = await import('./structure.js');
787
- result = hotspotsData(dbPath, {
788
- metric: args.metric,
789
- level: args.level,
790
- limit: args.limit,
791
- noTests: args.no_tests,
1121
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.structure, MCP_MAX_LIMIT),
1122
+ offset: args.offset ?? 0,
792
1123
  });
793
1124
  break;
794
1125
  }
@@ -796,34 +1127,42 @@ export async function startMCPServer(customDbPath, options = {}) {
796
1127
  const { coChangeData, coChangeTopData } = await import('./cochange.js');
797
1128
  result = args.file
798
1129
  ? coChangeData(args.file, dbPath, {
799
- limit: args.limit,
1130
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.co_changes, MCP_MAX_LIMIT),
1131
+ offset: args.offset ?? 0,
800
1132
  minJaccard: args.min_jaccard,
801
1133
  noTests: args.no_tests,
802
1134
  })
803
1135
  : coChangeTopData(dbPath, {
804
- limit: args.limit,
1136
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.co_changes, MCP_MAX_LIMIT),
1137
+ offset: args.offset ?? 0,
805
1138
  minJaccard: args.min_jaccard,
806
1139
  noTests: args.no_tests,
807
1140
  });
808
1141
  break;
809
1142
  }
810
1143
  case 'execution_flow': {
811
- const { flowData } = await import('./flow.js');
812
- result = flowData(args.name, dbPath, {
813
- depth: args.depth,
814
- file: args.file,
815
- kind: args.kind,
816
- noTests: args.no_tests,
817
- });
818
- break;
819
- }
820
- case 'list_entry_points': {
821
- const { listEntryPointsData } = await import('./flow.js');
822
- result = listEntryPointsData(dbPath, {
823
- noTests: args.no_tests,
824
- limit: Math.min(args.limit ?? MCP_DEFAULTS.list_entry_points, MCP_MAX_LIMIT),
825
- offset: args.offset ?? 0,
826
- });
1144
+ if (args.list) {
1145
+ const { listEntryPointsData } = await import('./flow.js');
1146
+ result = listEntryPointsData(dbPath, {
1147
+ noTests: args.no_tests,
1148
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1149
+ offset: args.offset ?? 0,
1150
+ });
1151
+ } else {
1152
+ if (!args.name) {
1153
+ result = { error: 'Provide a name or set list=true' };
1154
+ break;
1155
+ }
1156
+ const { flowData } = await import('./flow.js');
1157
+ result = flowData(args.name, dbPath, {
1158
+ depth: args.depth,
1159
+ file: args.file,
1160
+ kind: args.kind,
1161
+ noTests: args.no_tests,
1162
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1163
+ offset: args.offset ?? 0,
1164
+ });
1165
+ }
827
1166
  break;
828
1167
  }
829
1168
  case 'complexity': {
@@ -831,7 +1170,8 @@ export async function startMCPServer(customDbPath, options = {}) {
831
1170
  result = complexityData(dbPath, {
832
1171
  target: args.name,
833
1172
  file: args.file,
834
- limit: args.limit,
1173
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.complexity, MCP_MAX_LIMIT),
1174
+ offset: args.offset ?? 0,
835
1175
  sort: args.sort,
836
1176
  aboveThreshold: args.above_threshold,
837
1177
  health: args.health,
@@ -840,15 +1180,6 @@ export async function startMCPServer(customDbPath, options = {}) {
840
1180
  });
841
1181
  break;
842
1182
  }
843
- case 'manifesto': {
844
- const { manifestoData } = await import('./manifesto.js');
845
- result = manifestoData(dbPath, {
846
- file: args.file,
847
- noTests: args.no_tests,
848
- kind: args.kind,
849
- });
850
- break;
851
- }
852
1183
  case 'communities': {
853
1184
  const { communitiesData } = await import('./communities.js');
854
1185
  result = communitiesData(dbPath, {
@@ -856,6 +1187,184 @@ export async function startMCPServer(customDbPath, options = {}) {
856
1187
  resolution: args.resolution,
857
1188
  drift: args.drift,
858
1189
  noTests: args.no_tests,
1190
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.communities, MCP_MAX_LIMIT),
1191
+ offset: args.offset ?? 0,
1192
+ });
1193
+ break;
1194
+ }
1195
+ case 'code_owners': {
1196
+ const { ownersData } = await import('./owners.js');
1197
+ result = ownersData(dbPath, {
1198
+ file: args.file,
1199
+ owner: args.owner,
1200
+ boundary: args.boundary,
1201
+ kind: args.kind,
1202
+ noTests: args.no_tests,
1203
+ });
1204
+ break;
1205
+ }
1206
+ case 'audit': {
1207
+ if (args.quick) {
1208
+ result = explainData(args.target, dbPath, {
1209
+ noTests: args.no_tests,
1210
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.explain, MCP_MAX_LIMIT),
1211
+ offset: args.offset ?? 0,
1212
+ });
1213
+ } else {
1214
+ const { auditData } = await import('./audit.js');
1215
+ result = auditData(args.target, dbPath, {
1216
+ depth: args.depth,
1217
+ file: args.file,
1218
+ kind: args.kind,
1219
+ noTests: args.no_tests,
1220
+ });
1221
+ }
1222
+ break;
1223
+ }
1224
+ case 'batch_query': {
1225
+ const { batchData } = await import('./batch.js');
1226
+ result = batchData(args.command, args.targets, dbPath, {
1227
+ depth: args.depth,
1228
+ file: args.file,
1229
+ kind: args.kind,
1230
+ noTests: args.no_tests,
1231
+ });
1232
+ break;
1233
+ }
1234
+ case 'triage': {
1235
+ if (args.level === 'file' || args.level === 'directory') {
1236
+ const { hotspotsData } = await import('./structure.js');
1237
+ const TRIAGE_TO_HOTSPOT = {
1238
+ risk: 'fan-in',
1239
+ complexity: 'density',
1240
+ churn: 'coupling',
1241
+ mi: 'fan-in',
1242
+ };
1243
+ const metric = TRIAGE_TO_HOTSPOT[args.sort] ?? args.sort;
1244
+ result = hotspotsData(dbPath, {
1245
+ metric,
1246
+ level: args.level,
1247
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.hotspots, MCP_MAX_LIMIT),
1248
+ offset: args.offset ?? 0,
1249
+ noTests: args.no_tests,
1250
+ });
1251
+ } else {
1252
+ const { triageData } = await import('./triage.js');
1253
+ result = triageData(dbPath, {
1254
+ sort: args.sort,
1255
+ minScore: args.min_score,
1256
+ role: args.role,
1257
+ file: args.file,
1258
+ kind: args.kind,
1259
+ noTests: args.no_tests,
1260
+ weights: args.weights,
1261
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.triage, MCP_MAX_LIMIT),
1262
+ offset: args.offset ?? 0,
1263
+ });
1264
+ }
1265
+ break;
1266
+ }
1267
+ case 'branch_compare': {
1268
+ const { branchCompareData, branchCompareMermaid } = await import('./branch-compare.js');
1269
+ const bcData = await branchCompareData(args.base, args.target, {
1270
+ depth: args.depth,
1271
+ noTests: args.no_tests,
1272
+ });
1273
+ result = args.format === 'mermaid' ? branchCompareMermaid(bcData) : bcData;
1274
+ break;
1275
+ }
1276
+ case 'cfg': {
1277
+ const { cfgData, cfgToDOT, cfgToMermaid } = await import('./cfg.js');
1278
+ const cfgResult = cfgData(args.name, dbPath, {
1279
+ file: args.file,
1280
+ kind: args.kind,
1281
+ noTests: args.no_tests,
1282
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.query, MCP_MAX_LIMIT),
1283
+ offset: args.offset ?? 0,
1284
+ });
1285
+ if (args.format === 'dot') {
1286
+ result = { text: cfgToDOT(cfgResult) };
1287
+ } else if (args.format === 'mermaid') {
1288
+ result = { text: cfgToMermaid(cfgResult) };
1289
+ } else {
1290
+ result = cfgResult;
1291
+ }
1292
+ break;
1293
+ }
1294
+ case 'dataflow': {
1295
+ const dfMode = args.mode || 'edges';
1296
+ if (dfMode === 'impact') {
1297
+ const { dataflowImpactData } = await import('./dataflow.js');
1298
+ result = dataflowImpactData(args.name, dbPath, {
1299
+ depth: args.depth,
1300
+ file: args.file,
1301
+ kind: args.kind,
1302
+ noTests: args.no_tests,
1303
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_impact, MCP_MAX_LIMIT),
1304
+ offset: args.offset ?? 0,
1305
+ });
1306
+ } else {
1307
+ const { dataflowData } = await import('./dataflow.js');
1308
+ result = dataflowData(args.name, dbPath, {
1309
+ file: args.file,
1310
+ kind: args.kind,
1311
+ noTests: args.no_tests,
1312
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.query, MCP_MAX_LIMIT),
1313
+ offset: args.offset ?? 0,
1314
+ });
1315
+ }
1316
+ break;
1317
+ }
1318
+ case 'check': {
1319
+ const isDiffMode = args.ref || args.staged;
1320
+
1321
+ if (!isDiffMode && !args.rules) {
1322
+ // No ref, no staged → run manifesto rules on whole codebase
1323
+ const { manifestoData } = await import('./manifesto.js');
1324
+ result = manifestoData(dbPath, {
1325
+ file: args.file,
1326
+ noTests: args.no_tests,
1327
+ kind: args.kind,
1328
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.manifesto, MCP_MAX_LIMIT),
1329
+ offset: args.offset ?? 0,
1330
+ });
1331
+ } else {
1332
+ const { checkData } = await import('./check.js');
1333
+ const checkResult = checkData(dbPath, {
1334
+ ref: args.ref,
1335
+ staged: args.staged,
1336
+ cycles: args.cycles,
1337
+ blastRadius: args.blast_radius,
1338
+ signatures: args.signatures,
1339
+ boundaries: args.boundaries,
1340
+ depth: args.depth,
1341
+ noTests: args.no_tests,
1342
+ });
1343
+
1344
+ if (args.rules) {
1345
+ const { manifestoData } = await import('./manifesto.js');
1346
+ const manifestoResult = manifestoData(dbPath, {
1347
+ file: args.file,
1348
+ noTests: args.no_tests,
1349
+ kind: args.kind,
1350
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.manifesto, MCP_MAX_LIMIT),
1351
+ offset: args.offset ?? 0,
1352
+ });
1353
+ result = { check: checkResult, manifesto: manifestoResult };
1354
+ } else {
1355
+ result = checkResult;
1356
+ }
1357
+ }
1358
+ break;
1359
+ }
1360
+ case 'ast_query': {
1361
+ const { astQueryData } = await import('./ast.js');
1362
+ result = astQueryData(args.pattern, dbPath, {
1363
+ kind: args.kind,
1364
+ file: args.file,
1365
+ noTests: args.no_tests,
1366
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.ast_query, MCP_MAX_LIMIT),
1367
+ offset: args.offset ?? 0,
859
1368
  });
860
1369
  break;
861
1370
  }