@optave/codegraph 3.1.2 → 3.1.4

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.
Files changed (194) hide show
  1. package/README.md +19 -21
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1472
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1514
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/complexity.js +1 -1
  76. package/src/cycles.js +30 -85
  77. package/src/dataflow.js +1 -2
  78. package/src/db/connection.js +4 -4
  79. package/src/db/migrations.js +41 -0
  80. package/src/db/query-builder.js +6 -5
  81. package/src/db/repository/base.js +201 -0
  82. package/src/db/repository/cached-stmt.js +19 -0
  83. package/src/db/repository/cfg.js +27 -38
  84. package/src/db/repository/cochange.js +16 -3
  85. package/src/db/repository/complexity.js +11 -6
  86. package/src/db/repository/dataflow.js +6 -1
  87. package/src/db/repository/edges.js +120 -98
  88. package/src/db/repository/embeddings.js +14 -3
  89. package/src/db/repository/graph-read.js +32 -9
  90. package/src/db/repository/in-memory-repository.js +584 -0
  91. package/src/db/repository/index.js +6 -1
  92. package/src/db/repository/nodes.js +110 -40
  93. package/src/db/repository/sqlite-repository.js +219 -0
  94. package/src/db.js +5 -0
  95. package/src/embeddings/generator.js +163 -0
  96. package/src/embeddings/index.js +13 -0
  97. package/src/embeddings/models.js +218 -0
  98. package/src/embeddings/search/cli-formatter.js +151 -0
  99. package/src/embeddings/search/filters.js +46 -0
  100. package/src/embeddings/search/hybrid.js +121 -0
  101. package/src/embeddings/search/keyword.js +68 -0
  102. package/src/embeddings/search/prepare.js +66 -0
  103. package/src/embeddings/search/semantic.js +145 -0
  104. package/src/embeddings/stores/fts5.js +27 -0
  105. package/src/embeddings/stores/sqlite-blob.js +24 -0
  106. package/src/embeddings/strategies/source.js +14 -0
  107. package/src/embeddings/strategies/structured.js +43 -0
  108. package/src/embeddings/strategies/text-utils.js +43 -0
  109. package/src/errors.js +78 -0
  110. package/src/export.js +217 -520
  111. package/src/extractors/csharp.js +10 -2
  112. package/src/extractors/go.js +3 -1
  113. package/src/extractors/helpers.js +71 -0
  114. package/src/extractors/java.js +9 -2
  115. package/src/extractors/javascript.js +38 -1
  116. package/src/extractors/php.js +3 -1
  117. package/src/extractors/python.js +14 -3
  118. package/src/extractors/rust.js +3 -1
  119. package/src/graph/algorithms/bfs.js +49 -0
  120. package/src/graph/algorithms/centrality.js +16 -0
  121. package/src/graph/algorithms/index.js +5 -0
  122. package/src/graph/algorithms/louvain.js +26 -0
  123. package/src/graph/algorithms/shortest-path.js +41 -0
  124. package/src/graph/algorithms/tarjan.js +49 -0
  125. package/src/graph/builders/dependency.js +91 -0
  126. package/src/graph/builders/index.js +3 -0
  127. package/src/graph/builders/structure.js +40 -0
  128. package/src/graph/builders/temporal.js +33 -0
  129. package/src/graph/classifiers/index.js +2 -0
  130. package/src/graph/classifiers/risk.js +85 -0
  131. package/src/graph/classifiers/roles.js +64 -0
  132. package/src/graph/index.js +13 -0
  133. package/src/graph/model.js +230 -0
  134. package/src/index.js +33 -204
  135. package/src/infrastructure/result-formatter.js +2 -21
  136. package/src/mcp/index.js +2 -0
  137. package/src/mcp/middleware.js +26 -0
  138. package/src/mcp/server.js +128 -0
  139. package/src/mcp/tool-registry.js +801 -0
  140. package/src/mcp/tools/ast-query.js +14 -0
  141. package/src/mcp/tools/audit.js +21 -0
  142. package/src/mcp/tools/batch-query.js +11 -0
  143. package/src/mcp/tools/branch-compare.js +10 -0
  144. package/src/mcp/tools/cfg.js +21 -0
  145. package/src/mcp/tools/check.js +43 -0
  146. package/src/mcp/tools/co-changes.js +20 -0
  147. package/src/mcp/tools/code-owners.js +12 -0
  148. package/src/mcp/tools/communities.js +15 -0
  149. package/src/mcp/tools/complexity.js +18 -0
  150. package/src/mcp/tools/context.js +17 -0
  151. package/src/mcp/tools/dataflow.js +26 -0
  152. package/src/mcp/tools/diff-impact.js +24 -0
  153. package/src/mcp/tools/execution-flow.js +26 -0
  154. package/src/mcp/tools/export-graph.js +57 -0
  155. package/src/mcp/tools/file-deps.js +12 -0
  156. package/src/mcp/tools/file-exports.js +13 -0
  157. package/src/mcp/tools/find-cycles.js +15 -0
  158. package/src/mcp/tools/fn-impact.js +15 -0
  159. package/src/mcp/tools/impact-analysis.js +12 -0
  160. package/src/mcp/tools/index.js +71 -0
  161. package/src/mcp/tools/list-functions.js +14 -0
  162. package/src/mcp/tools/list-repos.js +11 -0
  163. package/src/mcp/tools/module-map.js +6 -0
  164. package/src/mcp/tools/node-roles.js +14 -0
  165. package/src/mcp/tools/path.js +12 -0
  166. package/src/mcp/tools/query.js +30 -0
  167. package/src/mcp/tools/semantic-search.js +65 -0
  168. package/src/mcp/tools/sequence.js +17 -0
  169. package/src/mcp/tools/structure.js +15 -0
  170. package/src/mcp/tools/symbol-children.js +14 -0
  171. package/src/mcp/tools/triage.js +35 -0
  172. package/src/mcp/tools/where.js +13 -0
  173. package/src/mcp.js +2 -1470
  174. package/src/native.js +34 -10
  175. package/src/parser.js +53 -2
  176. package/src/presentation/colors.js +44 -0
  177. package/src/presentation/export.js +444 -0
  178. package/src/presentation/result-formatter.js +21 -0
  179. package/src/presentation/sequence-renderer.js +43 -0
  180. package/src/presentation/table.js +47 -0
  181. package/src/presentation/viewer.js +634 -0
  182. package/src/queries.js +35 -2276
  183. package/src/resolve.js +1 -1
  184. package/src/sequence.js +2 -38
  185. package/src/shared/file-utils.js +153 -0
  186. package/src/shared/generators.js +125 -0
  187. package/src/shared/hierarchy.js +27 -0
  188. package/src/shared/normalize.js +59 -0
  189. package/src/snapshot.js +6 -5
  190. package/src/structure.js +15 -40
  191. package/src/triage.js +20 -72
  192. package/src/viewer.js +35 -656
  193. package/src/watcher.js +8 -148
  194. package/src/embedder.js +0 -1097
package/src/mcp.js CHANGED
@@ -1,1470 +1,2 @@
1
- /**
2
- * MCP (Model Context Protocol) server for codegraph.
3
- * Exposes codegraph queries as tools that AI coding assistants can call.
4
- *
5
- * Requires: npm install @modelcontextprotocol/sdk
6
- */
7
-
8
- import { createRequire } from 'node:module';
9
- import { AST_NODE_KINDS } from './ast.js';
10
- import { findCycles } from './cycles.js';
11
- import { findDbPath } from './db.js';
12
- import { MCP_DEFAULTS, MCP_MAX_LIMIT } from './paginate.js';
13
- import { diffImpactMermaid, EVERY_EDGE_KIND, EVERY_SYMBOL_KIND, VALID_ROLES } from './queries.js';
14
-
15
- const REPO_PROP = {
16
- repo: {
17
- type: 'string',
18
- description: 'Repository name from the registry (omit for local project)',
19
- },
20
- };
21
-
22
- const PAGINATION_PROPS = {
23
- limit: { type: 'number', description: 'Max results to return (pagination)' },
24
- offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
25
- };
26
-
27
- const BASE_TOOLS = [
28
- {
29
- name: 'query',
30
- description:
31
- 'Query the call graph: find callers/callees with transitive chain, or find shortest path between two symbols',
32
- inputSchema: {
33
- type: 'object',
34
- properties: {
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
- },
41
- depth: {
42
- type: 'number',
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,
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' },
67
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
68
- ...PAGINATION_PROPS,
69
- },
70
- required: ['name'],
71
- },
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
- },
94
- {
95
- name: 'file_deps',
96
- description: 'Show what a file imports and what imports it',
97
- inputSchema: {
98
- type: 'object',
99
- properties: {
100
- file: { type: 'string', description: 'File path (partial match supported)' },
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
- unused: {
117
- type: 'boolean',
118
- description: 'Show only exports with zero consumers',
119
- default: false,
120
- },
121
- ...PAGINATION_PROPS,
122
- },
123
- required: ['file'],
124
- },
125
- },
126
- {
127
- name: 'impact_analysis',
128
- description: 'Show files affected by changes to a given file (transitive)',
129
- inputSchema: {
130
- type: 'object',
131
- properties: {
132
- file: { type: 'string', description: 'File path to analyze' },
133
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
134
- ...PAGINATION_PROPS,
135
- },
136
- required: ['file'],
137
- },
138
- },
139
- {
140
- name: 'find_cycles',
141
- description: 'Detect circular dependencies in the codebase',
142
- inputSchema: {
143
- type: 'object',
144
- properties: {},
145
- },
146
- },
147
- {
148
- name: 'module_map',
149
- description: 'Get high-level overview of most-connected files',
150
- inputSchema: {
151
- type: 'object',
152
- properties: {
153
- limit: { type: 'number', description: 'Number of top files to show', default: 20 },
154
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
155
- },
156
- },
157
- },
158
- {
159
- name: 'fn_impact',
160
- description:
161
- 'Show function-level blast radius: all functions transitively affected by changes to a function',
162
- inputSchema: {
163
- type: 'object',
164
- properties: {
165
- name: { type: 'string', description: 'Function/method/class name (partial match)' },
166
- depth: { type: 'number', description: 'Max traversal depth', default: 5 },
167
- file: {
168
- type: 'string',
169
- description: 'Scope search to functions in this file (partial match)',
170
- },
171
- kind: {
172
- type: 'string',
173
- enum: EVERY_SYMBOL_KIND,
174
- description: 'Filter to a specific symbol kind',
175
- },
176
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
177
- ...PAGINATION_PROPS,
178
- },
179
- required: ['name'],
180
- },
181
- },
182
- {
183
- name: 'context',
184
- description:
185
- 'Full context for a function: source code, dependencies with summaries, callers, signature, and related tests — everything needed to understand or modify a function in one call',
186
- inputSchema: {
187
- type: 'object',
188
- properties: {
189
- name: { type: 'string', description: 'Function/method/class name (partial match)' },
190
- depth: {
191
- type: 'number',
192
- description: 'Include callee source up to N levels deep (0=no source, 1=direct)',
193
- default: 0,
194
- },
195
- file: {
196
- type: 'string',
197
- description: 'Scope search to functions in this file (partial match)',
198
- },
199
- kind: {
200
- type: 'string',
201
- enum: EVERY_SYMBOL_KIND,
202
- description: 'Filter to a specific symbol kind',
203
- },
204
- no_source: {
205
- type: 'boolean',
206
- description: 'Skip source extraction (metadata only)',
207
- default: false,
208
- },
209
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
210
- include_tests: {
211
- type: 'boolean',
212
- description: 'Include test file source code',
213
- default: false,
214
- },
215
- ...PAGINATION_PROPS,
216
- },
217
- required: ['name'],
218
- },
219
- },
220
- {
221
- name: 'symbol_children',
222
- description:
223
- 'List sub-declaration children of a symbol: parameters, properties, constants. Answers "what fields does this class have?" without reading source.',
224
- inputSchema: {
225
- type: 'object',
226
- properties: {
227
- name: { type: 'string', description: 'Function/method/class name (partial match)' },
228
- file: { type: 'string', description: 'Scope to file (partial match)' },
229
- kind: { type: 'string', enum: EVERY_SYMBOL_KIND, description: 'Filter by symbol kind' },
230
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
231
- ...PAGINATION_PROPS,
232
- },
233
- required: ['name'],
234
- },
235
- },
236
- {
237
- name: 'where',
238
- description:
239
- 'Find where a symbol is defined and used, or list symbols/imports/exports for a file. Minimal, fast lookup.',
240
- inputSchema: {
241
- type: 'object',
242
- properties: {
243
- target: { type: 'string', description: 'Symbol name or file path' },
244
- file_mode: {
245
- type: 'boolean',
246
- description: 'Treat target as file path (list symbols/imports/exports)',
247
- default: false,
248
- },
249
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
250
- ...PAGINATION_PROPS,
251
- },
252
- required: ['target'],
253
- },
254
- },
255
- {
256
- name: 'diff_impact',
257
- description: 'Analyze git diff to find which functions changed and their transitive callers',
258
- inputSchema: {
259
- type: 'object',
260
- properties: {
261
- staged: { type: 'boolean', description: 'Analyze staged changes only', default: false },
262
- ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
263
- depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
264
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
265
- format: {
266
- type: 'string',
267
- enum: ['json', 'mermaid'],
268
- description: 'Output format (default: json)',
269
- },
270
- ...PAGINATION_PROPS,
271
- },
272
- },
273
- },
274
- {
275
- name: 'semantic_search',
276
- description:
277
- '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.',
278
- inputSchema: {
279
- type: 'object',
280
- properties: {
281
- query: { type: 'string', description: 'Natural language search query' },
282
- limit: { type: 'number', description: 'Max results to return', default: 15 },
283
- min_score: { type: 'number', description: 'Minimum similarity score (0-1)', default: 0.2 },
284
- mode: {
285
- type: 'string',
286
- enum: ['hybrid', 'semantic', 'keyword'],
287
- description:
288
- 'Search mode: hybrid (BM25 + semantic, default), semantic (embeddings only), keyword (BM25 only)',
289
- },
290
- ...PAGINATION_PROPS,
291
- },
292
- required: ['query'],
293
- },
294
- },
295
- {
296
- name: 'export_graph',
297
- description:
298
- 'Export the dependency graph in DOT, Mermaid, JSON, GraphML, GraphSON, or Neo4j CSV format',
299
- inputSchema: {
300
- type: 'object',
301
- properties: {
302
- format: {
303
- type: 'string',
304
- enum: ['dot', 'mermaid', 'json', 'graphml', 'graphson', 'neo4j'],
305
- description: 'Export format',
306
- },
307
- file_level: {
308
- type: 'boolean',
309
- description: 'File-level graph (true) or function-level (false)',
310
- default: true,
311
- },
312
- ...PAGINATION_PROPS,
313
- },
314
- required: ['format'],
315
- },
316
- },
317
- {
318
- name: 'list_functions',
319
- description:
320
- 'List functions, methods, classes, structs, enums, traits, records, and modules in the codebase, optionally filtered by file or name pattern',
321
- inputSchema: {
322
- type: 'object',
323
- properties: {
324
- file: { type: 'string', description: 'Filter by file path (partial match)' },
325
- pattern: { type: 'string', description: 'Filter by function name (partial match)' },
326
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
327
- ...PAGINATION_PROPS,
328
- },
329
- },
330
- },
331
- {
332
- name: 'structure',
333
- description:
334
- 'Show project structure with directory hierarchy, cohesion scores, and per-file metrics. Per-file details are capped at 25 files by default; use full=true to show all.',
335
- inputSchema: {
336
- type: 'object',
337
- properties: {
338
- directory: { type: 'string', description: 'Filter to a specific directory path' },
339
- depth: { type: 'number', description: 'Max directory depth to show' },
340
- sort: {
341
- type: 'string',
342
- enum: ['cohesion', 'fan-in', 'fan-out', 'density', 'files'],
343
- description: 'Sort directories by metric',
344
- },
345
- full: {
346
- type: 'boolean',
347
- description: 'Return all files without limit',
348
- default: false,
349
- },
350
- ...PAGINATION_PROPS,
351
- },
352
- },
353
- },
354
- {
355
- name: 'node_roles',
356
- description:
357
- 'Show node role classification (entry, core, utility, adapter, dead, leaf) based on connectivity patterns',
358
- inputSchema: {
359
- type: 'object',
360
- properties: {
361
- role: {
362
- type: 'string',
363
- enum: VALID_ROLES,
364
- description: 'Filter to a specific role',
365
- },
366
- file: { type: 'string', description: 'Scope to a specific file (partial match)' },
367
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
368
- ...PAGINATION_PROPS,
369
- },
370
- },
371
- },
372
- {
373
- name: 'co_changes',
374
- description:
375
- 'Find files that historically change together based on git commit history. Requires prior `codegraph co-change --analyze`.',
376
- inputSchema: {
377
- type: 'object',
378
- properties: {
379
- file: {
380
- type: 'string',
381
- description: 'File path (partial match). Omit for top global pairs.',
382
- },
383
- limit: { type: 'number', description: 'Max results', default: 20 },
384
- min_jaccard: {
385
- type: 'number',
386
- description: 'Minimum Jaccard similarity (0-1)',
387
- default: 0.3,
388
- },
389
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
390
- offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
391
- },
392
- },
393
- },
394
- {
395
- name: 'execution_flow',
396
- description:
397
- 'Trace execution flow forward from an entry point through callees to leaves, or list all entry points with list=true',
398
- inputSchema: {
399
- type: 'object',
400
- properties: {
401
- name: {
402
- type: 'string',
403
- description:
404
- 'Entry point or function name (required unless list=true). Supports prefix-stripped matching.',
405
- },
406
- list: {
407
- type: 'boolean',
408
- description: 'List all entry points grouped by type',
409
- default: false,
410
- },
411
- depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
412
- file: {
413
- type: 'string',
414
- description: 'Scope search to functions in this file (partial match)',
415
- },
416
- kind: {
417
- type: 'string',
418
- enum: EVERY_SYMBOL_KIND,
419
- description: 'Filter to a specific symbol kind',
420
- },
421
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
422
- ...PAGINATION_PROPS,
423
- },
424
- },
425
- },
426
- {
427
- name: 'sequence',
428
- description:
429
- 'Generate a Mermaid sequence diagram from call graph edges. Participants are files, messages are function calls between them.',
430
- inputSchema: {
431
- type: 'object',
432
- properties: {
433
- name: {
434
- type: 'string',
435
- description: 'Entry point or function name to trace from (partial match)',
436
- },
437
- depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
438
- format: {
439
- type: 'string',
440
- enum: ['mermaid', 'json'],
441
- description: 'Output format (default: mermaid)',
442
- },
443
- dataflow: {
444
- type: 'boolean',
445
- description: 'Annotate with parameter names and return arrows',
446
- default: false,
447
- },
448
- file: {
449
- type: 'string',
450
- description: 'Scope search to functions in this file (partial match)',
451
- },
452
- kind: {
453
- type: 'string',
454
- enum: EVERY_SYMBOL_KIND,
455
- description: 'Filter to a specific symbol kind',
456
- },
457
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
458
- ...PAGINATION_PROPS,
459
- },
460
- required: ['name'],
461
- },
462
- },
463
- {
464
- name: 'complexity',
465
- description:
466
- 'Show per-function complexity metrics (cognitive, cyclomatic, nesting, Halstead, Maintainability Index). Sorted by most complex first.',
467
- inputSchema: {
468
- type: 'object',
469
- properties: {
470
- name: { type: 'string', description: 'Function name filter (partial match)' },
471
- file: { type: 'string', description: 'Scope to file (partial match)' },
472
- limit: { type: 'number', description: 'Max results', default: 20 },
473
- sort: {
474
- type: 'string',
475
- enum: ['cognitive', 'cyclomatic', 'nesting', 'mi', 'volume', 'effort', 'bugs', 'loc'],
476
- description: 'Sort metric',
477
- default: 'cognitive',
478
- },
479
- above_threshold: {
480
- type: 'boolean',
481
- description: 'Only functions exceeding warn thresholds',
482
- default: false,
483
- },
484
- health: {
485
- type: 'boolean',
486
- description: 'Include Halstead and Maintainability Index metrics',
487
- default: false,
488
- },
489
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
490
- kind: {
491
- type: 'string',
492
- description: 'Filter by symbol kind (function, method, class, etc.)',
493
- },
494
- offset: { type: 'number', description: 'Skip this many results (pagination, default: 0)' },
495
- },
496
- },
497
- },
498
- {
499
- name: 'communities',
500
- description:
501
- 'Detect natural module boundaries using Louvain community detection. Compares discovered communities against directory structure and surfaces architectural drift.',
502
- inputSchema: {
503
- type: 'object',
504
- properties: {
505
- functions: {
506
- type: 'boolean',
507
- description: 'Function-level instead of file-level',
508
- default: false,
509
- },
510
- resolution: {
511
- type: 'number',
512
- description: 'Louvain resolution parameter (higher = more communities)',
513
- default: 1.0,
514
- },
515
- drift: {
516
- type: 'boolean',
517
- description: 'Show only drift analysis (omit community member lists)',
518
- default: false,
519
- },
520
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
521
- ...PAGINATION_PROPS,
522
- },
523
- },
524
- },
525
- {
526
- name: 'code_owners',
527
- description:
528
- 'Show CODEOWNERS mapping for files and functions. Shows ownership coverage, per-owner breakdown, and cross-owner boundary edges.',
529
- inputSchema: {
530
- type: 'object',
531
- properties: {
532
- file: { type: 'string', description: 'Scope to a specific file (partial match)' },
533
- owner: { type: 'string', description: 'Filter to a specific owner (e.g. @team-name)' },
534
- boundary: {
535
- type: 'boolean',
536
- description: 'Show cross-owner boundary edges',
537
- default: false,
538
- },
539
- kind: {
540
- type: 'string',
541
- description: 'Filter by symbol kind (function, method, class, etc.)',
542
- },
543
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
544
- },
545
- },
546
- },
547
- {
548
- name: 'audit',
549
- description:
550
- '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.',
551
- inputSchema: {
552
- type: 'object',
553
- properties: {
554
- target: { type: 'string', description: 'File path or function name' },
555
- quick: {
556
- type: 'boolean',
557
- description: 'Structural summary only (skip impact + health)',
558
- default: false,
559
- },
560
- depth: { type: 'number', description: 'Impact analysis depth (default: 3)', default: 3 },
561
- file: { type: 'string', description: 'Scope to file (partial match)' },
562
- kind: {
563
- type: 'string',
564
- description: 'Filter by symbol kind (function, method, class, etc.)',
565
- },
566
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
567
- ...PAGINATION_PROPS,
568
- },
569
- required: ['target'],
570
- },
571
- },
572
- {
573
- name: 'batch_query',
574
- description:
575
- 'Run a query command against multiple targets in one call. Returns all results in a single JSON payload — ideal for multi-agent dispatch.',
576
- inputSchema: {
577
- type: 'object',
578
- properties: {
579
- command: {
580
- type: 'string',
581
- enum: [
582
- 'fn-impact',
583
- 'context',
584
- 'explain',
585
- 'where',
586
- 'query',
587
- 'impact',
588
- 'deps',
589
- 'flow',
590
- 'dataflow',
591
- 'complexity',
592
- ],
593
- description: 'The query command to run for each target',
594
- },
595
- targets: {
596
- type: 'array',
597
- items: { type: 'string' },
598
- description: 'List of target names (symbol names or file paths depending on command)',
599
- },
600
- depth: {
601
- type: 'number',
602
- description: 'Traversal depth (for fn-impact, context, fn, flow)',
603
- },
604
- file: {
605
- type: 'string',
606
- description: 'Scope to file (partial match)',
607
- },
608
- kind: {
609
- type: 'string',
610
- enum: EVERY_SYMBOL_KIND,
611
- description: 'Filter symbol kind',
612
- },
613
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
614
- },
615
- required: ['command', 'targets'],
616
- },
617
- },
618
- {
619
- name: 'triage',
620
- description:
621
- '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.',
622
- inputSchema: {
623
- type: 'object',
624
- properties: {
625
- level: {
626
- type: 'string',
627
- enum: ['function', 'file', 'directory'],
628
- description:
629
- 'Granularity: function (default) | file | directory. File/directory shows hotspots',
630
- },
631
- sort: {
632
- type: 'string',
633
- enum: ['risk', 'complexity', 'churn', 'fan-in', 'mi'],
634
- description: 'Sort metric (default: risk)',
635
- },
636
- min_score: {
637
- type: 'number',
638
- description: 'Only return symbols with risk score >= this threshold (0-1)',
639
- },
640
- role: {
641
- type: 'string',
642
- enum: VALID_ROLES,
643
- description: 'Filter by role classification',
644
- },
645
- file: { type: 'string', description: 'Scope to file (partial match)' },
646
- kind: {
647
- type: 'string',
648
- enum: ['function', 'method', 'class'],
649
- description: 'Filter by symbol kind',
650
- },
651
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
652
- weights: {
653
- type: 'object',
654
- description:
655
- 'Custom scoring weights (e.g. {"fanIn":1,"complexity":0,"churn":0,"role":0,"mi":0})',
656
- },
657
- ...PAGINATION_PROPS,
658
- },
659
- },
660
- },
661
- {
662
- name: 'branch_compare',
663
- description:
664
- 'Compare code structure between two git refs (branches, tags, commits). Shows added/removed/changed symbols and transitive caller impact using temporary git worktrees.',
665
- inputSchema: {
666
- type: 'object',
667
- properties: {
668
- base: { type: 'string', description: 'Base git ref (branch, tag, or commit SHA)' },
669
- target: { type: 'string', description: 'Target git ref to compare against base' },
670
- depth: { type: 'number', description: 'Max transitive caller depth', default: 3 },
671
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
672
- format: {
673
- type: 'string',
674
- enum: ['json', 'mermaid'],
675
- description: 'Output format (default: json)',
676
- },
677
- },
678
- required: ['base', 'target'],
679
- },
680
- },
681
- {
682
- name: 'cfg',
683
- description: 'Show intraprocedural control flow graph for a function.',
684
- inputSchema: {
685
- type: 'object',
686
- properties: {
687
- name: { type: 'string', description: 'Function/method name (partial match)' },
688
- format: {
689
- type: 'string',
690
- enum: ['json', 'dot', 'mermaid'],
691
- description: 'Output format (default: json)',
692
- },
693
- file: { type: 'string', description: 'Scope to file (partial match)' },
694
- kind: { type: 'string', enum: EVERY_SYMBOL_KIND, description: 'Filter by symbol kind' },
695
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
696
- ...PAGINATION_PROPS,
697
- },
698
- required: ['name'],
699
- },
700
- },
701
- {
702
- name: 'dataflow',
703
- description: 'Show data flow edges or data-dependent blast radius.',
704
- inputSchema: {
705
- type: 'object',
706
- properties: {
707
- name: { type: 'string', description: 'Function/method name (partial match)' },
708
- mode: {
709
- type: 'string',
710
- enum: ['edges', 'impact'],
711
- description: 'edges (default) or impact',
712
- },
713
- depth: { type: 'number', description: 'Max depth for impact mode', default: 5 },
714
- file: { type: 'string', description: 'Scope to file (partial match)' },
715
- kind: { type: 'string', enum: EVERY_SYMBOL_KIND, description: 'Filter by symbol kind' },
716
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
717
- ...PAGINATION_PROPS,
718
- },
719
- required: ['name'],
720
- },
721
- },
722
- {
723
- name: 'check',
724
- description:
725
- 'CI gate: run manifesto rules (no args), diff predicates (with ref/staged), or both (with rules flag). Returns pass/fail verdicts.',
726
- inputSchema: {
727
- type: 'object',
728
- properties: {
729
- ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
730
- staged: { type: 'boolean', description: 'Analyze staged changes instead of unstaged' },
731
- rules: {
732
- type: 'boolean',
733
- description: 'Also run manifesto rules alongside diff predicates',
734
- },
735
- cycles: { type: 'boolean', description: 'Enable cycles predicate (default: true)' },
736
- blast_radius: {
737
- type: 'number',
738
- description: 'Max transitive callers threshold (null = disabled)',
739
- },
740
- signatures: { type: 'boolean', description: 'Enable signatures predicate (default: true)' },
741
- boundaries: { type: 'boolean', description: 'Enable boundaries predicate (default: true)' },
742
- depth: { type: 'number', description: 'Max BFS depth for blast radius (default: 3)' },
743
- file: { type: 'string', description: 'Scope to file (partial match, manifesto mode)' },
744
- kind: {
745
- type: 'string',
746
- description: 'Filter by symbol kind (manifesto mode)',
747
- },
748
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
749
- ...PAGINATION_PROPS,
750
- },
751
- },
752
- },
753
- {
754
- name: 'ast_query',
755
- description:
756
- 'Search stored AST nodes (calls, literals, new, throw, await) by pattern. Requires a prior build.',
757
- inputSchema: {
758
- type: 'object',
759
- properties: {
760
- pattern: {
761
- type: 'string',
762
- description: 'GLOB pattern for node name (auto-wrapped in *..* for substring match)',
763
- },
764
- kind: {
765
- type: 'string',
766
- enum: AST_NODE_KINDS,
767
- description: 'Filter by AST node kind',
768
- },
769
- file: { type: 'string', description: 'Scope to file (partial match)' },
770
- no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
771
- ...PAGINATION_PROPS,
772
- },
773
- },
774
- },
775
- ];
776
-
777
- const LIST_REPOS_TOOL = {
778
- name: 'list_repos',
779
- description: 'List all repositories registered in the codegraph registry',
780
- inputSchema: {
781
- type: 'object',
782
- properties: {},
783
- },
784
- };
785
-
786
- /**
787
- * Build the tool list based on multi-repo mode.
788
- * @param {boolean} multiRepo - If true, inject `repo` prop into each tool and append `list_repos`
789
- * @returns {object[]}
790
- */
791
- function buildToolList(multiRepo) {
792
- if (!multiRepo) return BASE_TOOLS;
793
- return [
794
- ...BASE_TOOLS.map((tool) => ({
795
- ...tool,
796
- inputSchema: {
797
- ...tool.inputSchema,
798
- properties: { ...tool.inputSchema.properties, ...REPO_PROP },
799
- },
800
- })),
801
- LIST_REPOS_TOOL,
802
- ];
803
- }
804
-
805
- // Backward-compatible export: full multi-repo tool list
806
- const TOOLS = buildToolList(true);
807
-
808
- export { TOOLS, buildToolList };
809
-
810
- /**
811
- * Start the MCP server.
812
- * This function requires @modelcontextprotocol/sdk to be installed.
813
- *
814
- * @param {string} [customDbPath] - Path to a specific graph.db
815
- * @param {object} [options]
816
- * @param {boolean} [options.multiRepo] - Enable multi-repo access (default: false)
817
- * @param {string[]} [options.allowedRepos] - Restrict access to these repo names only
818
- */
819
- export async function startMCPServer(customDbPath, options = {}) {
820
- const { allowedRepos } = options;
821
- const multiRepo = options.multiRepo || !!allowedRepos;
822
- let Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema;
823
- try {
824
- const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
825
- Server = sdk.Server;
826
- const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
827
- StdioServerTransport = transport.StdioServerTransport;
828
- const types = await import('@modelcontextprotocol/sdk/types.js');
829
- ListToolsRequestSchema = types.ListToolsRequestSchema;
830
- CallToolRequestSchema = types.CallToolRequestSchema;
831
- } catch {
832
- console.error(
833
- 'MCP server requires @modelcontextprotocol/sdk.\n' +
834
- 'Install it with: npm install @modelcontextprotocol/sdk',
835
- );
836
- process.exit(1);
837
- }
838
-
839
- // Connect transport FIRST so the server can receive the client's
840
- // `initialize` request while heavy modules (queries, better-sqlite3)
841
- // are still loading. These are lazy-loaded on the first tool call
842
- // and cached for subsequent calls.
843
- let _queries;
844
- let _Database;
845
-
846
- async function getQueries() {
847
- if (!_queries) {
848
- _queries = await import('./queries.js');
849
- }
850
- return _queries;
851
- }
852
-
853
- function getDatabase() {
854
- if (!_Database) {
855
- const require = createRequire(import.meta.url);
856
- _Database = require('better-sqlite3');
857
- }
858
- return _Database;
859
- }
860
-
861
- const server = new Server(
862
- { name: 'codegraph', version: '1.0.0' },
863
- { capabilities: { tools: {} } },
864
- );
865
-
866
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
867
- tools: buildToolList(multiRepo),
868
- }));
869
-
870
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
871
- const { name, arguments: args } = request.params;
872
- try {
873
- const {
874
- impactAnalysisData,
875
- moduleMapData,
876
- fileDepsData,
877
- exportsData,
878
- fnDepsData,
879
- fnImpactData,
880
- pathData,
881
- contextData,
882
- childrenData,
883
- explainData,
884
- whereData,
885
- diffImpactData,
886
- listFunctionsData,
887
- rolesData,
888
- } = await getQueries();
889
- const Database = getDatabase();
890
- if (!multiRepo && args.repo) {
891
- throw new Error(
892
- 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
893
- );
894
- }
895
- if (!multiRepo && name === 'list_repos') {
896
- throw new Error(
897
- 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
898
- );
899
- }
900
-
901
- let dbPath = customDbPath || undefined;
902
- if (args.repo) {
903
- if (allowedRepos && !allowedRepos.includes(args.repo)) {
904
- throw new Error(`Repository "${args.repo}" is not in the allowed repos list.`);
905
- }
906
- const { resolveRepoDbPath } = await import('./registry.js');
907
- const resolved = resolveRepoDbPath(args.repo);
908
- if (!resolved)
909
- throw new Error(
910
- `Repository "${args.repo}" not found in registry or its database is missing.`,
911
- );
912
- dbPath = resolved;
913
- }
914
-
915
- let result;
916
- switch (name) {
917
- case 'query': {
918
- const qMode = args.mode || 'deps';
919
- if (qMode === 'path') {
920
- if (!args.to) {
921
- result = { error: 'path mode requires a "to" argument' };
922
- break;
923
- }
924
- result = pathData(args.name, args.to, dbPath, {
925
- maxDepth: args.depth ?? 10,
926
- edgeKinds: args.edge_kinds,
927
- reverse: args.reverse,
928
- fromFile: args.from_file,
929
- toFile: args.to_file,
930
- kind: args.kind,
931
- noTests: args.no_tests,
932
- });
933
- } else {
934
- result = fnDepsData(args.name, dbPath, {
935
- depth: args.depth,
936
- file: args.file,
937
- kind: args.kind,
938
- noTests: args.no_tests,
939
- limit: Math.min(args.limit ?? MCP_DEFAULTS.query, MCP_MAX_LIMIT),
940
- offset: args.offset ?? 0,
941
- });
942
- }
943
- break;
944
- }
945
- case 'path':
946
- result = pathData(args.from, args.to, dbPath, {
947
- maxDepth: args.depth ?? 10,
948
- edgeKinds: args.edge_kinds,
949
- fromFile: args.from_file,
950
- toFile: args.to_file,
951
- noTests: args.no_tests,
952
- });
953
- break;
954
- case 'file_deps':
955
- result = fileDepsData(args.file, dbPath, {
956
- noTests: args.no_tests,
957
- limit: Math.min(args.limit ?? MCP_DEFAULTS.file_deps, MCP_MAX_LIMIT),
958
- offset: args.offset ?? 0,
959
- });
960
- break;
961
- case 'file_exports':
962
- result = exportsData(args.file, dbPath, {
963
- noTests: args.no_tests,
964
- unused: args.unused,
965
- limit: Math.min(args.limit ?? MCP_DEFAULTS.file_exports, MCP_MAX_LIMIT),
966
- offset: args.offset ?? 0,
967
- });
968
- break;
969
- case 'impact_analysis':
970
- result = impactAnalysisData(args.file, dbPath, {
971
- noTests: args.no_tests,
972
- limit: Math.min(args.limit ?? MCP_DEFAULTS.impact_analysis, MCP_MAX_LIMIT),
973
- offset: args.offset ?? 0,
974
- });
975
- break;
976
- case 'find_cycles': {
977
- const db = new Database(findDbPath(dbPath), { readonly: true });
978
- const cycles = findCycles(db);
979
- db.close();
980
- result = { cycles, count: cycles.length };
981
- break;
982
- }
983
- case 'module_map':
984
- result = moduleMapData(dbPath, args.limit || 20, { noTests: args.no_tests });
985
- break;
986
- case 'fn_impact':
987
- result = fnImpactData(args.name, dbPath, {
988
- depth: args.depth,
989
- file: args.file,
990
- kind: args.kind,
991
- noTests: args.no_tests,
992
- limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_impact, MCP_MAX_LIMIT),
993
- offset: args.offset ?? 0,
994
- });
995
- break;
996
- case 'context':
997
- result = contextData(args.name, dbPath, {
998
- depth: args.depth,
999
- file: args.file,
1000
- kind: args.kind,
1001
- noSource: args.no_source,
1002
- noTests: args.no_tests,
1003
- includeTests: args.include_tests,
1004
- limit: Math.min(args.limit ?? MCP_DEFAULTS.context, MCP_MAX_LIMIT),
1005
- offset: args.offset ?? 0,
1006
- });
1007
- break;
1008
- case 'symbol_children':
1009
- result = childrenData(args.name, dbPath, {
1010
- file: args.file,
1011
- kind: args.kind,
1012
- noTests: args.no_tests,
1013
- limit: Math.min(args.limit ?? MCP_DEFAULTS.context, MCP_MAX_LIMIT),
1014
- offset: args.offset ?? 0,
1015
- });
1016
- break;
1017
- case 'where':
1018
- result = whereData(args.target, dbPath, {
1019
- file: args.file_mode,
1020
- noTests: args.no_tests,
1021
- limit: Math.min(args.limit ?? MCP_DEFAULTS.where, MCP_MAX_LIMIT),
1022
- offset: args.offset ?? 0,
1023
- });
1024
- break;
1025
- case 'diff_impact':
1026
- if (args.format === 'mermaid') {
1027
- result = diffImpactMermaid(dbPath, {
1028
- staged: args.staged,
1029
- ref: args.ref,
1030
- depth: args.depth,
1031
- noTests: args.no_tests,
1032
- });
1033
- } else {
1034
- result = diffImpactData(dbPath, {
1035
- staged: args.staged,
1036
- ref: args.ref,
1037
- depth: args.depth,
1038
- noTests: args.no_tests,
1039
- limit: Math.min(args.limit ?? MCP_DEFAULTS.diff_impact, MCP_MAX_LIMIT),
1040
- offset: args.offset ?? 0,
1041
- });
1042
- }
1043
- break;
1044
- case 'semantic_search': {
1045
- const mode = args.mode || 'hybrid';
1046
- const searchOpts = {
1047
- limit: Math.min(args.limit ?? MCP_DEFAULTS.semantic_search, MCP_MAX_LIMIT),
1048
- offset: args.offset ?? 0,
1049
- minScore: args.min_score,
1050
- };
1051
-
1052
- if (mode === 'keyword') {
1053
- const { ftsSearchData } = await import('./embedder.js');
1054
- result = ftsSearchData(args.query, dbPath, searchOpts);
1055
- if (result === null) {
1056
- return {
1057
- content: [
1058
- {
1059
- type: 'text',
1060
- text: 'No FTS5 index found. Run `codegraph embed` to build the keyword index.',
1061
- },
1062
- ],
1063
- isError: true,
1064
- };
1065
- }
1066
- } else if (mode === 'semantic') {
1067
- const { searchData } = await import('./embedder.js');
1068
- result = await searchData(args.query, dbPath, searchOpts);
1069
- if (result === null) {
1070
- return {
1071
- content: [
1072
- {
1073
- type: 'text',
1074
- text: 'Semantic search unavailable. Run `codegraph embed` first.',
1075
- },
1076
- ],
1077
- isError: true,
1078
- };
1079
- }
1080
- } else {
1081
- // hybrid (default) — falls back to semantic if no FTS5
1082
- const { hybridSearchData, searchData } = await import('./embedder.js');
1083
- result = await hybridSearchData(args.query, dbPath, searchOpts);
1084
- if (result === null) {
1085
- result = await searchData(args.query, dbPath, searchOpts);
1086
- if (result === null) {
1087
- return {
1088
- content: [
1089
- {
1090
- type: 'text',
1091
- text: 'Semantic search unavailable. Run `codegraph embed` first.',
1092
- },
1093
- ],
1094
- isError: true,
1095
- };
1096
- }
1097
- }
1098
- }
1099
- break;
1100
- }
1101
- case 'export_graph': {
1102
- const {
1103
- exportDOT,
1104
- exportGraphML,
1105
- exportGraphSON,
1106
- exportJSON,
1107
- exportMermaid,
1108
- exportNeo4jCSV,
1109
- } = await import('./export.js');
1110
- const db = new Database(findDbPath(dbPath), { readonly: true });
1111
- const fileLevel = args.file_level !== false;
1112
- const exportLimit = args.limit
1113
- ? Math.min(args.limit, MCP_MAX_LIMIT)
1114
- : MCP_DEFAULTS.export_graph;
1115
- switch (args.format) {
1116
- case 'dot':
1117
- result = exportDOT(db, { fileLevel, limit: exportLimit });
1118
- break;
1119
- case 'mermaid':
1120
- result = exportMermaid(db, { fileLevel, limit: exportLimit });
1121
- break;
1122
- case 'json':
1123
- result = exportJSON(db, {
1124
- limit: exportLimit,
1125
- offset: args.offset ?? 0,
1126
- });
1127
- break;
1128
- case 'graphml':
1129
- result = exportGraphML(db, { fileLevel, limit: exportLimit });
1130
- break;
1131
- case 'graphson':
1132
- result = exportGraphSON(db, {
1133
- fileLevel,
1134
- limit: exportLimit,
1135
- offset: args.offset ?? 0,
1136
- });
1137
- break;
1138
- case 'neo4j':
1139
- result = exportNeo4jCSV(db, { fileLevel, limit: exportLimit });
1140
- break;
1141
- default:
1142
- db.close();
1143
- return {
1144
- content: [
1145
- {
1146
- type: 'text',
1147
- text: `Unknown format: ${args.format}. Use dot, mermaid, json, graphml, graphson, or neo4j.`,
1148
- },
1149
- ],
1150
- isError: true,
1151
- };
1152
- }
1153
- db.close();
1154
- break;
1155
- }
1156
- case 'list_functions':
1157
- result = listFunctionsData(dbPath, {
1158
- file: args.file,
1159
- pattern: args.pattern,
1160
- noTests: args.no_tests,
1161
- limit: Math.min(args.limit ?? MCP_DEFAULTS.list_functions, MCP_MAX_LIMIT),
1162
- offset: args.offset ?? 0,
1163
- });
1164
- break;
1165
- case 'node_roles':
1166
- result = rolesData(dbPath, {
1167
- role: args.role,
1168
- file: args.file,
1169
- noTests: args.no_tests,
1170
- limit: Math.min(args.limit ?? MCP_DEFAULTS.node_roles, MCP_MAX_LIMIT),
1171
- offset: args.offset ?? 0,
1172
- });
1173
- break;
1174
- case 'structure': {
1175
- const { structureData } = await import('./structure.js');
1176
- result = structureData(dbPath, {
1177
- directory: args.directory,
1178
- depth: args.depth,
1179
- sort: args.sort,
1180
- full: args.full,
1181
- limit: Math.min(args.limit ?? MCP_DEFAULTS.structure, MCP_MAX_LIMIT),
1182
- offset: args.offset ?? 0,
1183
- });
1184
- break;
1185
- }
1186
- case 'co_changes': {
1187
- const { coChangeData, coChangeTopData } = await import('./cochange.js');
1188
- result = args.file
1189
- ? coChangeData(args.file, dbPath, {
1190
- limit: Math.min(args.limit ?? MCP_DEFAULTS.co_changes, MCP_MAX_LIMIT),
1191
- offset: args.offset ?? 0,
1192
- minJaccard: args.min_jaccard,
1193
- noTests: args.no_tests,
1194
- })
1195
- : coChangeTopData(dbPath, {
1196
- limit: Math.min(args.limit ?? MCP_DEFAULTS.co_changes, MCP_MAX_LIMIT),
1197
- offset: args.offset ?? 0,
1198
- minJaccard: args.min_jaccard,
1199
- noTests: args.no_tests,
1200
- });
1201
- break;
1202
- }
1203
- case 'execution_flow': {
1204
- if (args.list) {
1205
- const { listEntryPointsData } = await import('./flow.js');
1206
- result = listEntryPointsData(dbPath, {
1207
- noTests: args.no_tests,
1208
- limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1209
- offset: args.offset ?? 0,
1210
- });
1211
- } else {
1212
- if (!args.name) {
1213
- result = { error: 'Provide a name or set list=true' };
1214
- break;
1215
- }
1216
- const { flowData } = await import('./flow.js');
1217
- result = flowData(args.name, dbPath, {
1218
- depth: args.depth,
1219
- file: args.file,
1220
- kind: args.kind,
1221
- noTests: args.no_tests,
1222
- limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1223
- offset: args.offset ?? 0,
1224
- });
1225
- }
1226
- break;
1227
- }
1228
- case 'sequence': {
1229
- const { sequenceData, sequenceToMermaid } = await import('./sequence.js');
1230
- const seqResult = sequenceData(args.name, dbPath, {
1231
- depth: args.depth,
1232
- file: args.file,
1233
- kind: args.kind,
1234
- dataflow: args.dataflow,
1235
- noTests: args.no_tests,
1236
- limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
1237
- offset: args.offset ?? 0,
1238
- });
1239
- result =
1240
- args.format === 'json'
1241
- ? seqResult
1242
- : { text: sequenceToMermaid(seqResult), ...seqResult };
1243
- break;
1244
- }
1245
- case 'complexity': {
1246
- const { complexityData } = await import('./complexity.js');
1247
- result = complexityData(dbPath, {
1248
- target: args.name,
1249
- file: args.file,
1250
- limit: Math.min(args.limit ?? MCP_DEFAULTS.complexity, MCP_MAX_LIMIT),
1251
- offset: args.offset ?? 0,
1252
- sort: args.sort,
1253
- aboveThreshold: args.above_threshold,
1254
- health: args.health,
1255
- noTests: args.no_tests,
1256
- kind: args.kind,
1257
- });
1258
- break;
1259
- }
1260
- case 'communities': {
1261
- const { communitiesData } = await import('./communities.js');
1262
- result = communitiesData(dbPath, {
1263
- functions: args.functions,
1264
- resolution: args.resolution,
1265
- drift: args.drift,
1266
- noTests: args.no_tests,
1267
- limit: Math.min(args.limit ?? MCP_DEFAULTS.communities, MCP_MAX_LIMIT),
1268
- offset: args.offset ?? 0,
1269
- });
1270
- break;
1271
- }
1272
- case 'code_owners': {
1273
- const { ownersData } = await import('./owners.js');
1274
- result = ownersData(dbPath, {
1275
- file: args.file,
1276
- owner: args.owner,
1277
- boundary: args.boundary,
1278
- kind: args.kind,
1279
- noTests: args.no_tests,
1280
- });
1281
- break;
1282
- }
1283
- case 'audit': {
1284
- if (args.quick) {
1285
- result = explainData(args.target, dbPath, {
1286
- noTests: args.no_tests,
1287
- limit: Math.min(args.limit ?? MCP_DEFAULTS.explain, MCP_MAX_LIMIT),
1288
- offset: args.offset ?? 0,
1289
- });
1290
- } else {
1291
- const { auditData } = await import('./audit.js');
1292
- result = auditData(args.target, dbPath, {
1293
- depth: args.depth,
1294
- file: args.file,
1295
- kind: args.kind,
1296
- noTests: args.no_tests,
1297
- });
1298
- }
1299
- break;
1300
- }
1301
- case 'batch_query': {
1302
- const { batchData } = await import('./batch.js');
1303
- result = batchData(args.command, args.targets, dbPath, {
1304
- depth: args.depth,
1305
- file: args.file,
1306
- kind: args.kind,
1307
- noTests: args.no_tests,
1308
- });
1309
- break;
1310
- }
1311
- case 'triage': {
1312
- if (args.level === 'file' || args.level === 'directory') {
1313
- const { hotspotsData } = await import('./structure.js');
1314
- const TRIAGE_TO_HOTSPOT = {
1315
- risk: 'fan-in',
1316
- complexity: 'density',
1317
- churn: 'coupling',
1318
- mi: 'fan-in',
1319
- };
1320
- const metric = TRIAGE_TO_HOTSPOT[args.sort] ?? args.sort;
1321
- result = hotspotsData(dbPath, {
1322
- metric,
1323
- level: args.level,
1324
- limit: Math.min(args.limit ?? MCP_DEFAULTS.hotspots, MCP_MAX_LIMIT),
1325
- offset: args.offset ?? 0,
1326
- noTests: args.no_tests,
1327
- });
1328
- } else {
1329
- const { triageData } = await import('./triage.js');
1330
- result = triageData(dbPath, {
1331
- sort: args.sort,
1332
- minScore: args.min_score,
1333
- role: args.role,
1334
- file: args.file,
1335
- kind: args.kind,
1336
- noTests: args.no_tests,
1337
- weights: args.weights,
1338
- limit: Math.min(args.limit ?? MCP_DEFAULTS.triage, MCP_MAX_LIMIT),
1339
- offset: args.offset ?? 0,
1340
- });
1341
- }
1342
- break;
1343
- }
1344
- case 'branch_compare': {
1345
- const { branchCompareData, branchCompareMermaid } = await import('./branch-compare.js');
1346
- const bcData = await branchCompareData(args.base, args.target, {
1347
- depth: args.depth,
1348
- noTests: args.no_tests,
1349
- });
1350
- result = args.format === 'mermaid' ? branchCompareMermaid(bcData) : bcData;
1351
- break;
1352
- }
1353
- case 'cfg': {
1354
- const { cfgData, cfgToDOT, cfgToMermaid } = await import('./cfg.js');
1355
- const cfgResult = cfgData(args.name, dbPath, {
1356
- file: args.file,
1357
- kind: args.kind,
1358
- noTests: args.no_tests,
1359
- limit: Math.min(args.limit ?? MCP_DEFAULTS.query, MCP_MAX_LIMIT),
1360
- offset: args.offset ?? 0,
1361
- });
1362
- if (args.format === 'dot') {
1363
- result = { text: cfgToDOT(cfgResult) };
1364
- } else if (args.format === 'mermaid') {
1365
- result = { text: cfgToMermaid(cfgResult) };
1366
- } else {
1367
- result = cfgResult;
1368
- }
1369
- break;
1370
- }
1371
- case 'dataflow': {
1372
- const dfMode = args.mode || 'edges';
1373
- if (dfMode === 'impact') {
1374
- const { dataflowImpactData } = await import('./dataflow.js');
1375
- result = dataflowImpactData(args.name, dbPath, {
1376
- depth: args.depth,
1377
- file: args.file,
1378
- kind: args.kind,
1379
- noTests: args.no_tests,
1380
- limit: Math.min(args.limit ?? MCP_DEFAULTS.fn_impact, MCP_MAX_LIMIT),
1381
- offset: args.offset ?? 0,
1382
- });
1383
- } else {
1384
- const { dataflowData } = await import('./dataflow.js');
1385
- result = dataflowData(args.name, dbPath, {
1386
- file: args.file,
1387
- kind: args.kind,
1388
- noTests: args.no_tests,
1389
- limit: Math.min(args.limit ?? MCP_DEFAULTS.query, MCP_MAX_LIMIT),
1390
- offset: args.offset ?? 0,
1391
- });
1392
- }
1393
- break;
1394
- }
1395
- case 'check': {
1396
- const isDiffMode = args.ref || args.staged;
1397
-
1398
- if (!isDiffMode && !args.rules) {
1399
- // No ref, no staged → run manifesto rules on whole codebase
1400
- const { manifestoData } = await import('./manifesto.js');
1401
- result = manifestoData(dbPath, {
1402
- file: args.file,
1403
- noTests: args.no_tests,
1404
- kind: args.kind,
1405
- limit: Math.min(args.limit ?? MCP_DEFAULTS.manifesto, MCP_MAX_LIMIT),
1406
- offset: args.offset ?? 0,
1407
- });
1408
- } else {
1409
- const { checkData } = await import('./check.js');
1410
- const checkResult = checkData(dbPath, {
1411
- ref: args.ref,
1412
- staged: args.staged,
1413
- cycles: args.cycles,
1414
- blastRadius: args.blast_radius,
1415
- signatures: args.signatures,
1416
- boundaries: args.boundaries,
1417
- depth: args.depth,
1418
- noTests: args.no_tests,
1419
- });
1420
-
1421
- if (args.rules) {
1422
- const { manifestoData } = await import('./manifesto.js');
1423
- const manifestoResult = manifestoData(dbPath, {
1424
- file: args.file,
1425
- noTests: args.no_tests,
1426
- kind: args.kind,
1427
- limit: Math.min(args.limit ?? MCP_DEFAULTS.manifesto, MCP_MAX_LIMIT),
1428
- offset: args.offset ?? 0,
1429
- });
1430
- result = { check: checkResult, manifesto: manifestoResult };
1431
- } else {
1432
- result = checkResult;
1433
- }
1434
- }
1435
- break;
1436
- }
1437
- case 'ast_query': {
1438
- const { astQueryData } = await import('./ast.js');
1439
- result = astQueryData(args.pattern, dbPath, {
1440
- kind: args.kind,
1441
- file: args.file,
1442
- noTests: args.no_tests,
1443
- limit: Math.min(args.limit ?? MCP_DEFAULTS.ast_query, MCP_MAX_LIMIT),
1444
- offset: args.offset ?? 0,
1445
- });
1446
- break;
1447
- }
1448
- case 'list_repos': {
1449
- const { listRepos, pruneRegistry } = await import('./registry.js');
1450
- pruneRegistry();
1451
- let repos = listRepos();
1452
- if (allowedRepos) {
1453
- repos = repos.filter((r) => allowedRepos.includes(r.name));
1454
- }
1455
- result = { repos };
1456
- break;
1457
- }
1458
- default:
1459
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
1460
- }
1461
-
1462
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1463
- } catch (err) {
1464
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1465
- }
1466
- });
1467
-
1468
- const transport = new StdioServerTransport();
1469
- await server.connect(transport);
1470
- }
1
+ export { startMCPServer } from './mcp/server.js';
2
+ export { buildToolList, TOOLS } from './mcp/tool-registry.js';