@optave/codegraph 2.5.1 → 2.6.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
@@ -50,6 +50,7 @@ const BASE_TOOLS = [
50
50
  properties: {
51
51
  file: { type: 'string', description: 'File path (partial match supported)' },
52
52
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
53
+ ...PAGINATION_PROPS,
53
54
  },
54
55
  required: ['file'],
55
56
  },
@@ -62,6 +63,7 @@ const BASE_TOOLS = [
62
63
  properties: {
63
64
  file: { type: 'string', description: 'File path to analyze' },
64
65
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
66
+ ...PAGINATION_PROPS,
65
67
  },
66
68
  required: ['file'],
67
69
  },
@@ -103,6 +105,7 @@ const BASE_TOOLS = [
103
105
  description: 'Filter to a specific symbol kind',
104
106
  },
105
107
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
108
+ ...PAGINATION_PROPS,
106
109
  },
107
110
  required: ['name'],
108
111
  },
@@ -126,6 +129,7 @@ const BASE_TOOLS = [
126
129
  description: 'Filter to a specific symbol kind',
127
130
  },
128
131
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
132
+ ...PAGINATION_PROPS,
129
133
  },
130
134
  required: ['name'],
131
135
  },
@@ -190,6 +194,7 @@ const BASE_TOOLS = [
190
194
  description: 'Include test file source code',
191
195
  default: false,
192
196
  },
197
+ ...PAGINATION_PROPS,
193
198
  },
194
199
  required: ['name'],
195
200
  },
@@ -203,6 +208,7 @@ const BASE_TOOLS = [
203
208
  properties: {
204
209
  target: { type: 'string', description: 'File path or function name' },
205
210
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
211
+ ...PAGINATION_PROPS,
206
212
  },
207
213
  required: ['target'],
208
214
  },
@@ -241,19 +247,27 @@ const BASE_TOOLS = [
241
247
  enum: ['json', 'mermaid'],
242
248
  description: 'Output format (default: json)',
243
249
  },
250
+ ...PAGINATION_PROPS,
244
251
  },
245
252
  },
246
253
  },
247
254
  {
248
255
  name: 'semantic_search',
249
256
  description:
250
- 'Search code symbols by meaning using embeddings (requires prior `codegraph embed`)',
257
+ '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
258
  inputSchema: {
252
259
  type: 'object',
253
260
  properties: {
254
261
  query: { type: 'string', description: 'Natural language search query' },
255
262
  limit: { type: 'number', description: 'Max results to return', default: 15 },
256
263
  min_score: { type: 'number', description: 'Minimum similarity score (0-1)', default: 0.2 },
264
+ mode: {
265
+ type: 'string',
266
+ enum: ['hybrid', 'semantic', 'keyword'],
267
+ description:
268
+ 'Search mode: hybrid (BM25 + semantic, default), semantic (embeddings only), keyword (BM25 only)',
269
+ },
270
+ ...PAGINATION_PROPS,
257
271
  },
258
272
  required: ['query'],
259
273
  },
@@ -312,6 +326,7 @@ const BASE_TOOLS = [
312
326
  description: 'Return all files without limit',
313
327
  default: false,
314
328
  },
329
+ ...PAGINATION_PROPS,
315
330
  },
316
331
  },
317
332
  },
@@ -352,6 +367,7 @@ const BASE_TOOLS = [
352
367
  },
353
368
  limit: { type: 'number', description: 'Number of results to return', default: 10 },
354
369
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
370
+ offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
355
371
  },
356
372
  },
357
373
  },
@@ -373,6 +389,7 @@ const BASE_TOOLS = [
373
389
  default: 0.3,
374
390
  },
375
391
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
392
+ offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
376
393
  },
377
394
  },
378
395
  },
@@ -399,6 +416,7 @@ const BASE_TOOLS = [
399
416
  description: 'Filter to a specific symbol kind',
400
417
  },
401
418
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
419
+ ...PAGINATION_PROPS,
402
420
  },
403
421
  required: ['name'],
404
422
  },
@@ -446,6 +464,7 @@ const BASE_TOOLS = [
446
464
  type: 'string',
447
465
  description: 'Filter by symbol kind (function, method, class, etc.)',
448
466
  },
467
+ offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
449
468
  },
450
469
  },
451
470
  },
@@ -462,6 +481,7 @@ const BASE_TOOLS = [
462
481
  type: 'string',
463
482
  description: 'Filter by symbol kind (function, method, class, etc.)',
464
483
  },
484
+ ...PAGINATION_PROPS,
465
485
  },
466
486
  },
467
487
  },
@@ -488,6 +508,172 @@ const BASE_TOOLS = [
488
508
  default: false,
489
509
  },
490
510
  no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
511
+ ...PAGINATION_PROPS,
512
+ },
513
+ },
514
+ },
515
+ {
516
+ name: 'code_owners',
517
+ description:
518
+ 'Show CODEOWNERS mapping for files and functions. Shows ownership coverage, per-owner breakdown, and cross-owner boundary edges.',
519
+ inputSchema: {
520
+ type: 'object',
521
+ properties: {
522
+ file: { type: 'string', description: 'Scope to a specific file (partial match)' },
523
+ owner: { type: 'string', description: 'Filter to a specific owner (e.g. @team-name)' },
524
+ boundary: {
525
+ type: 'boolean',
526
+ description: 'Show cross-owner boundary edges',
527
+ default: false,
528
+ },
529
+ kind: {
530
+ type: 'string',
531
+ description: 'Filter by symbol kind (function, method, class, etc.)',
532
+ },
533
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
534
+ },
535
+ },
536
+ },
537
+ {
538
+ name: 'audit',
539
+ description:
540
+ '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.',
541
+ inputSchema: {
542
+ type: 'object',
543
+ properties: {
544
+ target: { type: 'string', description: 'File path or function name' },
545
+ depth: { type: 'number', description: 'Impact analysis depth (default: 3)', default: 3 },
546
+ file: { type: 'string', description: 'Scope to file (partial match)' },
547
+ kind: {
548
+ type: 'string',
549
+ description: 'Filter by symbol kind (function, method, class, etc.)',
550
+ },
551
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
552
+ },
553
+ required: ['target'],
554
+ },
555
+ },
556
+ {
557
+ name: 'batch_query',
558
+ description:
559
+ 'Run a query command against multiple targets in one call. Returns all results in a single JSON payload — ideal for multi-agent dispatch.',
560
+ inputSchema: {
561
+ type: 'object',
562
+ properties: {
563
+ command: {
564
+ type: 'string',
565
+ enum: [
566
+ 'fn-impact',
567
+ 'context',
568
+ 'explain',
569
+ 'where',
570
+ 'query',
571
+ 'fn',
572
+ 'impact',
573
+ 'deps',
574
+ 'flow',
575
+ 'complexity',
576
+ ],
577
+ description: 'The query command to run for each target',
578
+ },
579
+ targets: {
580
+ type: 'array',
581
+ items: { type: 'string' },
582
+ description: 'List of target names (symbol names or file paths depending on command)',
583
+ },
584
+ depth: {
585
+ type: 'number',
586
+ description: 'Traversal depth (for fn-impact, context, fn, flow)',
587
+ },
588
+ file: {
589
+ type: 'string',
590
+ description: 'Scope to file (partial match)',
591
+ },
592
+ kind: {
593
+ type: 'string',
594
+ enum: ALL_SYMBOL_KINDS,
595
+ description: 'Filter symbol kind',
596
+ },
597
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
598
+ },
599
+ required: ['command', 'targets'],
600
+ },
601
+ },
602
+ {
603
+ name: 'triage',
604
+ description:
605
+ '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.',
606
+ inputSchema: {
607
+ type: 'object',
608
+ properties: {
609
+ sort: {
610
+ type: 'string',
611
+ enum: ['risk', 'complexity', 'churn', 'fan-in', 'mi'],
612
+ description: 'Sort metric (default: risk)',
613
+ },
614
+ min_score: {
615
+ type: 'number',
616
+ description: 'Only return symbols with risk score >= this threshold (0-1)',
617
+ },
618
+ role: {
619
+ type: 'string',
620
+ enum: VALID_ROLES,
621
+ description: 'Filter by role classification',
622
+ },
623
+ file: { type: 'string', description: 'Scope to file (partial match)' },
624
+ kind: {
625
+ type: 'string',
626
+ enum: ['function', 'method', 'class'],
627
+ description: 'Filter by symbol kind',
628
+ },
629
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
630
+ weights: {
631
+ type: 'object',
632
+ description:
633
+ 'Custom scoring weights (e.g. {"fanIn":1,"complexity":0,"churn":0,"role":0,"mi":0})',
634
+ },
635
+ ...PAGINATION_PROPS,
636
+ },
637
+ },
638
+ },
639
+ {
640
+ name: 'branch_compare',
641
+ description:
642
+ 'Compare code structure between two git refs (branches, tags, commits). Shows added/removed/changed symbols and transitive caller impact using temporary git worktrees.',
643
+ inputSchema: {
644
+ type: 'object',
645
+ properties: {
646
+ base: { type: 'string', description: 'Base git ref (branch, tag, or commit SHA)' },
647
+ target: { type: 'string', description: 'Target git ref to compare against base' },
648
+ depth: { type: 'number', description: 'Max transitive caller depth', default: 3 },
649
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
650
+ format: {
651
+ type: 'string',
652
+ enum: ['json', 'mermaid'],
653
+ description: 'Output format (default: json)',
654
+ },
655
+ },
656
+ required: ['base', 'target'],
657
+ },
658
+ },
659
+ {
660
+ name: 'check',
661
+ 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.',
663
+ inputSchema: {
664
+ type: 'object',
665
+ properties: {
666
+ ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
667
+ staged: { type: 'boolean', description: 'Analyze staged changes instead of unstaged' },
668
+ cycles: { type: 'boolean', description: 'Enable cycles predicate (default: true)' },
669
+ blast_radius: {
670
+ type: 'number',
671
+ description: 'Max transitive callers threshold (null = disabled)',
672
+ },
673
+ signatures: { type: 'boolean', description: 'Enable signatures predicate (default: true)' },
674
+ boundaries: { type: 'boolean', description: 'Enable boundaries predicate (default: true)' },
675
+ depth: { type: 'number', description: 'Max BFS depth for blast radius (default: 3)' },
676
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
491
677
  },
492
678
  },
493
679
  },
@@ -623,10 +809,18 @@ export async function startMCPServer(customDbPath, options = {}) {
623
809
  });
624
810
  break;
625
811
  case 'file_deps':
626
- result = fileDepsData(args.file, dbPath, { noTests: args.no_tests });
812
+ result = fileDepsData(args.file, dbPath, {
813
+ noTests: args.no_tests,
814
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.file_deps, MCP_MAX_LIMIT),
815
+ offset: args.offset ?? 0,
816
+ });
627
817
  break;
628
818
  case 'impact_analysis':
629
- result = impactAnalysisData(args.file, dbPath, { noTests: args.no_tests });
819
+ result = impactAnalysisData(args.file, dbPath, {
820
+ noTests: args.no_tests,
821
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.impact_analysis, MCP_MAX_LIMIT),
822
+ offset: args.offset ?? 0,
823
+ });
630
824
  break;
631
825
  case 'find_cycles': {
632
826
  const db = new Database(findDbPath(dbPath), { readonly: true });
@@ -644,6 +838,8 @@ export async function startMCPServer(customDbPath, options = {}) {
644
838
  file: args.file,
645
839
  kind: args.kind,
646
840
  noTests: args.no_tests,
841
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_deps, MCP_MAX_LIMIT),
842
+ offset: args.offset ?? 0,
647
843
  });
648
844
  break;
649
845
  case 'fn_impact':
@@ -652,6 +848,8 @@ export async function startMCPServer(customDbPath, options = {}) {
652
848
  file: args.file,
653
849
  kind: args.kind,
654
850
  noTests: args.no_tests,
851
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_impact, MCP_MAX_LIMIT),
852
+ offset: args.offset ?? 0,
655
853
  });
656
854
  break;
657
855
  case 'symbol_path':
@@ -673,10 +871,16 @@ export async function startMCPServer(customDbPath, options = {}) {
673
871
  noSource: args.no_source,
674
872
  noTests: args.no_tests,
675
873
  includeTests: args.include_tests,
874
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.context, MCP_MAX_LIMIT),
875
+ offset: args.offset ?? 0,
676
876
  });
677
877
  break;
678
878
  case 'explain':
679
- result = explainData(args.target, dbPath, { noTests: args.no_tests });
879
+ result = explainData(args.target, dbPath, {
880
+ noTests: args.no_tests,
881
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.explain, MCP_MAX_LIMIT),
882
+ offset: args.offset ?? 0,
883
+ });
680
884
  break;
681
885
  case 'where':
682
886
  result = whereData(args.target, dbPath, {
@@ -700,22 +904,65 @@ export async function startMCPServer(customDbPath, options = {}) {
700
904
  ref: args.ref,
701
905
  depth: args.depth,
702
906
  noTests: args.no_tests,
907
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.diff_impact, MCP_MAX_LIMIT),
908
+ offset: args.offset ?? 0,
703
909
  });
704
910
  }
705
911
  break;
706
912
  case 'semantic_search': {
707
- const { searchData } = await import('./embedder.js');
708
- result = await searchData(args.query, dbPath, {
709
- limit: args.limit,
913
+ const mode = args.mode || 'hybrid';
914
+ const searchOpts = {
915
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.semantic_search, MCP_MAX_LIMIT),
916
+ offset: args.offset ?? 0,
710
917
  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
- };
918
+ };
919
+
920
+ if (mode === 'keyword') {
921
+ const { ftsSearchData } = await import('./embedder.js');
922
+ result = ftsSearchData(args.query, dbPath, searchOpts);
923
+ if (result === null) {
924
+ return {
925
+ content: [
926
+ {
927
+ type: 'text',
928
+ text: 'No FTS5 index found. Run `codegraph embed` to build the keyword index.',
929
+ },
930
+ ],
931
+ isError: true,
932
+ };
933
+ }
934
+ } else if (mode === 'semantic') {
935
+ const { searchData } = await import('./embedder.js');
936
+ result = await searchData(args.query, dbPath, searchOpts);
937
+ if (result === null) {
938
+ return {
939
+ content: [
940
+ {
941
+ type: 'text',
942
+ text: 'Semantic search unavailable. Run `codegraph embed` first.',
943
+ },
944
+ ],
945
+ isError: true,
946
+ };
947
+ }
948
+ } else {
949
+ // hybrid (default) — falls back to semantic if no FTS5
950
+ const { hybridSearchData, searchData } = await import('./embedder.js');
951
+ result = await hybridSearchData(args.query, dbPath, searchOpts);
952
+ if (result === null) {
953
+ result = await searchData(args.query, dbPath, searchOpts);
954
+ if (result === null) {
955
+ return {
956
+ content: [
957
+ {
958
+ type: 'text',
959
+ text: 'Semantic search unavailable. Run `codegraph embed` first.',
960
+ },
961
+ ],
962
+ isError: true,
963
+ };
964
+ }
965
+ }
719
966
  }
720
967
  break;
721
968
  }
@@ -779,6 +1026,8 @@ export async function startMCPServer(customDbPath, options = {}) {
779
1026
  depth: args.depth,
780
1027
  sort: args.sort,
781
1028
  full: args.full,
1029
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.structure, MCP_MAX_LIMIT),
1030
+ offset: args.offset ?? 0,
782
1031
  });
783
1032
  break;
784
1033
  }
@@ -787,7 +1036,8 @@ export async function startMCPServer(customDbPath, options = {}) {
787
1036
  result = hotspotsData(dbPath, {
788
1037
  metric: args.metric,
789
1038
  level: args.level,
790
- limit: args.limit,
1039
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.hotspots, MCP_MAX_LIMIT),
1040
+ offset: args.offset ?? 0,
791
1041
  noTests: args.no_tests,
792
1042
  });
793
1043
  break;
@@ -796,12 +1046,14 @@ export async function startMCPServer(customDbPath, options = {}) {
796
1046
  const { coChangeData, coChangeTopData } = await import('./cochange.js');
797
1047
  result = args.file
798
1048
  ? coChangeData(args.file, dbPath, {
799
- limit: args.limit,
1049
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.co_changes, MCP_MAX_LIMIT),
1050
+ offset: args.offset ?? 0,
800
1051
  minJaccard: args.min_jaccard,
801
1052
  noTests: args.no_tests,
802
1053
  })
803
1054
  : coChangeTopData(dbPath, {
804
- limit: args.limit,
1055
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.co_changes, MCP_MAX_LIMIT),
1056
+ offset: args.offset ?? 0,
805
1057
  minJaccard: args.min_jaccard,
806
1058
  noTests: args.no_tests,
807
1059
  });
@@ -814,6 +1066,8 @@ export async function startMCPServer(customDbPath, options = {}) {
814
1066
  file: args.file,
815
1067
  kind: args.kind,
816
1068
  noTests: args.no_tests,
1069
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1070
+ offset: args.offset ?? 0,
817
1071
  });
818
1072
  break;
819
1073
  }
@@ -831,7 +1085,8 @@ export async function startMCPServer(customDbPath, options = {}) {
831
1085
  result = complexityData(dbPath, {
832
1086
  target: args.name,
833
1087
  file: args.file,
834
- limit: args.limit,
1088
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.complexity, MCP_MAX_LIMIT),
1089
+ offset: args.offset ?? 0,
835
1090
  sort: args.sort,
836
1091
  aboveThreshold: args.above_threshold,
837
1092
  health: args.health,
@@ -846,6 +1101,8 @@ export async function startMCPServer(customDbPath, options = {}) {
846
1101
  file: args.file,
847
1102
  noTests: args.no_tests,
848
1103
  kind: args.kind,
1104
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.manifesto, MCP_MAX_LIMIT),
1105
+ offset: args.offset ?? 0,
849
1106
  });
850
1107
  break;
851
1108
  }
@@ -856,6 +1113,77 @@ export async function startMCPServer(customDbPath, options = {}) {
856
1113
  resolution: args.resolution,
857
1114
  drift: args.drift,
858
1115
  noTests: args.no_tests,
1116
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.communities, MCP_MAX_LIMIT),
1117
+ offset: args.offset ?? 0,
1118
+ });
1119
+ break;
1120
+ }
1121
+ case 'code_owners': {
1122
+ const { ownersData } = await import('./owners.js');
1123
+ result = ownersData(dbPath, {
1124
+ file: args.file,
1125
+ owner: args.owner,
1126
+ boundary: args.boundary,
1127
+ kind: args.kind,
1128
+ noTests: args.no_tests,
1129
+ });
1130
+ break;
1131
+ }
1132
+ 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
+ });
1140
+ break;
1141
+ }
1142
+ case 'batch_query': {
1143
+ const { batchData } = await import('./batch.js');
1144
+ result = batchData(args.command, args.targets, dbPath, {
1145
+ depth: args.depth,
1146
+ file: args.file,
1147
+ kind: args.kind,
1148
+ noTests: args.no_tests,
1149
+ });
1150
+ break;
1151
+ }
1152
+ 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
+ });
1165
+ break;
1166
+ }
1167
+ case 'branch_compare': {
1168
+ const { branchCompareData, branchCompareMermaid } = await import('./branch-compare.js');
1169
+ const bcData = await branchCompareData(args.base, args.target, {
1170
+ depth: args.depth,
1171
+ noTests: args.no_tests,
1172
+ });
1173
+ result = args.format === 'mermaid' ? branchCompareMermaid(bcData) : bcData;
1174
+ break;
1175
+ }
1176
+ 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,
1186
+ noTests: args.no_tests,
859
1187
  });
860
1188
  break;
861
1189
  }