@optave/codegraph 2.6.0 → 3.0.1

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"])',
38
59
  },
60
+ reverse: {
61
+ type: 'boolean',
62
+ description: 'Follow edges backward in path mode',
63
+ default: false,
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',
@@ -55,6 +104,20 @@ const BASE_TOOLS = [
55
104
  required: ['file'],
56
105
  },
57
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,
117
+ },
118
+ required: ['file'],
119
+ },
120
+ },
58
121
  {
59
122
  name: 'impact_analysis',
60
123
  description: 'Show files affected by changes to a given file (transitive)',
@@ -87,29 +150,6 @@ const BASE_TOOLS = [
87
150
  },
88
151
  },
89
152
  },
90
- {
91
- name: 'fn_deps',
92
- description: 'Show function-level dependency chain: what a function calls and what calls it',
93
- inputSchema: {
94
- type: 'object',
95
- properties: {
96
- name: { type: 'string', description: 'Function/method/class name (partial match)' },
97
- depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
98
- file: {
99
- type: 'string',
100
- description: 'Scope search to functions in this file (partial match)',
101
- },
102
- kind: {
103
- type: 'string',
104
- enum: ALL_SYMBOL_KINDS,
105
- description: 'Filter to a specific symbol kind',
106
- },
107
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
108
- ...PAGINATION_PROPS,
109
- },
110
- required: ['name'],
111
- },
112
- },
113
153
  {
114
154
  name: 'fn_impact',
115
155
  description:
@@ -125,7 +165,7 @@ const BASE_TOOLS = [
125
165
  },
126
166
  kind: {
127
167
  type: 'string',
128
- enum: ALL_SYMBOL_KINDS,
168
+ enum: EVERY_SYMBOL_KIND,
129
169
  description: 'Filter to a specific symbol kind',
130
170
  },
131
171
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
@@ -134,33 +174,6 @@ const BASE_TOOLS = [
134
174
  required: ['name'],
135
175
  },
136
176
  },
137
- {
138
- name: 'symbol_path',
139
- description: 'Find the shortest path between two symbols in the call graph (A calls...calls B)',
140
- inputSchema: {
141
- type: 'object',
142
- properties: {
143
- from: { type: 'string', description: 'Source symbol name (partial match)' },
144
- to: { type: 'string', description: 'Target symbol name (partial match)' },
145
- max_depth: { type: 'number', description: 'Maximum BFS depth', default: 10 },
146
- edge_kinds: {
147
- type: 'array',
148
- items: { type: 'string' },
149
- description: 'Edge kinds to follow (default: ["calls"])',
150
- },
151
- reverse: { type: 'boolean', description: 'Follow edges backward', default: false },
152
- from_file: { type: 'string', description: 'Disambiguate source by file (partial match)' },
153
- to_file: { type: 'string', description: 'Disambiguate target by file (partial match)' },
154
- kind: {
155
- type: 'string',
156
- enum: ALL_SYMBOL_KINDS,
157
- description: 'Filter both symbols by kind',
158
- },
159
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
160
- },
161
- required: ['from', 'to'],
162
- },
163
- },
164
177
  {
165
178
  name: 'context',
166
179
  description:
@@ -180,7 +193,7 @@ const BASE_TOOLS = [
180
193
  },
181
194
  kind: {
182
195
  type: 'string',
183
- enum: ALL_SYMBOL_KINDS,
196
+ enum: EVERY_SYMBOL_KIND,
184
197
  description: 'Filter to a specific symbol kind',
185
198
  },
186
199
  no_source: {
@@ -200,17 +213,19 @@ const BASE_TOOLS = [
200
213
  },
201
214
  },
202
215
  {
203
- name: 'explain',
216
+ name: 'symbol_children',
204
217
  description:
205
- '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.',
206
219
  inputSchema: {
207
220
  type: 'object',
208
221
  properties: {
209
- 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' },
210
225
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
211
226
  ...PAGINATION_PROPS,
212
227
  },
213
- required: ['target'],
228
+ required: ['name'],
214
229
  },
215
230
  },
216
231
  {
@@ -274,13 +289,14 @@ const BASE_TOOLS = [
274
289
  },
275
290
  {
276
291
  name: 'export_graph',
277
- 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',
278
294
  inputSchema: {
279
295
  type: 'object',
280
296
  properties: {
281
297
  format: {
282
298
  type: 'string',
283
- enum: ['dot', 'mermaid', 'json'],
299
+ enum: ['dot', 'mermaid', 'json', 'graphml', 'graphson', 'neo4j'],
284
300
  description: 'Export format',
285
301
  },
286
302
  file_level: {
@@ -348,29 +364,6 @@ const BASE_TOOLS = [
348
364
  },
349
365
  },
350
366
  },
351
- {
352
- name: 'hotspots',
353
- description:
354
- 'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
355
- inputSchema: {
356
- type: 'object',
357
- properties: {
358
- metric: {
359
- type: 'string',
360
- enum: ['fan-in', 'fan-out', 'density', 'coupling'],
361
- description: 'Metric to rank by',
362
- },
363
- level: {
364
- type: 'string',
365
- enum: ['file', 'directory'],
366
- description: 'Rank files or directories',
367
- },
368
- limit: { type: 'number', description: 'Number of results to return', default: 10 },
369
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
370
- offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
371
- },
372
- },
373
- },
374
367
  {
375
368
  name: 'co_changes',
376
369
  description:
@@ -396,14 +389,19 @@ const BASE_TOOLS = [
396
389
  {
397
390
  name: 'execution_flow',
398
391
  description:
399
- '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',
400
393
  inputSchema: {
401
394
  type: 'object',
402
395
  properties: {
403
396
  name: {
404
397
  type: 'string',
405
398
  description:
406
- '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,
407
405
  },
408
406
  depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
409
407
  file: {
@@ -412,25 +410,12 @@ const BASE_TOOLS = [
412
410
  },
413
411
  kind: {
414
412
  type: 'string',
415
- enum: ALL_SYMBOL_KINDS,
413
+ enum: EVERY_SYMBOL_KIND,
416
414
  description: 'Filter to a specific symbol kind',
417
415
  },
418
416
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
419
417
  ...PAGINATION_PROPS,
420
418
  },
421
- required: ['name'],
422
- },
423
- },
424
- {
425
- name: 'list_entry_points',
426
- description:
427
- 'List all framework entry points (routes, commands, events) in the codebase, grouped by type',
428
- inputSchema: {
429
- type: 'object',
430
- properties: {
431
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
432
- ...PAGINATION_PROPS,
433
- },
434
419
  },
435
420
  },
436
421
  {
@@ -468,23 +453,6 @@ const BASE_TOOLS = [
468
453
  },
469
454
  },
470
455
  },
471
- {
472
- name: 'manifesto',
473
- description:
474
- 'Evaluate manifesto rules and return pass/fail verdicts for code health. Checks function complexity, file metrics, and cycle rules against configured thresholds.',
475
- inputSchema: {
476
- type: 'object',
477
- properties: {
478
- file: { type: 'string', description: 'Scope to file (partial match)' },
479
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
480
- kind: {
481
- type: 'string',
482
- description: 'Filter by symbol kind (function, method, class, etc.)',
483
- },
484
- ...PAGINATION_PROPS,
485
- },
486
- },
487
- },
488
456
  {
489
457
  name: 'communities',
490
458
  description:
@@ -542,6 +510,11 @@ const BASE_TOOLS = [
542
510
  type: 'object',
543
511
  properties: {
544
512
  target: { type: 'string', description: 'File path or function name' },
513
+ quick: {
514
+ type: 'boolean',
515
+ description: 'Structural summary only (skip impact + health)',
516
+ default: false,
517
+ },
545
518
  depth: { type: 'number', description: 'Impact analysis depth (default: 3)', default: 3 },
546
519
  file: { type: 'string', description: 'Scope to file (partial match)' },
547
520
  kind: {
@@ -549,6 +522,7 @@ const BASE_TOOLS = [
549
522
  description: 'Filter by symbol kind (function, method, class, etc.)',
550
523
  },
551
524
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
525
+ ...PAGINATION_PROPS,
552
526
  },
553
527
  required: ['target'],
554
528
  },
@@ -568,10 +542,10 @@ const BASE_TOOLS = [
568
542
  'explain',
569
543
  'where',
570
544
  'query',
571
- 'fn',
572
545
  'impact',
573
546
  'deps',
574
547
  'flow',
548
+ 'dataflow',
575
549
  'complexity',
576
550
  ],
577
551
  description: 'The query command to run for each target',
@@ -591,7 +565,7 @@ const BASE_TOOLS = [
591
565
  },
592
566
  kind: {
593
567
  type: 'string',
594
- enum: ALL_SYMBOL_KINDS,
568
+ enum: EVERY_SYMBOL_KIND,
595
569
  description: 'Filter symbol kind',
596
570
  },
597
571
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
@@ -606,6 +580,12 @@ const BASE_TOOLS = [
606
580
  inputSchema: {
607
581
  type: 'object',
608
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
+ },
609
589
  sort: {
610
590
  type: 'string',
611
591
  enum: ['risk', 'complexity', 'churn', 'fan-in', 'mi'],
@@ -656,15 +636,60 @@ const BASE_TOOLS = [
656
636
  required: ['base', 'target'],
657
637
  },
658
638
  },
639
+ {
640
+ name: 'cfg',
641
+ description: 'Show intraprocedural control flow graph for a function.',
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.',
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
+ },
659
680
  {
660
681
  name: 'check',
661
682
  description:
662
- 'Run CI validation predicates against git changes. Checks for new cycles, blast radius violations, signature changes, and boundary violations. Returns pass/fail per predicate — ideal for CI gates.',
683
+ 'CI gate: run manifesto rules (no args), diff predicates (with ref/staged), or both (with rules flag). Returns pass/fail verdicts.',
663
684
  inputSchema: {
664
685
  type: 'object',
665
686
  properties: {
666
687
  ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
667
688
  staged: { type: 'boolean', description: 'Analyze staged changes instead of unstaged' },
689
+ rules: {
690
+ type: 'boolean',
691
+ description: 'Also run manifesto rules alongside diff predicates',
692
+ },
668
693
  cycles: { type: 'boolean', description: 'Enable cycles predicate (default: true)' },
669
694
  blast_radius: {
670
695
  type: 'number',
@@ -673,7 +698,35 @@ const BASE_TOOLS = [
673
698
  signatures: { type: 'boolean', description: 'Enable signatures predicate (default: true)' },
674
699
  boundaries: { type: 'boolean', description: 'Enable boundaries predicate (default: true)' },
675
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)',
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)' },
676
728
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
729
+ ...PAGINATION_PROPS,
677
730
  },
678
731
  },
679
732
  },
@@ -743,14 +796,15 @@ export async function startMCPServer(customDbPath, options = {}) {
743
796
 
744
797
  // Lazy import query functions to avoid circular deps at module load
745
798
  const {
746
- queryNameData,
747
799
  impactAnalysisData,
748
800
  moduleMapData,
749
801
  fileDepsData,
802
+ exportsData,
750
803
  fnDepsData,
751
804
  fnImpactData,
752
805
  pathData,
753
806
  contextData,
807
+ childrenData,
754
808
  explainData,
755
809
  whereData,
756
810
  diffImpactData,
@@ -801,11 +855,41 @@ export async function startMCPServer(customDbPath, options = {}) {
801
855
 
802
856
  let result;
803
857
  switch (name) {
804
- case 'query_function':
805
- 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,
806
892
  noTests: args.no_tests,
807
- limit: Math.min(args.limit ?? MCP_DEFAULTS.query_function, MCP_MAX_LIMIT),
808
- offset: args.offset ?? 0,
809
893
  });
810
894
  break;
811
895
  case 'file_deps':
@@ -815,6 +899,13 @@ export async function startMCPServer(customDbPath, options = {}) {
815
899
  offset: args.offset ?? 0,
816
900
  });
817
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
+ });
908
+ break;
818
909
  case 'impact_analysis':
819
910
  result = impactAnalysisData(args.file, dbPath, {
820
911
  noTests: args.no_tests,
@@ -832,16 +923,6 @@ export async function startMCPServer(customDbPath, options = {}) {
832
923
  case 'module_map':
833
924
  result = moduleMapData(dbPath, args.limit || 20, { noTests: args.no_tests });
834
925
  break;
835
- case 'fn_deps':
836
- result = fnDepsData(args.name, dbPath, {
837
- depth: args.depth,
838
- file: args.file,
839
- kind: args.kind,
840
- noTests: args.no_tests,
841
- limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_deps, MCP_MAX_LIMIT),
842
- offset: args.offset ?? 0,
843
- });
844
- break;
845
926
  case 'fn_impact':
846
927
  result = fnImpactData(args.name, dbPath, {
847
928
  depth: args.depth,
@@ -852,17 +933,6 @@ export async function startMCPServer(customDbPath, options = {}) {
852
933
  offset: args.offset ?? 0,
853
934
  });
854
935
  break;
855
- case 'symbol_path':
856
- result = pathData(args.from, args.to, dbPath, {
857
- maxDepth: args.max_depth,
858
- edgeKinds: args.edge_kinds,
859
- reverse: args.reverse,
860
- fromFile: args.from_file,
861
- toFile: args.to_file,
862
- kind: args.kind,
863
- noTests: args.no_tests,
864
- });
865
- break;
866
936
  case 'context':
867
937
  result = contextData(args.name, dbPath, {
868
938
  depth: args.depth,
@@ -875,10 +945,12 @@ export async function startMCPServer(customDbPath, options = {}) {
875
945
  offset: args.offset ?? 0,
876
946
  });
877
947
  break;
878
- case 'explain':
879
- result = explainData(args.target, dbPath, {
948
+ case 'symbol_children':
949
+ result = childrenData(args.name, dbPath, {
950
+ file: args.file,
951
+ kind: args.kind,
880
952
  noTests: args.no_tests,
881
- limit: Math.min(args.limit ?? MCP_DEFAULTS.explain, MCP_MAX_LIMIT),
953
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.context, MCP_MAX_LIMIT),
882
954
  offset: args.offset ?? 0,
883
955
  });
884
956
  break;
@@ -967,7 +1039,14 @@ export async function startMCPServer(customDbPath, options = {}) {
967
1039
  break;
968
1040
  }
969
1041
  case 'export_graph': {
970
- 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');
971
1050
  const db = new Database(findDbPath(dbPath), { readonly: true });
972
1051
  const fileLevel = args.file_level !== false;
973
1052
  const exportLimit = args.limit
@@ -986,13 +1065,26 @@ export async function startMCPServer(customDbPath, options = {}) {
986
1065
  offset: args.offset ?? 0,
987
1066
  });
988
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;
989
1081
  default:
990
1082
  db.close();
991
1083
  return {
992
1084
  content: [
993
1085
  {
994
1086
  type: 'text',
995
- text: `Unknown format: ${args.format}. Use dot, mermaid, or json.`,
1087
+ text: `Unknown format: ${args.format}. Use dot, mermaid, json, graphml, graphson, or neo4j.`,
996
1088
  },
997
1089
  ],
998
1090
  isError: true,
@@ -1031,17 +1123,6 @@ export async function startMCPServer(customDbPath, options = {}) {
1031
1123
  });
1032
1124
  break;
1033
1125
  }
1034
- case 'hotspots': {
1035
- const { hotspotsData } = await import('./structure.js');
1036
- result = hotspotsData(dbPath, {
1037
- metric: args.metric,
1038
- level: args.level,
1039
- limit: Math.min(args.limit ?? MCP_DEFAULTS.hotspots, MCP_MAX_LIMIT),
1040
- offset: args.offset ?? 0,
1041
- noTests: args.no_tests,
1042
- });
1043
- break;
1044
- }
1045
1126
  case 'co_changes': {
1046
1127
  const { coChangeData, coChangeTopData } = await import('./cochange.js');
1047
1128
  result = args.file
@@ -1060,24 +1141,28 @@ export async function startMCPServer(customDbPath, options = {}) {
1060
1141
  break;
1061
1142
  }
1062
1143
  case 'execution_flow': {
1063
- const { flowData } = await import('./flow.js');
1064
- result = flowData(args.name, dbPath, {
1065
- depth: args.depth,
1066
- file: args.file,
1067
- kind: args.kind,
1068
- noTests: args.no_tests,
1069
- limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1070
- offset: args.offset ?? 0,
1071
- });
1072
- break;
1073
- }
1074
- case 'list_entry_points': {
1075
- const { listEntryPointsData } = await import('./flow.js');
1076
- result = listEntryPointsData(dbPath, {
1077
- noTests: args.no_tests,
1078
- limit: Math.min(args.limit ?? MCP_DEFAULTS.list_entry_points, MCP_MAX_LIMIT),
1079
- offset: args.offset ?? 0,
1080
- });
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
+ }
1081
1166
  break;
1082
1167
  }
1083
1168
  case 'complexity': {
@@ -1095,17 +1180,6 @@ export async function startMCPServer(customDbPath, options = {}) {
1095
1180
  });
1096
1181
  break;
1097
1182
  }
1098
- case 'manifesto': {
1099
- const { manifestoData } = await import('./manifesto.js');
1100
- result = manifestoData(dbPath, {
1101
- file: args.file,
1102
- noTests: args.no_tests,
1103
- kind: args.kind,
1104
- limit: Math.min(args.limit ?? MCP_DEFAULTS.manifesto, MCP_MAX_LIMIT),
1105
- offset: args.offset ?? 0,
1106
- });
1107
- break;
1108
- }
1109
1183
  case 'communities': {
1110
1184
  const { communitiesData } = await import('./communities.js');
1111
1185
  result = communitiesData(dbPath, {
@@ -1130,13 +1204,21 @@ export async function startMCPServer(customDbPath, options = {}) {
1130
1204
  break;
1131
1205
  }
1132
1206
  case 'audit': {
1133
- const { auditData } = await import('./audit.js');
1134
- result = auditData(args.target, dbPath, {
1135
- depth: args.depth,
1136
- file: args.file,
1137
- kind: args.kind,
1138
- noTests: args.no_tests,
1139
- });
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
+ }
1140
1222
  break;
1141
1223
  }
1142
1224
  case 'batch_query': {
@@ -1150,18 +1232,36 @@ export async function startMCPServer(customDbPath, options = {}) {
1150
1232
  break;
1151
1233
  }
1152
1234
  case 'triage': {
1153
- const { triageData } = await import('./triage.js');
1154
- result = triageData(dbPath, {
1155
- sort: args.sort,
1156
- minScore: args.min_score,
1157
- role: args.role,
1158
- file: args.file,
1159
- kind: args.kind,
1160
- noTests: args.no_tests,
1161
- weights: args.weights,
1162
- limit: Math.min(args.limit ?? MCP_DEFAULTS.triage, MCP_MAX_LIMIT),
1163
- offset: args.offset ?? 0,
1164
- });
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
+ }
1165
1265
  break;
1166
1266
  }
1167
1267
  case 'branch_compare': {
@@ -1173,17 +1273,98 @@ export async function startMCPServer(customDbPath, options = {}) {
1173
1273
  result = args.format === 'mermaid' ? branchCompareMermaid(bcData) : bcData;
1174
1274
  break;
1175
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
+ }
1176
1318
  case 'check': {
1177
- const { checkData } = await import('./check.js');
1178
- result = checkData(dbPath, {
1179
- ref: args.ref,
1180
- staged: args.staged,
1181
- cycles: args.cycles,
1182
- blastRadius: args.blast_radius,
1183
- signatures: args.signatures,
1184
- boundaries: args.boundaries,
1185
- depth: args.depth,
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,
1186
1365
  noTests: args.no_tests,
1366
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.ast_query, MCP_MAX_LIMIT),
1367
+ offset: args.offset ?? 0,
1187
1368
  });
1188
1369
  break;
1189
1370
  }