@mgamil/mapx 0.2.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 (203) hide show
  1. package/LICENSE +194 -0
  2. package/README.md +488 -0
  3. package/VERSION +1 -0
  4. package/dist/agents/generator.d.ts +74 -0
  5. package/dist/agents/generator.js +375 -0
  6. package/dist/agents/templates.d.ts +29 -0
  7. package/dist/agents/templates.js +459 -0
  8. package/dist/cli.d.ts +16 -0
  9. package/dist/cli.js +1835 -0
  10. package/dist/core/cluster-engine.d.ts +32 -0
  11. package/dist/core/cluster-engine.js +314 -0
  12. package/dist/core/config.d.ts +29 -0
  13. package/dist/core/config.js +178 -0
  14. package/dist/core/context-builder.d.ts +61 -0
  15. package/dist/core/context-builder.js +252 -0
  16. package/dist/core/flow-tracer.d.ts +63 -0
  17. package/dist/core/flow-tracer.js +366 -0
  18. package/dist/core/git-tracker.d.ts +20 -0
  19. package/dist/core/git-tracker.js +159 -0
  20. package/dist/core/graph.d.ts +42 -0
  21. package/dist/core/graph.js +186 -0
  22. package/dist/core/metrics.d.ts +24 -0
  23. package/dist/core/metrics.js +87 -0
  24. package/dist/core/scanner.d.ts +53 -0
  25. package/dist/core/scanner.js +949 -0
  26. package/dist/core/store-bun.d.ts +13 -0
  27. package/dist/core/store-bun.js +34 -0
  28. package/dist/core/store-interface.d.ts +15 -0
  29. package/dist/core/store-interface.js +7 -0
  30. package/dist/core/store-node.d.ts +13 -0
  31. package/dist/core/store-node.js +35 -0
  32. package/dist/core/store.d.ts +132 -0
  33. package/dist/core/store.js +614 -0
  34. package/dist/core/workspace-manager.d.ts +9 -0
  35. package/dist/core/workspace-manager.js +64 -0
  36. package/dist/exporters/dot-exporter.d.ts +16 -0
  37. package/dist/exporters/dot-exporter.js +179 -0
  38. package/dist/exporters/graph-exporter.d.ts +14 -0
  39. package/dist/exporters/graph-exporter.js +85 -0
  40. package/dist/exporters/index.d.ts +9 -0
  41. package/dist/exporters/index.js +12 -0
  42. package/dist/exporters/llm-exporter.d.ts +18 -0
  43. package/dist/exporters/llm-exporter.js +224 -0
  44. package/dist/exporters/svg-exporter.d.ts +19 -0
  45. package/dist/exporters/svg-exporter.js +319 -0
  46. package/dist/exporters/toon-exporter.d.ts +16 -0
  47. package/dist/exporters/toon-exporter.js +246 -0
  48. package/dist/frameworks/detectors/aspnet.d.ts +11 -0
  49. package/dist/frameworks/detectors/aspnet.js +52 -0
  50. package/dist/frameworks/detectors/django.d.ts +14 -0
  51. package/dist/frameworks/detectors/django.js +135 -0
  52. package/dist/frameworks/detectors/drupal.d.ts +13 -0
  53. package/dist/frameworks/detectors/drupal.js +94 -0
  54. package/dist/frameworks/detectors/express.d.ts +12 -0
  55. package/dist/frameworks/detectors/express.js +234 -0
  56. package/dist/frameworks/detectors/fastapi.d.ts +12 -0
  57. package/dist/frameworks/detectors/fastapi.js +203 -0
  58. package/dist/frameworks/detectors/flask.d.ts +12 -0
  59. package/dist/frameworks/detectors/flask.js +244 -0
  60. package/dist/frameworks/detectors/go.d.ts +11 -0
  61. package/dist/frameworks/detectors/go.js +75 -0
  62. package/dist/frameworks/detectors/laravel.d.ts +11 -0
  63. package/dist/frameworks/detectors/laravel.js +462 -0
  64. package/dist/frameworks/detectors/nestjs.d.ts +12 -0
  65. package/dist/frameworks/detectors/nestjs.js +155 -0
  66. package/dist/frameworks/detectors/nextjs.d.ts +11 -0
  67. package/dist/frameworks/detectors/nextjs.js +118 -0
  68. package/dist/frameworks/detectors/rails.d.ts +12 -0
  69. package/dist/frameworks/detectors/rails.js +76 -0
  70. package/dist/frameworks/detectors/react-router.d.ts +11 -0
  71. package/dist/frameworks/detectors/react-router.js +115 -0
  72. package/dist/frameworks/detectors/rust.d.ts +11 -0
  73. package/dist/frameworks/detectors/rust.js +59 -0
  74. package/dist/frameworks/detectors/spring.d.ts +11 -0
  75. package/dist/frameworks/detectors/spring.js +56 -0
  76. package/dist/frameworks/detectors/sveltekit.d.ts +11 -0
  77. package/dist/frameworks/detectors/sveltekit.js +154 -0
  78. package/dist/frameworks/detectors/symfony.d.ts +13 -0
  79. package/dist/frameworks/detectors/symfony.js +175 -0
  80. package/dist/frameworks/detectors/tanstack-router.d.ts +12 -0
  81. package/dist/frameworks/detectors/tanstack-router.js +80 -0
  82. package/dist/frameworks/detectors/vapor.d.ts +11 -0
  83. package/dist/frameworks/detectors/vapor.js +52 -0
  84. package/dist/frameworks/detectors/vue-router.d.ts +12 -0
  85. package/dist/frameworks/detectors/vue-router.js +237 -0
  86. package/dist/frameworks/detectors/wordpress.d.ts +13 -0
  87. package/dist/frameworks/detectors/wordpress.js +141 -0
  88. package/dist/frameworks/detectors/yii.d.ts +11 -0
  89. package/dist/frameworks/detectors/yii.js +131 -0
  90. package/dist/frameworks/framework-registry.d.ts +13 -0
  91. package/dist/frameworks/framework-registry.js +77 -0
  92. package/dist/frameworks/route-registry.d.ts +26 -0
  93. package/dist/frameworks/route-registry.js +102 -0
  94. package/dist/index.d.ts +19 -0
  95. package/dist/index.js +30 -0
  96. package/dist/languages/index.d.ts +2 -0
  97. package/dist/languages/index.js +7 -0
  98. package/dist/languages/installer.d.ts +13 -0
  99. package/dist/languages/installer.js +103 -0
  100. package/dist/languages/registry.d.ts +19 -0
  101. package/dist/languages/registry.js +427 -0
  102. package/dist/main.d.ts +2 -0
  103. package/dist/main.js +20 -0
  104. package/dist/mcp.d.ts +11 -0
  105. package/dist/mcp.js +1699 -0
  106. package/dist/parsers/common-methods.d.ts +3 -0
  107. package/dist/parsers/common-methods.js +33 -0
  108. package/dist/parsers/fallback-parser.d.ts +10 -0
  109. package/dist/parsers/fallback-parser.js +18 -0
  110. package/dist/parsers/generic-wasm-parser.d.ts +23 -0
  111. package/dist/parsers/generic-wasm-parser.js +168 -0
  112. package/dist/parsers/ignored-symbols.d.ts +26 -0
  113. package/dist/parsers/ignored-symbols.js +77 -0
  114. package/dist/parsers/index.d.ts +9 -0
  115. package/dist/parsers/index.js +13 -0
  116. package/dist/parsers/languages/javascript.d.ts +11 -0
  117. package/dist/parsers/languages/javascript.js +28 -0
  118. package/dist/parsers/languages/php.d.ts +15 -0
  119. package/dist/parsers/languages/php.js +648 -0
  120. package/dist/parsers/languages/typescript.d.ts +10 -0
  121. package/dist/parsers/languages/typescript.js +9 -0
  122. package/dist/parsers/languages/vue.d.ts +13 -0
  123. package/dist/parsers/languages/vue.js +63 -0
  124. package/dist/parsers/parse-worker.d.ts +2 -0
  125. package/dist/parsers/parse-worker.js +185 -0
  126. package/dist/parsers/parser-interface.d.ts +9 -0
  127. package/dist/parsers/parser-interface.js +0 -0
  128. package/dist/parsers/parser-registry.d.ts +8 -0
  129. package/dist/parsers/parser-registry.js +52 -0
  130. package/dist/parsers/wasm-parser.d.ts +16 -0
  131. package/dist/parsers/wasm-parser.js +110 -0
  132. package/dist/types.d.ts +172 -0
  133. package/dist/types.js +0 -0
  134. package/dist/ui/index.html +270 -0
  135. package/dist/ui/main.js +581 -0
  136. package/dist/ui/main.js.map +7 -0
  137. package/dist/ui/styles.css +573 -0
  138. package/dist/ui-events.d.ts +36 -0
  139. package/dist/ui-events.js +61 -0
  140. package/dist/ui-server.d.ts +12 -0
  141. package/dist/ui-server.js +504 -0
  142. package/package.json +179 -0
  143. package/queries/bash/references.scm +22 -0
  144. package/queries/bash/symbols.scm +15 -0
  145. package/queries/c/references.scm +14 -0
  146. package/queries/c/symbols.scm +30 -0
  147. package/queries/c-sharp/references.scm +26 -0
  148. package/queries/c-sharp/symbols.scm +57 -0
  149. package/queries/cpp/references.scm +21 -0
  150. package/queries/cpp/symbols.scm +44 -0
  151. package/queries/dart/references.scm +33 -0
  152. package/queries/dart/symbols.scm +38 -0
  153. package/queries/elixir/references.scm +45 -0
  154. package/queries/elixir/symbols.scm +41 -0
  155. package/queries/go/references.scm +22 -0
  156. package/queries/go/symbols.scm +53 -0
  157. package/queries/java/references.scm +32 -0
  158. package/queries/java/symbols.scm +41 -0
  159. package/queries/javascript/references.scm +14 -0
  160. package/queries/javascript/symbols.scm +23 -0
  161. package/queries/kotlin/references.scm +31 -0
  162. package/queries/kotlin/symbols.scm +24 -0
  163. package/queries/lua/references.scm +19 -0
  164. package/queries/lua/symbols.scm +29 -0
  165. package/queries/pascal/references.scm +29 -0
  166. package/queries/pascal/symbols.scm +45 -0
  167. package/queries/php/references.scm +109 -0
  168. package/queries/php/symbols.scm +33 -0
  169. package/queries/python/references.scm +50 -0
  170. package/queries/python/symbols.scm +21 -0
  171. package/queries/ruby/references.scm +48 -0
  172. package/queries/ruby/symbols.scm +24 -0
  173. package/queries/rust/references.scm +31 -0
  174. package/queries/rust/symbols.scm +35 -0
  175. package/queries/scala/references.scm +30 -0
  176. package/queries/scala/symbols.scm +35 -0
  177. package/queries/svelte/references.scm +20 -0
  178. package/queries/svelte/symbols.scm +30 -0
  179. package/queries/swift/references.scm +22 -0
  180. package/queries/swift/symbols.scm +37 -0
  181. package/queries/typescript/references.scm +25 -0
  182. package/queries/typescript/symbols.scm +35 -0
  183. package/queries/vue/references.scm +20 -0
  184. package/queries/vue/symbols.scm +28 -0
  185. package/queries/zig/references.scm +20 -0
  186. package/queries/zig/symbols.scm +22 -0
  187. package/wasm/tree-sitter-c.wasm +0 -0
  188. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  189. package/wasm/tree-sitter-cpp.wasm +0 -0
  190. package/wasm/tree-sitter-dart.wasm +0 -0
  191. package/wasm/tree-sitter-go.wasm +0 -0
  192. package/wasm/tree-sitter-java.wasm +0 -0
  193. package/wasm/tree-sitter-javascript.wasm +0 -0
  194. package/wasm/tree-sitter-kotlin.wasm +0 -0
  195. package/wasm/tree-sitter-php.wasm +0 -0
  196. package/wasm/tree-sitter-python.wasm +0 -0
  197. package/wasm/tree-sitter-ruby.wasm +0 -0
  198. package/wasm/tree-sitter-rust.wasm +0 -0
  199. package/wasm/tree-sitter-scala.wasm +0 -0
  200. package/wasm/tree-sitter-swift.wasm +0 -0
  201. package/wasm/tree-sitter-tsx.wasm +0 -0
  202. package/wasm/tree-sitter-typescript.wasm +0 -0
  203. package/wasm/tree-sitter-vue.wasm +0 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,1699 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import { z } from "zod";
9
+ import { resolve } from "node:path";
10
+ import { existsSync } from "node:fs";
11
+ import { createServer } from "node:http";
12
+ import { Store } from "./core/store.js";
13
+ import { MapxGraph } from "./core/graph.js";
14
+ import { Scanner, buildMatcher } from "./core/scanner.js";
15
+ import { Config } from "./core/config.js";
16
+ import { FlowTracer } from "./core/flow-tracer.js";
17
+ import { AgentGenerator } from "./agents/generator.js";
18
+ import { LLMExporter } from "./exporters/llm-exporter.js";
19
+ import { GraphExporter } from "./exporters/graph-exporter.js";
20
+ import { DotExporter } from "./exporters/dot-exporter.js";
21
+ import { SvgExporter } from "./exporters/svg-exporter.js";
22
+ import { calculateMetrics } from "./core/metrics.js";
23
+ import { ContextBuilder } from "./core/context-builder.js";
24
+ import { getChangedFiles, isGitRepo } from "./core/git-tracker.js";
25
+ import { getBuiltinLanguages } from "./languages/registry.js";
26
+ import { isLanguageInstalled, installLanguage, uninstallLanguage } from "./languages/installer.js";
27
+ import { RouteRegistry } from "./frameworks/route-registry.js";
28
+ import { UiEventBus } from "./ui-events.js";
29
+ import { WorkspaceManager } from "./core/workspace-manager.js";
30
+ let defaultDir = null;
31
+ const DirSchema = z.object({
32
+ dir: z.string().optional()
33
+ });
34
+ function resolveDir(args) {
35
+ const parsed = DirSchema.parse(args);
36
+ if (parsed.dir) return resolve(parsed.dir);
37
+ if (defaultDir) return defaultDir;
38
+ return null;
39
+ }
40
+ async function loadContext(dir) {
41
+ const configPath = resolve(dir, ".mapx", "config.json");
42
+ if (!existsSync(configPath)) {
43
+ return { error: `Mapx not initialized in ${dir}. Run \`mapx init ${dir}\` first.` };
44
+ }
45
+ const config = await Config.load(dir);
46
+ const dbPath = resolve(dir, ".mapx", "mapx.db");
47
+ const store = new Store(dbPath);
48
+ const graph = new MapxGraph(config.repo.name);
49
+ const files = store.getAllFiles();
50
+ for (const file of files) {
51
+ graph.addFileNode(file.path, file.language, file.size_bytes, file.lines);
52
+ }
53
+ const symbols = store.getAllSymbols();
54
+ for (const sym of symbols) {
55
+ graph.addSymbolNode(sym.name, sym.file_path, sym.name, sym.kind, sym.start_line, sym.end_line, sym.scope);
56
+ }
57
+ const edges = store.getAllEdges();
58
+ for (const edge of edges) {
59
+ graph.addDependencyEdge({
60
+ sourceFile: edge.source_file,
61
+ targetFile: edge.target_file,
62
+ sourceSymbol: edge.source_symbol,
63
+ targetSymbol: edge.target_symbol,
64
+ edgeType: edge.edge_type,
65
+ repo: edge.repo,
66
+ weight: edge.weight,
67
+ verifiability: edge.verifiability,
68
+ targetRepo: edge.target_repo
69
+ });
70
+ }
71
+ return { config, store, graph };
72
+ }
73
+ const dirProperty = {
74
+ dir: {
75
+ type: "string",
76
+ description: "Target project directory (absolute or relative path). Defaults to the directory set when starting the MCP server."
77
+ }
78
+ };
79
+ function buildServer(options) {
80
+ const server = new Server(
81
+ { name: "mapx", version: "0.1.3" },
82
+ { capabilities: { tools: {} } }
83
+ );
84
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
85
+ if (options?.debug) {
86
+ process.stderr.write(`[mapx debug] Received list_tools request
87
+ `);
88
+ }
89
+ return {
90
+ tools: [
91
+ {
92
+ name: "mapx_scan",
93
+ description: "Scan the codebase and build/update the code graph. Run this when you need to understand project structure or after files have changed.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ exclude: { type: "string", description: "Comma-separated list of exclude glob patterns to append" },
98
+ include: { type: "string", description: "Comma-separated list of include glob patterns to append" },
99
+ repo: { type: "string", description: "Scan only a specific registered repository" },
100
+ all: { type: "boolean", description: "Scan all registered repositories" },
101
+ ...dirProperty
102
+ }
103
+ }
104
+ },
105
+ {
106
+ name: "mapx_sync",
107
+ description: "Incremental scan: re-scan only changed files in the codebase.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ exclude: { type: "string", description: "Comma-separated list of exclude glob patterns to append" },
112
+ include: { type: "string", description: "Comma-separated list of include glob patterns to append" },
113
+ repo: { type: "string", description: "Update/sync only a specific registered repository" },
114
+ all: { type: "boolean", description: "Update/sync all registered repositories" },
115
+ ...dirProperty
116
+ }
117
+ }
118
+ },
119
+ {
120
+ name: "mapx_query",
121
+ description: "Search for symbols (classes, functions, methods) by name pattern. Returns definitions with file locations and signatures.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ term: { type: "string", description: "Symbol name or pattern to search for" },
126
+ ...dirProperty
127
+ },
128
+ required: ["term"]
129
+ }
130
+ },
131
+ {
132
+ name: "mapx_dependencies",
133
+ description: "Get dependencies and reverse dependencies for a file. Shows what a file depends on and what depends on it.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ file: { type: "string", description: "File path to analyze (relative to project root)" },
138
+ ...dirProperty
139
+ },
140
+ required: ["file"]
141
+ }
142
+ },
143
+ {
144
+ name: "mapx_export",
145
+ description: "Export a compact, token-efficient summary of the code graph. Use this at the start of a session to quickly understand the codebase structure.",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ format: { type: "string", enum: ["llm", "json", "dot", "svg", "toon"], description: "Output format", default: "llm" },
150
+ tokens: { type: "number", description: "Token budget for LLM format", default: 8192 },
151
+ repo: { type: "string", description: "Filter by repo name" },
152
+ exclude: { type: "string", description: "Comma-separated list of exclude glob patterns to append" },
153
+ include: { type: "string", description: "Comma-separated list of include glob patterns to append" },
154
+ cluster: { type: "string", enum: ["none", "auto"], description: "Cluster rendering mode for DOT/SVG", default: "none" },
155
+ depth: { type: "number", description: "Maximum cluster nesting depth for DOT/SVG export", default: 3 },
156
+ fallback_grid: { type: "boolean", description: "Force using fallback grid SVG export", default: false },
157
+ ...dirProperty
158
+ }
159
+ }
160
+ },
161
+ {
162
+ name: "mapx_status",
163
+ description: "Check what files have changed since the last scan. Use this to determine if a re-scan is needed.",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {
167
+ exclude: { type: "string", description: "Comma-separated list of exclude glob patterns to append" },
168
+ include: { type: "string", description: "Comma-separated list of include glob patterns to append" },
169
+ ...dirProperty
170
+ }
171
+ }
172
+ },
173
+ {
174
+ name: "mapx_metrics",
175
+ description: "Get coupling (afferent/efferent) and instability metrics for all files, optionally filtered by language and verified-only.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ lang: { type: "string", description: "Filter metrics by language" },
180
+ verifiedOnly: { type: "boolean", description: "Only compute metrics using verified edges", default: false },
181
+ ...dirProperty
182
+ }
183
+ }
184
+ },
185
+ {
186
+ name: "mapx_edges",
187
+ description: "Granular query of dependency edges in the code graph.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ type: { type: "string", description: "Filter edges by type" },
192
+ from: { type: "string", description: "Filter edges originating from a file pattern (substring match)" },
193
+ to: { type: "string", description: "Filter edges targeting a file pattern (substring match)" },
194
+ ...dirProperty
195
+ }
196
+ }
197
+ },
198
+ {
199
+ name: "mapx_clusters",
200
+ description: "List detected code clusters/modules. Returns cluster hierarchy with file counts and inter-cluster dependencies.",
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ source: { type: "string", enum: ["all", "namespace", "directory", "community"], description: "Filter clusters by source type" },
205
+ cluster: { type: "string", description: "Specific cluster name to inspect" },
206
+ ...dirProperty
207
+ }
208
+ }
209
+ },
210
+ {
211
+ name: "mapx_trace",
212
+ description: "Trace data flow paths from a starting symbol or file. Returns upstream callers (up), downstream callees (down), or both. Use this to understand how data moves through the codebase.",
213
+ inputSchema: {
214
+ type: "object",
215
+ required: ["start"],
216
+ properties: {
217
+ start: { type: "string", description: "Symbol (e.g. 'UserController::store') or file path" },
218
+ direction: { type: "string", enum: ["up", "down", "both"], default: "both", description: "Direction of traversal" },
219
+ depth: { type: "number", default: 3, description: "Max traversal depth" },
220
+ format: { type: "string", enum: ["text", "dot", "json"], default: "text", description: "Output format" },
221
+ include_structural: { type: "boolean", default: false, description: "Include structural edges (e.g., import/extends)" },
222
+ ...dirProperty
223
+ }
224
+ }
225
+ },
226
+ {
227
+ name: "mapx_routes",
228
+ description: "Show routes from all detected frameworks",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ framework: { type: "string", description: "Filter by framework name" },
233
+ method: { type: "string", description: "Filter by HTTP method (GET, POST, etc.)" },
234
+ pathPattern: { type: "string", description: "Filter by route path pattern" },
235
+ ...dirProperty
236
+ }
237
+ }
238
+ },
239
+ {
240
+ name: "mapx_hooks",
241
+ description: "Show hooks/events from all detected frameworks",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ framework: { type: "string", description: "Filter by framework name" },
246
+ type: { type: "string", description: "Filter by hook type" },
247
+ namePattern: { type: "string", description: "Filter by hook name pattern" },
248
+ ...dirProperty
249
+ }
250
+ }
251
+ },
252
+ {
253
+ name: "mapx_sources",
254
+ description: "Find all entry points in the codebase \u2014 files/symbols with no incoming data-flow edges. Useful for understanding where data enters the system.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ ...dirProperty
259
+ }
260
+ }
261
+ },
262
+ {
263
+ name: "mapx_agents_generate",
264
+ description: "Generate provider-specific LLM integration files (AGENTS.md, CLAUDE.md, etc.) for the project.",
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ ...dirProperty,
269
+ providers: {
270
+ type: "array",
271
+ items: { type: "string" },
272
+ description: "List of providers to generate files for (e.g. generic, claude, cursor, copilot, windsurf, cline, aider, gemini, continue, zed)."
273
+ },
274
+ all: {
275
+ type: "boolean",
276
+ description: "Generate files for all available providers."
277
+ }
278
+ }
279
+ }
280
+ },
281
+ {
282
+ name: "mapx_sinks",
283
+ description: "Find all terminal consumers \u2014 files/symbols with no outgoing data-flow edges. Useful for identifying where data is persisted, queued, or sent externally.",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {
287
+ ...dirProperty
288
+ }
289
+ }
290
+ },
291
+ {
292
+ name: "mapx_search",
293
+ description: "Symbol search with kind/file/exact filters and importance scores.",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ term: { type: "string", description: "Symbol name or pattern to search for" },
298
+ kind: { type: "string", description: "Filter by symbol kind (e.g. class, method)" },
299
+ file: { type: "string", description: "Filter by file path prefix" },
300
+ exact: { type: "boolean", description: "Only match exact name", default: false },
301
+ limit: { type: "number", description: "Max results to return", default: 20 },
302
+ ...dirProperty
303
+ },
304
+ required: ["term"]
305
+ }
306
+ },
307
+ {
308
+ name: "mapx_context",
309
+ description: "Smart context builder: graph-expansion + keyword matching + PageRank ranking.",
310
+ inputSchema: {
311
+ type: "object",
312
+ properties: {
313
+ task: { type: "string", description: "Task description" },
314
+ seeds: { type: "array", items: { type: "string" }, description: "Specific symbols or file paths to anchor context" },
315
+ tokens: { type: "number", description: "Token budget", default: 8192 },
316
+ depth: { type: "number", description: "Graph traversal depth", default: 2 },
317
+ ...dirProperty
318
+ },
319
+ required: ["task"]
320
+ }
321
+ },
322
+ {
323
+ name: "mapx_callers",
324
+ description: "Who calls this symbol? (symbol-level, with depth)",
325
+ inputSchema: {
326
+ type: "object",
327
+ properties: {
328
+ symbol: { type: "string", description: "Symbol name" },
329
+ depth: { type: "number", description: "Traversal depth", default: 1 },
330
+ ...dirProperty
331
+ },
332
+ required: ["symbol"]
333
+ }
334
+ },
335
+ {
336
+ name: "mapx_callees",
337
+ description: "What does this symbol call? (symbol-level, with depth)",
338
+ inputSchema: {
339
+ type: "object",
340
+ properties: {
341
+ symbol: { type: "string", description: "Symbol name" },
342
+ depth: { type: "number", description: "Traversal depth", default: 1 },
343
+ ...dirProperty
344
+ },
345
+ required: ["symbol"]
346
+ }
347
+ },
348
+ {
349
+ name: "mapx_impact",
350
+ description: "Transitive blast-radius of changing a symbol with risk scoring.",
351
+ inputSchema: {
352
+ type: "object",
353
+ properties: {
354
+ symbol: { type: "string", description: "Symbol name" },
355
+ depth: { type: "number", description: "Traversal depth", default: 3 },
356
+ ...dirProperty
357
+ },
358
+ required: ["symbol"]
359
+ }
360
+ },
361
+ {
362
+ name: "mapx_node",
363
+ description: "Full symbol details + optional source code extraction.",
364
+ inputSchema: {
365
+ type: "object",
366
+ properties: {
367
+ symbol: { type: "string", description: "Symbol name" },
368
+ source: { type: "boolean", description: "Extract and display source code", default: false },
369
+ ...dirProperty
370
+ },
371
+ required: ["symbol"]
372
+ }
373
+ },
374
+ {
375
+ name: "mapx_files",
376
+ description: "Indexed file list with path/language/sort filters.",
377
+ inputSchema: {
378
+ type: "object",
379
+ properties: {
380
+ path: { type: "string", description: "Filter by path prefix" },
381
+ lang: { type: "string", description: "Filter by language" },
382
+ sort: { type: "string", enum: ["lines", "path"], description: "Sort field", default: "path" },
383
+ limit: { type: "number", description: "Max files to return", default: 50 },
384
+ ...dirProperty
385
+ }
386
+ }
387
+ },
388
+ {
389
+ name: "mapx_lang_list",
390
+ description: "List all supported languages, their extensions, tier, and installation status.",
391
+ inputSchema: {
392
+ type: "object",
393
+ properties: {}
394
+ }
395
+ },
396
+ {
397
+ name: "mapx_lang_install",
398
+ description: "Install grammar and query files for a dynamically installable language.",
399
+ inputSchema: {
400
+ type: "object",
401
+ properties: {
402
+ lang: { type: "string", description: "Name of the language (e.g. ruby, c, cpp, swift, kotlin, svelte, vue, lua, elixir, zig, bash, pascal, dart, scala)" }
403
+ },
404
+ required: ["lang"]
405
+ }
406
+ },
407
+ {
408
+ name: "mapx_lang_uninstall",
409
+ description: "Uninstall grammar and query files for a dynamically installable language.",
410
+ inputSchema: {
411
+ type: "object",
412
+ properties: {
413
+ lang: { type: "string", description: "Name of the language to uninstall" }
414
+ },
415
+ required: ["lang"]
416
+ }
417
+ },
418
+ {
419
+ name: "mapx_workspaces",
420
+ description: "List registered repositories and discover unregistered submodules, peer repos, and VS Code workspace folders.",
421
+ inputSchema: {
422
+ type: "object",
423
+ properties: {
424
+ action: {
425
+ type: "string",
426
+ enum: ["list", "discover"],
427
+ description: 'Action to perform. "list" returns registered repos with stats + discovered repos. "discover" returns only unregistered discoveries.',
428
+ default: "list"
429
+ },
430
+ ...dirProperty
431
+ }
432
+ }
433
+ }
434
+ ]
435
+ };
436
+ });
437
+ const executeTool = async (request) => {
438
+ const { name, arguments: args } = request.params;
439
+ let activeStore;
440
+ const resolveOrFail = (a) => {
441
+ const dir = resolveDir(a);
442
+ if (!dir) return { error: 'No project directory set. Either pass a "dir" argument or start the server with --dir /path/to/project.' };
443
+ const eventBus = UiEventBus.getInstance();
444
+ if (!eventBus.mapxDir) eventBus.setMapxDir(dir);
445
+ return { dir };
446
+ };
447
+ const loadCtx = async (dir) => {
448
+ const ctx = await loadContext(dir);
449
+ if (!("error" in ctx)) activeStore = ctx.store;
450
+ return ctx;
451
+ };
452
+ try {
453
+ switch (name) {
454
+ case "mapx_scan": {
455
+ const resolved = resolveOrFail(args || {});
456
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
457
+ const dir = resolved.dir;
458
+ const ctx = await loadCtx(dir);
459
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
460
+ const excludeStr = args?.exclude;
461
+ const includeStr = args?.include;
462
+ const repo = args?.repo;
463
+ const all = !!args?.all;
464
+ const exclude = excludeStr ? excludeStr.split(",").map((s) => s.trim()) : [];
465
+ const include = includeStr ? includeStr.split(",").map((s) => s.trim()) : [];
466
+ let repoNames = void 0;
467
+ if (repo) {
468
+ repoNames = [repo];
469
+ } else if (all) {
470
+ repoNames = ["all"];
471
+ }
472
+ try {
473
+ const scanner = new Scanner(ctx.store, ctx.config, ctx.graph, void 0, { excludes: exclude, includes: include });
474
+ const result = await scanner.scanFull(repoNames);
475
+ return {
476
+ content: [{
477
+ type: "text",
478
+ text: `Scanned ${result.filesScanned} files in ${dir} (${Object.entries(result.languageBreakdown).map(([l, c]) => `${l}: ${c}`).join(", ")})
479
+ Found ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`
480
+ }]
481
+ };
482
+ } catch (err) {
483
+ return { content: [{ type: "text", text: `Scan failed: ${err.message}` }] };
484
+ }
485
+ }
486
+ case "mapx_sync": {
487
+ const resolved = resolveOrFail(args || {});
488
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
489
+ const dir = resolved.dir;
490
+ const ctx = await loadCtx(dir);
491
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
492
+ const excludeStr = args?.exclude;
493
+ const includeStr = args?.include;
494
+ const repo = args?.repo;
495
+ const all = !!args?.all;
496
+ const exclude = excludeStr ? excludeStr.split(",").map((s) => s.trim()) : [];
497
+ const include = includeStr ? includeStr.split(",").map((s) => s.trim()) : [];
498
+ let repoNames = void 0;
499
+ if (repo) {
500
+ repoNames = [repo];
501
+ } else if (all) {
502
+ repoNames = ["all"];
503
+ }
504
+ try {
505
+ const scanner = new Scanner(ctx.store, ctx.config, ctx.graph, void 0, { excludes: exclude, includes: include });
506
+ const result = await scanner.scanIncremental(repoNames);
507
+ return {
508
+ content: [{
509
+ type: "text",
510
+ text: `Updated ${result.filesScanned} files in ${dir} (${Object.entries(result.languageBreakdown).map(([l, c]) => `${l}: ${c}`).join(", ")})
511
+ ${result.symbolsFound} symbols updated, ${result.edgesFound} edges updated in ${result.durationMs}ms`
512
+ }]
513
+ };
514
+ } catch (err) {
515
+ return { content: [{ type: "text", text: `Sync failed: ${err.message}` }] };
516
+ }
517
+ }
518
+ case "mapx_query": {
519
+ const resolved = resolveOrFail(args || {});
520
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
521
+ const dir = resolved.dir;
522
+ const term = args?.term;
523
+ if (!term) return { content: [{ type: "text", text: "Missing required parameter: term" }] };
524
+ const ctx = await loadCtx(dir);
525
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
526
+ const results = ctx.store.searchSymbols(term);
527
+ if (results.length === 0) {
528
+ return { content: [{ type: "text", text: `No symbols matching "${term}" in ${dir}` }] };
529
+ }
530
+ const lines = results.map((sym) => {
531
+ const scope = sym.scope ? `${sym.scope}::` : "";
532
+ return `${sym.kind} ${scope}${sym.name}
533
+ @ ${sym.file_path}:${sym.start_line}${sym.signature && sym.signature !== sym.name ? `
534
+ signature: ${sym.signature}` : ""}`;
535
+ });
536
+ return { content: [{ type: "text", text: lines.join("\n\n") }] };
537
+ }
538
+ case "mapx_dependencies": {
539
+ const resolved = resolveOrFail(args || {});
540
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
541
+ const dir = resolved.dir;
542
+ const file = args?.file;
543
+ if (!file) return { content: [{ type: "text", text: "Missing required parameter: file" }] };
544
+ const ctx = await loadCtx(dir);
545
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
546
+ const deps = ctx.graph.getDependencies(file);
547
+ const rdeps = ctx.graph.getReverseDependencies(file);
548
+ const parts = [];
549
+ if (deps.length > 0) {
550
+ parts.push("Dependencies:");
551
+ for (const d of deps) parts.push(` \u2192 ${d.target} (${d.type})`);
552
+ }
553
+ if (rdeps.length > 0) {
554
+ parts.push("Depended on by:");
555
+ for (const r of rdeps) parts.push(` \u2190 ${r.source} (${r.type})`);
556
+ }
557
+ if (parts.length === 0) parts.push("No dependencies found");
558
+ return { content: [{ type: "text", text: parts.join("\n") }] };
559
+ }
560
+ case "mapx_export": {
561
+ const resolved = resolveOrFail(args || {});
562
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
563
+ const dir = resolved.dir;
564
+ const ctx = await loadCtx(dir);
565
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
566
+ const format = args?.format || "llm";
567
+ const tokens = args?.tokens || 8192;
568
+ const repo = args?.repo;
569
+ const excludeStr = args?.exclude;
570
+ const includeStr = args?.include;
571
+ const exclude = excludeStr ? excludeStr.split(",").map((s) => s.trim()) : [];
572
+ const include = includeStr ? includeStr.split(",").map((s) => s.trim()) : [];
573
+ const excludes = [
574
+ ...ctx.config.settings.excludePatterns ?? [],
575
+ ...exclude
576
+ ];
577
+ const includes = [
578
+ ...ctx.config.settings.includePatterns ?? [],
579
+ ...include
580
+ ];
581
+ const matcher = buildMatcher(excludes, includes);
582
+ const allFiles = ctx.store.getAllFiles(repo).map((f) => f.path);
583
+ const filteredFiles = allFiles.filter((f) => matcher(f));
584
+ const clusterMode = args?.cluster === "none" ? "none" : "auto";
585
+ const clusterDepth = args?.depth !== void 0 ? parseInt(args.depth, 10) : 3;
586
+ const fallbackGrid = !!args?.fallback_grid;
587
+ const clusterOpts = { cluster: clusterMode, depth: clusterDepth, forceFallback: fallbackGrid };
588
+ if (format === "json") {
589
+ const exporter2 = new GraphExporter(ctx.store, ctx.graph);
590
+ return { content: [{ type: "text", text: exporter2.exportAsJSONString(repo, filteredFiles) }] };
591
+ }
592
+ if (format === "dot") {
593
+ const exporter2 = new DotExporter(ctx.store, ctx.graph);
594
+ return { content: [{ type: "text", text: exporter2.export(repo, filteredFiles, clusterOpts) }] };
595
+ }
596
+ if (format === "svg") {
597
+ const exporter2 = new SvgExporter(ctx.store, ctx.graph);
598
+ return { content: [{ type: "text", text: exporter2.export(repo, filteredFiles, clusterOpts) }] };
599
+ }
600
+ const exporter = new LLMExporter(ctx.store, ctx.graph);
601
+ const output = exporter.export({ format: "llm", tokenBudget: tokens, repo, files: filteredFiles });
602
+ return { content: [{ type: "text", text: output }] };
603
+ }
604
+ case "mapx_status": {
605
+ const resolved = resolveOrFail(args || {});
606
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
607
+ const dir = resolved.dir;
608
+ const ctx = await loadCtx(dir);
609
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
610
+ const lastScan = ctx.store.getMeta("last_scan_time:" + ctx.config.repo.name) || ctx.store.getMeta("last_scan_time");
611
+ const lastCommit = ctx.store.getMeta("last_scan_commit:" + ctx.config.repo.name) || ctx.store.getMeta("last_scan_commit");
612
+ const fileCount = ctx.store.getFileCount();
613
+ const symbolCount = ctx.store.getSymbolCount();
614
+ const edgeCount = ctx.store.getEdgeCount();
615
+ const verifiedEdgeCount = ctx.store.raw.prepare("SELECT COUNT(*) as cnt FROM edges WHERE verifiability = 'verified'").get()?.cnt || 0;
616
+ const inferredEdgeCount = ctx.store.raw.prepare("SELECT COUNT(*) as cnt FROM edges WHERE verifiability = 'inferred'").get()?.cnt || 0;
617
+ const breakdown = ctx.store.getLanguageBreakdown();
618
+ const topFiles = ctx.store.getTopFilesByPageRank(ctx.graph, 5);
619
+ const topSymbols = ctx.store.getTopSymbolsByPageRank(ctx.graph, 5);
620
+ const repoRoot = resolve(dir, ctx.config.repo.path);
621
+ let isStale = false;
622
+ let gitInfo = "";
623
+ if (isGitRepo(repoRoot)) {
624
+ const changes = getChangedFiles(repoRoot, lastCommit || void 0);
625
+ if (changes.length === 0) {
626
+ gitInfo = "No changes since last scan (\u2713 index is current)";
627
+ } else {
628
+ isStale = true;
629
+ gitInfo = `${changes.length} changed files (\u26A0 stale)`;
630
+ }
631
+ } else {
632
+ gitInfo = "Not a git repository (\u2713 index is current)";
633
+ }
634
+ const recommendations = isStale ? "\u26A0 Index is stale. Run `mapx sync` or `mapx update` to bring it up to date." : "\u2713 Index is up to date.";
635
+ const textOutput = `Directory: ${dir}
636
+ Last scan: ${lastScan || "never"}
637
+ Last commit: ${lastCommit || "none"}
638
+ Files: ${fileCount} | Symbols: ${symbolCount} | Edges: ${edgeCount} (verified: ${verifiedEdgeCount}, inferred: ${inferredEdgeCount})
639
+
640
+ Language Breakdown:
641
+ ${Object.entries(breakdown).map(([l, c]) => ` ${l}: ${c} files`).join("\n")}
642
+
643
+ Top Files by PageRank:
644
+ ${topFiles.map((tf) => ` ${tf.pagerank.toFixed(6)} ${tf.path}`).join("\n")}
645
+
646
+ Top Symbols by PageRank:
647
+ ${topSymbols.map((ts) => ` ${ts.pagerank.toFixed(6)} ${ts.scope ? `${ts.scope}::` : ""}${ts.name} (${ts.filePath})`).join("\n")}
648
+
649
+ Git Status:
650
+ ${gitInfo}
651
+
652
+ Recommendation:
653
+ ${recommendations}`;
654
+ return {
655
+ content: [{
656
+ type: "text",
657
+ text: textOutput
658
+ }]
659
+ };
660
+ }
661
+ case "mapx_search": {
662
+ const resolved = resolveOrFail(args || {});
663
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
664
+ const dir = resolved.dir;
665
+ const term = args?.term;
666
+ if (!term) return { content: [{ type: "text", text: "Missing required parameter: term" }] };
667
+ const ctx = await loadCtx(dir);
668
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
669
+ const results = ctx.store.searchSymbolsFiltered({
670
+ term,
671
+ kind: args?.kind,
672
+ filePrefix: args?.file,
673
+ exact: !!args?.exact,
674
+ limit: args?.limit
675
+ });
676
+ if (results.length === 0) {
677
+ return { content: [{ type: "text", text: `No symbols matching "${term}"` }] };
678
+ }
679
+ const rankedAll = ctx.graph.getRankedSymbols();
680
+ const rankMap = /* @__PURE__ */ new Map();
681
+ for (const item of rankedAll) {
682
+ rankMap.set(`${item.filePath}::${item.name}`, item.pagerank);
683
+ }
684
+ const lines = results.map((sym) => {
685
+ const scope = sym.scope ? `${sym.scope}::` : "";
686
+ const key = `${sym.file_path}::${sym.name}`;
687
+ const pagerankVal = rankMap.get(key) || 0;
688
+ return `${sym.kind} ${scope}${sym.name} [pagerank: ${pagerankVal.toFixed(6)}]
689
+ @ ${sym.file_path}:${sym.start_line}${sym.signature && sym.signature !== sym.name ? `
690
+ signature: ${sym.signature}` : ""}`;
691
+ });
692
+ return { content: [{ type: "text", text: lines.join("\n\n") }] };
693
+ }
694
+ case "mapx_context": {
695
+ const resolved = resolveOrFail(args || {});
696
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
697
+ const dir = resolved.dir;
698
+ const task = args?.task;
699
+ if (!task) return { content: [{ type: "text", text: "Missing required parameter: task" }] };
700
+ const ctx = await loadCtx(dir);
701
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
702
+ const builder = new ContextBuilder(ctx.store, ctx.graph);
703
+ const format = args?.format || "text";
704
+ const result = await builder.buildContext({
705
+ task,
706
+ seeds: args?.seeds,
707
+ tokens: args?.tokens,
708
+ depth: args?.depth
709
+ });
710
+ if (format === "json") {
711
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
712
+ }
713
+ const mdLines = [];
714
+ mdLines.push("# Mapx smart Context");
715
+ mdLines.push(`*Estimated tokens:* ${result.estimatedTokens}
716
+ `);
717
+ mdLines.push("## Included Files");
718
+ if (result.includedFiles.length === 0) {
719
+ mdLines.push("None");
720
+ } else {
721
+ for (const f of result.includedFiles) {
722
+ mdLines.push(`### [${f.path}](file://${resolve(dir, f.path)})`);
723
+ mdLines.push(`- Language: ${f.language}`);
724
+ mdLines.push(`- Lines: ${f.lineCount} | Size: ${f.sizeBytes} bytes`);
725
+ if (f.symbols.length > 0) {
726
+ mdLines.push("- Symbols:");
727
+ for (const sym of f.symbols) {
728
+ const scopeStr = sym.scope ? `${sym.scope}::` : "";
729
+ mdLines.push(` - \`${sym.kind}\` \`${scopeStr}${sym.name}\` (lines ${sym.startLine}-${sym.endLine})`);
730
+ }
731
+ }
732
+ }
733
+ }
734
+ if (result.edges.length > 0) {
735
+ mdLines.push("\n## Cross-File Dependencies");
736
+ for (const edge of result.edges) {
737
+ const srcSym = edge.sourceSymbol ? `#${edge.sourceSymbol}` : "";
738
+ const tgtSym = edge.targetSymbol ? `#${edge.targetSymbol}` : "";
739
+ mdLines.push(`- \`${edge.sourceFile}${srcSym}\` \u2192 \`${edge.targetFile}${tgtSym}\` (${edge.edgeType})`);
740
+ }
741
+ }
742
+ if (result.excludedFiles.length > 0) {
743
+ mdLines.push("\n## Excluded Files (Token budget exhausted)");
744
+ for (const f of result.excludedFiles) {
745
+ mdLines.push(`- ${f}`);
746
+ }
747
+ }
748
+ return { content: [{ type: "text", text: mdLines.join("\n") }] };
749
+ }
750
+ case "mapx_callers": {
751
+ const resolved = resolveOrFail(args || {});
752
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
753
+ const dir = resolved.dir;
754
+ const symbolName = args?.symbol;
755
+ if (!symbolName) return { content: [{ type: "text", text: "Missing required parameter: symbol" }] };
756
+ const ctx = await loadCtx(dir);
757
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
758
+ const maxDepth = args?.depth ?? 1;
759
+ const queue = [{ symName: symbolName, depth: 0 }];
760
+ const visited = /* @__PURE__ */ new Set([symbolName]);
761
+ const results = [];
762
+ while (queue.length > 0) {
763
+ const { symName, depth } = queue.shift();
764
+ if (depth >= maxDepth) continue;
765
+ const callers = ctx.store.getCallersOfSymbol(symName);
766
+ for (const edge of callers) {
767
+ const callerName = edge.source_symbol ? `${edge.source_symbol}` : "<top-level>";
768
+ const calleeName = edge.target_symbol || symName;
769
+ const meta = edge.metadata ? JSON.parse(edge.metadata) : {};
770
+ results.push({
771
+ caller: callerName,
772
+ callee: calleeName,
773
+ file: edge.source_file,
774
+ line: meta.startLine || 1,
775
+ depth: depth + 1
776
+ });
777
+ const nextSym = edge.source_symbol;
778
+ if (nextSym && !visited.has(nextSym)) {
779
+ visited.add(nextSym);
780
+ queue.push({ symName: nextSym, depth: depth + 1 });
781
+ }
782
+ }
783
+ }
784
+ if (results.length === 0) {
785
+ return { content: [{ type: "text", text: `No callers found for "${symbolName}"` }] };
786
+ }
787
+ const lines = results.map((res) => {
788
+ const indent = " ".repeat(res.depth);
789
+ return `${indent}\u2190 ${res.caller} (calls ${res.callee})
790
+ ${indent} @ ${res.file}:${res.line}`;
791
+ });
792
+ return { content: [{ type: "text", text: `Callers of "${symbolName}":
793
+ ` + lines.join("\n") }] };
794
+ }
795
+ case "mapx_callees": {
796
+ const resolved = resolveOrFail(args || {});
797
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
798
+ const dir = resolved.dir;
799
+ const symbolName = args?.symbol;
800
+ if (!symbolName) return { content: [{ type: "text", text: "Missing required parameter: symbol" }] };
801
+ const ctx = await loadCtx(dir);
802
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
803
+ const maxDepth = args?.depth ?? 1;
804
+ const queue = [{ symName: symbolName, depth: 0 }];
805
+ const visited = /* @__PURE__ */ new Set([symbolName]);
806
+ const results = [];
807
+ while (queue.length > 0) {
808
+ const { symName, depth } = queue.shift();
809
+ if (depth >= maxDepth) continue;
810
+ const callees = ctx.store.getCalleesOfSymbol(symName);
811
+ for (const edge of callees) {
812
+ const calleeName = edge.target_symbol || "<unknown>";
813
+ const callerName = edge.source_symbol || symName;
814
+ const meta = edge.metadata ? JSON.parse(edge.metadata) : {};
815
+ results.push({
816
+ caller: callerName,
817
+ callee: calleeName,
818
+ file: edge.target_file,
819
+ line: meta.startLine || 1,
820
+ depth: depth + 1
821
+ });
822
+ if (edge.target_symbol && !visited.has(edge.target_symbol)) {
823
+ visited.add(edge.target_symbol);
824
+ queue.push({ symName: edge.target_symbol, depth: depth + 1 });
825
+ }
826
+ }
827
+ }
828
+ if (results.length === 0) {
829
+ return { content: [{ type: "text", text: `No callees found for "${symbolName}"` }] };
830
+ }
831
+ const lines = results.map((res) => {
832
+ const indent = " ".repeat(res.depth);
833
+ return `${indent}\u2192 ${res.callee} (called by ${res.caller})
834
+ ${indent} @ ${res.file}:${res.line}`;
835
+ });
836
+ return { content: [{ type: "text", text: `Callees of "${symbolName}":
837
+ ` + lines.join("\n") }] };
838
+ }
839
+ case "mapx_impact": {
840
+ const resolved = resolveOrFail(args || {});
841
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
842
+ const dir = resolved.dir;
843
+ const symbolName = args?.symbol;
844
+ if (!symbolName) return { content: [{ type: "text", text: "Missing required parameter: symbol" }] };
845
+ const ctx = await loadCtx(dir);
846
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
847
+ const maxDepth = args?.depth ?? 3;
848
+ const queue = [{ symName: symbolName, depth: 0 }];
849
+ const visited = /* @__PURE__ */ new Set([symbolName]);
850
+ const items = [];
851
+ while (queue.length > 0) {
852
+ const { symName, depth } = queue.shift();
853
+ if (depth >= maxDepth) continue;
854
+ const callers = ctx.store.getCallersOfSymbol(symName);
855
+ for (const edge of callers) {
856
+ const callerName = edge.source_symbol || "<top-level>";
857
+ const key = `${edge.source_file}::${callerName}`;
858
+ if (visited.has(key)) continue;
859
+ visited.add(key);
860
+ let risk = "LOW";
861
+ const isStructural = ["import", "require", "extends", "implements"].includes(edge.edge_type);
862
+ const curDepth = depth + 1;
863
+ if (curDepth === 1) {
864
+ risk = isStructural ? "MEDIUM" : "HIGH";
865
+ } else if (curDepth === 2) {
866
+ risk = isStructural ? "LOW" : "MEDIUM";
867
+ } else {
868
+ risk = "LOW";
869
+ }
870
+ items.push({
871
+ symbol: callerName,
872
+ file: edge.source_file,
873
+ depth: curDepth,
874
+ edgeType: edge.edge_type,
875
+ risk
876
+ });
877
+ if (edge.source_symbol) {
878
+ queue.push({ symName: edge.source_symbol, depth: curDepth });
879
+ }
880
+ }
881
+ }
882
+ let recommendation = "No callers found \u2014 safe to change";
883
+ if (items.some((x) => x.risk === "HIGH")) {
884
+ recommendation = "Treat as BREAKING CHANGE \u2014 update all HIGH-risk callers";
885
+ } else if (items.length > 0) {
886
+ recommendation = "Low blast radius \u2014 proceed with caution";
887
+ }
888
+ const outJson = {
889
+ affected: items,
890
+ summary: {
891
+ high: items.filter((x) => x.risk === "HIGH").length,
892
+ medium: items.filter((x) => x.risk === "MEDIUM").length,
893
+ low: items.filter((x) => x.risk === "LOW").length
894
+ },
895
+ recommendation
896
+ };
897
+ return { content: [{ type: "text", text: JSON.stringify(outJson, null, 2) }] };
898
+ }
899
+ case "mapx_node": {
900
+ const resolved = resolveOrFail(args || {});
901
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
902
+ const dir = resolved.dir;
903
+ const symbolName = args?.symbol;
904
+ if (!symbolName) return { content: [{ type: "text", text: "Missing required parameter: symbol" }] };
905
+ const ctx = await loadCtx(dir);
906
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
907
+ const sym = ctx.store.getSymbolByName(symbolName);
908
+ if (!sym) {
909
+ return { content: [{ type: "text", text: `Error: Symbol "${symbolName}" not found.` }] };
910
+ }
911
+ const callers = ctx.store.getCallersOfSymbol(symbolName);
912
+ const callees = ctx.store.getCalleesOfSymbol(symbolName);
913
+ let outputText = `Symbol: ${sym.scope ? `${sym.scope}::` : ""}${sym.name}
914
+ Kind: ${sym.kind}
915
+ File: ${sym.file_path}
916
+ Lines: ${sym.start_line}-${sym.end_line}
917
+ Signature: ${sym.signature}
918
+ Callers: ${callers.length}
919
+ Callees: ${callees.length}`;
920
+ if (args?.source) {
921
+ try {
922
+ const { readFileSync } = await import("node:fs");
923
+ const absolutePath = resolve(dir, sym.file_path);
924
+ const content = readFileSync(absolutePath, "utf8");
925
+ const lines = content.split("\n");
926
+ const start = sym.start_line - 1;
927
+ const end = sym.end_line;
928
+ const sliced = lines.slice(start, end).join("\n");
929
+ outputText += `
930
+
931
+ Source Code:
932
+ ----------------------------------------
933
+ ${sliced}
934
+ ----------------------------------------`;
935
+ } catch (err) {
936
+ outputText += `
937
+
938
+ Failed to read source code: ${err.message}`;
939
+ }
940
+ }
941
+ return { content: [{ type: "text", text: outputText }] };
942
+ }
943
+ case "mapx_files": {
944
+ const resolved = resolveOrFail(args || {});
945
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
946
+ const dir = resolved.dir;
947
+ const ctx = await loadCtx(dir);
948
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
949
+ const results = ctx.store.getFilesFiltered({
950
+ pathPrefix: args?.path,
951
+ lang: args?.lang,
952
+ sort: args?.sort,
953
+ limit: args?.limit
954
+ });
955
+ if (results.length === 0) {
956
+ return { content: [{ type: "text", text: "No files found matching filters." }] };
957
+ }
958
+ const outText = results.map((f) => ` ${f.path} (${f.language}, ${f.lines} lines, ${f.size_bytes} bytes)`).join("\n");
959
+ return { content: [{ type: "text", text: outText }] };
960
+ }
961
+ case "mapx_metrics": {
962
+ const resolved = resolveOrFail(args || {});
963
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
964
+ const dir = resolved.dir;
965
+ const ctx = await loadCtx(dir);
966
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
967
+ const lang = args?.lang;
968
+ const verifiedOnly = !!args?.verifiedOnly;
969
+ const metrics = calculateMetrics(ctx.store, {
970
+ repo: ctx.config.repo.name,
971
+ language: lang,
972
+ verifiedOnly
973
+ });
974
+ if (metrics.length === 0) {
975
+ return { content: [{ type: "text", text: "No metrics found." }] };
976
+ }
977
+ const lines = [
978
+ "\u2500\u2500 Coupling & Instability Metrics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
979
+ `${"File Path".padEnd(45)} | ${"Lang".padEnd(10)} | ${"Ca".padStart(4)} | ${"Ce".padStart(4)} | ${"Instability".padStart(11)}`,
980
+ "-".repeat(85)
981
+ ];
982
+ for (const m of metrics) {
983
+ const pathTrunc = m.path.length > 45 ? "..." + m.path.substring(m.path.length - 42) : m.path;
984
+ lines.push(`${pathTrunc.padEnd(45)} | ${m.language.padEnd(10)} | ${String(m.afferent).padStart(4)} | ${String(m.efferent).padStart(4)} | ${m.instability.toFixed(4).padStart(11)}`);
985
+ }
986
+ return {
987
+ content: [{
988
+ type: "text",
989
+ text: lines.join("\n")
990
+ }]
991
+ };
992
+ }
993
+ case "mapx_edges": {
994
+ const resolved = resolveOrFail(args || {});
995
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
996
+ const dir = resolved.dir;
997
+ const ctx = await loadCtx(dir);
998
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
999
+ const type = args?.type;
1000
+ const from = args?.from;
1001
+ const to = args?.to;
1002
+ const edges = ctx.store.queryEdges({
1003
+ repo: ctx.config.repo.name,
1004
+ type,
1005
+ from,
1006
+ to
1007
+ });
1008
+ if (edges.length === 0) {
1009
+ return { content: [{ type: "text", text: "No matching edges found." }] };
1010
+ }
1011
+ const lines = [`Found ${edges.length} matching edges:`];
1012
+ for (const e of edges) {
1013
+ const srcSym = e.source_symbol ? `#${e.source_symbol}` : "";
1014
+ const tgtSym = e.target_symbol ? `#${e.target_symbol}` : "";
1015
+ const infSuffix = e.verifiability === "inferred" ? " [inferred]" : "";
1016
+ lines.push(`- ${e.source_file}${srcSym} \u2192 ${e.target_file}${tgtSym} (${e.edge_type})${infSuffix}`);
1017
+ }
1018
+ return {
1019
+ content: [{
1020
+ type: "text",
1021
+ text: lines.join("\n")
1022
+ }]
1023
+ };
1024
+ }
1025
+ case "mapx_clusters": {
1026
+ const resolved = resolveOrFail(args || {});
1027
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1028
+ const dir = resolved.dir;
1029
+ const ctx = await loadCtx(dir);
1030
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
1031
+ const source = args?.source || "all";
1032
+ const clusterQuery = args?.cluster;
1033
+ const clusters = ctx.store.getClusters(ctx.config.repo.name);
1034
+ let filtered = clusters;
1035
+ if (source && source !== "all") {
1036
+ filtered = clusters.filter((c) => c.source === source);
1037
+ }
1038
+ if (clusterQuery) {
1039
+ const targetCluster = clusters.find((c) => c.name === clusterQuery);
1040
+ if (!targetCluster) {
1041
+ return { content: [{ type: "text", text: `Cluster "${clusterQuery}" not found.` }] };
1042
+ }
1043
+ const files = ctx.store.getClusterFiles(targetCluster.name, ctx.config.repo.name);
1044
+ const clusterEdges = ctx.store.getClusterEdges(targetCluster.name, ctx.config.repo.name);
1045
+ const lines2 = [];
1046
+ lines2.push(`${targetCluster.name} [${targetCluster.source}] ${targetCluster.file_count} files`);
1047
+ for (const f of files) {
1048
+ lines2.push(` ${f}`);
1049
+ }
1050
+ const dependsOn = clusterEdges.filter((e) => e.sourceCluster === targetCluster.name);
1051
+ lines2.push("\nDepends on:");
1052
+ if (dependsOn.length === 0) {
1053
+ lines2.push(" (none)");
1054
+ } else {
1055
+ for (const dep of dependsOn) {
1056
+ lines2.push(` ${dep.targetCluster.padEnd(25)} [${dep.edgeCount} edges \u2014 dominant: ${dep.dominantType}]`);
1057
+ }
1058
+ }
1059
+ const dependedOnBy = clusterEdges.filter((e) => e.targetCluster === targetCluster.name);
1060
+ lines2.push("\nDepended on by:");
1061
+ if (dependedOnBy.length === 0) {
1062
+ lines2.push(" (none)");
1063
+ } else {
1064
+ for (const dep of dependedOnBy) {
1065
+ lines2.push(` ${dep.sourceCluster.padEnd(25)} [${dep.edgeCount} edges \u2014 dominant: ${dep.dominantType}]`);
1066
+ }
1067
+ }
1068
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
1069
+ }
1070
+ const roots = [];
1071
+ const childrenMap = /* @__PURE__ */ new Map();
1072
+ for (const c of filtered) {
1073
+ if (!c.parent_name) {
1074
+ roots.push(c);
1075
+ } else {
1076
+ const parentName = c.parent_name;
1077
+ if (!childrenMap.has(parentName)) {
1078
+ childrenMap.set(parentName, []);
1079
+ }
1080
+ childrenMap.get(parentName).push(c);
1081
+ }
1082
+ }
1083
+ for (const list of childrenMap.values()) {
1084
+ list.sort((a, b) => a.name.localeCompare(b.name));
1085
+ }
1086
+ roots.sort((a, b) => a.name.localeCompare(b.name));
1087
+ const lines = [];
1088
+ const printTree = (node, indent) => {
1089
+ const padding = " ".repeat(indent);
1090
+ const namePart = node.name;
1091
+ const sourcePart = `(${node.source})`;
1092
+ const filesPart = `[${node.file_count} files]`;
1093
+ const formatted = `${padding}${namePart.padEnd(35 - indent * 2)}${sourcePart.padEnd(15)} ${filesPart}`;
1094
+ lines.push(formatted);
1095
+ const children = childrenMap.get(node.name) || [];
1096
+ for (const child of children) {
1097
+ printTree(child, indent + 1);
1098
+ }
1099
+ };
1100
+ for (const root of roots) {
1101
+ printTree(root, 0);
1102
+ }
1103
+ const nsCount = filtered.filter((c) => c.source === "namespace").length;
1104
+ const dirCount = filtered.filter((c) => c.source === "directory").length;
1105
+ const commCount = filtered.filter((c) => c.source === "community").length;
1106
+ lines.push(`
1107
+ ${filtered.length} clusters detected (${nsCount} namespace, ${dirCount} directory, ${commCount} community)`);
1108
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1109
+ }
1110
+ case "mapx_trace": {
1111
+ const resolved = resolveOrFail(args || {});
1112
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1113
+ const dir = resolved.dir;
1114
+ const ctx = await loadCtx(dir);
1115
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
1116
+ const start = args?.start;
1117
+ const direction = args?.direction || "both";
1118
+ const depth = args?.depth || 3;
1119
+ const format = args?.format || "text";
1120
+ const includeStructural = args?.include_structural || false;
1121
+ if (!start) {
1122
+ return { content: [{ type: "text", text: 'Error: "start" argument is required.' }] };
1123
+ }
1124
+ const tracer = new FlowTracer(ctx.store);
1125
+ const result = tracer.trace({
1126
+ startSymbol: start,
1127
+ direction,
1128
+ maxDepth: depth,
1129
+ includeStructural,
1130
+ repo: ctx.config.repo.name
1131
+ });
1132
+ if (format === "json") {
1133
+ const jsonOutput = {
1134
+ start: result.start,
1135
+ direction: result.direction,
1136
+ maxDepth: depth,
1137
+ nodeCount: result.nodeCount,
1138
+ edgeCount: result.edgeCount,
1139
+ maxDepthReached: result.maxDepthReached,
1140
+ sources: result.sources.map((s) => ({ file: s.file, symbol: s.symbol })),
1141
+ sinks: result.sinks.map((s) => ({ file: s.file, symbol: s.symbol })),
1142
+ cycles: result.cycles,
1143
+ nodes: Array.from(new Map(result.paths.flatMap((p) => p.nodes).map((n) => [`${n.file}::${n.symbol || ""}`, n])).values()).map((n) => ({
1144
+ file: n.file,
1145
+ symbol: n.symbol,
1146
+ depth: n.depth,
1147
+ incomingEdgeType: n.incomingEdgeType
1148
+ })),
1149
+ edges: Array.from(new Set(result.paths.flatMap((p) => {
1150
+ const arr = [];
1151
+ for (let i = 1; i < p.nodes.length; i++) {
1152
+ arr.push(JSON.stringify({
1153
+ from: p.nodes[i - 1].file,
1154
+ to: p.nodes[i].file,
1155
+ edgeType: p.nodes[i].incomingEdgeType,
1156
+ fromSymbol: p.nodes[i - 1].symbol,
1157
+ toSymbol: p.nodes[i].symbol
1158
+ }));
1159
+ }
1160
+ return arr;
1161
+ }))).map((s) => JSON.parse(s))
1162
+ };
1163
+ return { content: [{ type: "text", text: JSON.stringify(jsonOutput, null, 2) }] };
1164
+ }
1165
+ if (format === "dot") {
1166
+ const lines2 = [];
1167
+ const safeStartName = (result.start.symbol || result.start.file).replace(/[^a-zA-Z0-9]/g, "_");
1168
+ lines2.push(`digraph Trace_${safeStartName} {`);
1169
+ lines2.push(" rankdir=TB;");
1170
+ lines2.push(` label="Trace: ${result.start.symbol || result.start.file} (${result.direction}stream, depth\u2264${depth})";`);
1171
+ lines2.push(" fontsize=12;");
1172
+ lines2.push(" node [shape=box, style=filled, fontsize=10];");
1173
+ lines2.push("");
1174
+ const uniqueNodes = /* @__PURE__ */ new Map();
1175
+ const edgesSet = /* @__PURE__ */ new Set();
1176
+ for (const p of result.paths) {
1177
+ for (let i = 0; i < p.nodes.length; i++) {
1178
+ const n = p.nodes[i];
1179
+ const key = `${n.file}::${n.symbol || ""}`;
1180
+ if (!uniqueNodes.has(key)) {
1181
+ let shape = "box";
1182
+ let color = "#E8F4FD";
1183
+ const isStart = n.file === result.start.file && n.symbol === result.start.symbol;
1184
+ const isSink = result.sinks.some((s) => s.file === n.file && s.symbol === n.symbol);
1185
+ const isSource = result.sources.some((s) => s.file === n.file && s.symbol === n.symbol);
1186
+ if (isStart) {
1187
+ shape = "diamond";
1188
+ color = "#FFE0B2";
1189
+ } else if (isSink) {
1190
+ shape = "octagon";
1191
+ color = "#FFEBEE";
1192
+ } else if (isSource) {
1193
+ shape = "ellipse";
1194
+ color = "#E8F5E9";
1195
+ }
1196
+ uniqueNodes.set(key, { file: n.file, symbol: n.symbol, shape, color });
1197
+ }
1198
+ if (i > 0) {
1199
+ const fromNode = p.nodes[i - 1];
1200
+ const toNode = p.nodes[i];
1201
+ edgesSet.add(JSON.stringify({
1202
+ from: `${fromNode.file}::${fromNode.symbol || ""}`,
1203
+ to: `${toNode.file}::${toNode.symbol || ""}`,
1204
+ type: toNode.incomingEdgeType
1205
+ }));
1206
+ }
1207
+ }
1208
+ }
1209
+ for (const [key, n] of uniqueNodes.entries()) {
1210
+ const label = n.symbol || n.file.split("/").pop() || n.file;
1211
+ lines2.push(` "${key}" [label="${label}", fillcolor="${n.color}", shape=${n.shape}];`);
1212
+ }
1213
+ lines2.push("");
1214
+ for (const edgeStr of edgesSet) {
1215
+ const e = JSON.parse(edgeStr);
1216
+ lines2.push(` "${e.from}" -> "${e.to}" [label="${e.type}"];`);
1217
+ }
1218
+ lines2.push("}");
1219
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
1220
+ }
1221
+ const dirSymbol = result.direction === "down" ? "\u2193 downstream" : result.direction === "up" ? "\u2191 upstream" : "\u2195 bidirectional";
1222
+ const lines = [`Trace: ${start} ${dirSymbol} depth\u2264${depth}`];
1223
+ lines.push("\u2500".repeat(53));
1224
+ lines.push("");
1225
+ const printNode = (node, indentLevel) => {
1226
+ const indent = " ".repeat(indentLevel);
1227
+ const prefix = indentLevel === 0 ? "" : `\u2514\u2500[${node.incomingEdgeType}]\u2500\u2192 `;
1228
+ const displayName = node.symbol || node.file;
1229
+ const filePart = node.symbol ? ` (${node.file})` : "";
1230
+ const isSink = result.sinks.some((s) => s.file === node.file && s.symbol === node.symbol);
1231
+ const sinkStr = isSink ? " \u2297 sink" : "";
1232
+ const cycle = result.cycles.find((c) => c.fromFile === node.file && c.fromSymbol === node.symbol);
1233
+ const cycleStr = cycle ? " \u21BB cycle" : "";
1234
+ lines.push(`${indent}${prefix}${displayName}${filePart}${sinkStr}${cycleStr}`);
1235
+ if (!cycle) {
1236
+ const children = [];
1237
+ const seenChildKeys = /* @__PURE__ */ new Set();
1238
+ for (const path of result.paths) {
1239
+ const idx = path.nodes.findIndex((n) => n.file === node.file && n.symbol === node.symbol && n.depth === node.depth);
1240
+ if (idx !== -1 && idx + 1 < path.nodes.length) {
1241
+ const nextNode = path.nodes[idx + 1];
1242
+ const key = `${nextNode.file}::${nextNode.symbol || ""}::${nextNode.depth}`;
1243
+ if (!seenChildKeys.has(key)) {
1244
+ seenChildKeys.add(key);
1245
+ children.push(nextNode);
1246
+ }
1247
+ }
1248
+ }
1249
+ for (const child of children) {
1250
+ printNode(child, indentLevel + 1);
1251
+ }
1252
+ }
1253
+ };
1254
+ const startNode = {
1255
+ file: result.start.file,
1256
+ symbol: result.start.symbol,
1257
+ depth: 0,
1258
+ incomingEdgeType: "start"
1259
+ };
1260
+ printNode(startNode, 0);
1261
+ lines.push("");
1262
+ const cyclesStr = result.cycles.length > 0 ? ` Cycles: ${result.cycles.length}` : "";
1263
+ lines.push(`Nodes: ${result.nodeCount} Edges: ${result.edgeCount} Max depth: ${depth}${cyclesStr}`);
1264
+ if (result.sinks.length > 0) {
1265
+ const sinkNames = result.sinks.map((s) => s.symbol || s.file.split("/").pop() || s.file);
1266
+ lines.push(`Sinks: ${sinkNames.join(", ")}`);
1267
+ }
1268
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1269
+ }
1270
+ case "mapx_sources": {
1271
+ const resolved = resolveOrFail(args || {});
1272
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1273
+ const dir = resolved.dir;
1274
+ const ctx = await loadCtx(dir);
1275
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
1276
+ const tracer = new FlowTracer(ctx.store);
1277
+ const sources = tracer.findSources(ctx.config.repo.name);
1278
+ const lines = [`Entry points (data sources) \u2014 ${sources.length} found:`];
1279
+ for (const s of sources) {
1280
+ let extra = "[no incoming data edges]";
1281
+ if (s.file.includes("routes/")) {
1282
+ const routes = ctx.store.getEdgesForFile(s.file).filter((e) => e.edge_type === "route");
1283
+ extra = `[route file \u2014 ${routes.length} controller endpoints]`;
1284
+ } else if (s.file.includes("app/Jobs/")) {
1285
+ extra = "[dispatched externally \u2014 queue worker]";
1286
+ } else if (s.file.includes("app/Listeners/")) {
1287
+ extra = "[event listener \u2014 external trigger]";
1288
+ } else if (s.file.includes("app/Http/Middleware/")) {
1289
+ extra = "[middleware \u2014 filter chain entry]";
1290
+ }
1291
+ lines.push(` ${s.file.padEnd(40)} ${extra}`);
1292
+ }
1293
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1294
+ }
1295
+ case "mapx_sinks": {
1296
+ const resolved = resolveOrFail(args || {});
1297
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1298
+ const dir = resolved.dir;
1299
+ const ctx = await loadCtx(dir);
1300
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
1301
+ const tracer = new FlowTracer(ctx.store);
1302
+ const sinks = tracer.findSinks(ctx.config.repo.name);
1303
+ const lines = [`Terminal consumers (data sinks) \u2014 ${sinks.length} found:`];
1304
+ for (const s of sinks) {
1305
+ const inEdges = ctx.store.getReverseEdges(s.file).filter((e) => [
1306
+ "call",
1307
+ "instantiation",
1308
+ "param_type",
1309
+ "return_type",
1310
+ "relation",
1311
+ "dispatch",
1312
+ "notify",
1313
+ "route"
1314
+ ].includes(e.edge_type));
1315
+ let extra = `[terminal \u2014 no outgoing data edges]`;
1316
+ if (s.file.includes("DatabaseManager") || s.file.includes("database")) {
1317
+ extra = `[DB facade \u2192 raw SQL \u2014 ${inEdges.length} in-edges]`;
1318
+ } else if (s.file.includes("CacheManager") || s.file.includes("cache")) {
1319
+ extra = `[Cache facade \u2192 Redis/Memcache \u2014 ${inEdges.length} in-edges]`;
1320
+ } else if (s.file.includes("Mailer") || s.file.includes("mail")) {
1321
+ extra = `[Mail facade \u2192 SMTP \u2014 ${inEdges.length} in-edges]`;
1322
+ } else if (s.file.includes("QueueManager") || s.file.includes("queue")) {
1323
+ extra = `[Queue::push \u2014 ${inEdges.length} in-edges]`;
1324
+ }
1325
+ lines.push(` ${s.file.padEnd(40)} ${extra}`);
1326
+ }
1327
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1328
+ }
1329
+ case "mapx_agents_generate": {
1330
+ const resolved = resolveOrFail(args || {});
1331
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1332
+ const dir = resolved.dir;
1333
+ const generator = new AgentGenerator();
1334
+ const available = generator.listProviders();
1335
+ let targetProviders = [];
1336
+ const requestArgs = args || {};
1337
+ if (requestArgs.all) {
1338
+ targetProviders = available;
1339
+ } else if (requestArgs.providers && Array.isArray(requestArgs.providers)) {
1340
+ targetProviders = requestArgs.providers.filter((p) => available.includes(p));
1341
+ } else {
1342
+ targetProviders = ["generic"];
1343
+ }
1344
+ if (targetProviders.length === 0) {
1345
+ return { content: [{ type: "text", text: "No valid providers specified." }] };
1346
+ }
1347
+ const actions = generator.plan(targetProviders, { dir });
1348
+ const results = [];
1349
+ for (const action of actions) {
1350
+ if (action.status === "up_to_date") {
1351
+ results.push(`${action.filename}: Up to date.`);
1352
+ } else {
1353
+ generator.execute(action);
1354
+ results.push(`${action.filename}: Successfully generated/updated (${action.status}).`);
1355
+ }
1356
+ }
1357
+ return { content: [{ type: "text", text: results.join("\n") }] };
1358
+ }
1359
+ case "mapx_routes": {
1360
+ const resolved = resolveOrFail(args || {});
1361
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1362
+ const dir = resolved.dir;
1363
+ const routeRegistry = new RouteRegistry();
1364
+ await routeRegistry.load(dir);
1365
+ const routes = routeRegistry.queryRoutes({
1366
+ framework: args?.framework,
1367
+ method: args?.method,
1368
+ path: args?.pathPattern
1369
+ });
1370
+ return {
1371
+ content: [{
1372
+ type: "text",
1373
+ text: JSON.stringify(routes, null, 2)
1374
+ }]
1375
+ };
1376
+ }
1377
+ case "mapx_hooks": {
1378
+ const resolved = resolveOrFail(args || {});
1379
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1380
+ const dir = resolved.dir;
1381
+ const routeRegistry = new RouteRegistry();
1382
+ await routeRegistry.load(dir);
1383
+ const hooks = routeRegistry.queryHooks({
1384
+ framework: args?.framework,
1385
+ hookType: args?.type,
1386
+ hookName: args?.namePattern
1387
+ });
1388
+ return {
1389
+ content: [{
1390
+ type: "text",
1391
+ text: JSON.stringify(hooks, null, 2)
1392
+ }]
1393
+ };
1394
+ }
1395
+ case "mapx_lang_list": {
1396
+ const langs = getBuiltinLanguages();
1397
+ const listStr = Object.entries(langs).map(([name2, def]) => {
1398
+ const installed = isLanguageInstalled(name2) ? "Installed" : "Not Installed";
1399
+ return `- ${name2} (${def.extensions.join(", ")} | tier: ${def.tier} | status: ${installed})`;
1400
+ }).join("\n");
1401
+ return { content: [{ type: "text", text: `Supported languages:
1402
+ ${listStr}` }] };
1403
+ }
1404
+ case "mapx_lang_install": {
1405
+ const { lang } = args;
1406
+ if (!lang) {
1407
+ return { content: [{ type: "text", text: 'Error: Missing "lang" parameter.' }] };
1408
+ }
1409
+ try {
1410
+ await installLanguage(lang);
1411
+ return { content: [{ type: "text", text: `Successfully installed language '${lang}'.` }] };
1412
+ } catch (err) {
1413
+ return { content: [{ type: "text", text: `Error installing language '${lang}': ${err.message}` }] };
1414
+ }
1415
+ }
1416
+ case "mapx_lang_uninstall": {
1417
+ const { lang } = args;
1418
+ if (!lang) {
1419
+ return { content: [{ type: "text", text: 'Error: Missing "lang" parameter.' }] };
1420
+ }
1421
+ try {
1422
+ await uninstallLanguage(lang);
1423
+ return { content: [{ type: "text", text: `Successfully uninstalled language '${lang}'.` }] };
1424
+ } catch (err) {
1425
+ return { content: [{ type: "text", text: `Error uninstalling language '${lang}': ${err.message}` }] };
1426
+ }
1427
+ }
1428
+ case "mapx_workspaces": {
1429
+ const resolved = resolveOrFail(args || {});
1430
+ if ("error" in resolved) return { content: [{ type: "text", text: resolved.error }] };
1431
+ const dir = resolved.dir;
1432
+ const ctx = await loadCtx(dir);
1433
+ if ("error" in ctx) return { content: [{ type: "text", text: ctx.error }] };
1434
+ const action = args?.action || "list";
1435
+ const registeredPaths = /* @__PURE__ */ new Set();
1436
+ for (const r of ctx.config.repos) {
1437
+ registeredPaths.add(resolve(dir, r.path));
1438
+ }
1439
+ const submodules = WorkspaceManager.discoverSubmodules(dir);
1440
+ const peers = WorkspaceManager.discoverPeerRepos(dir);
1441
+ const { readdirSync } = await import("node:fs");
1442
+ const wsFiles = readdirSync(dir).filter((f) => f.endsWith(".code-workspace"));
1443
+ const { join: pathJoin } = await import("node:path");
1444
+ const vscodeFolders = [];
1445
+ for (const f of wsFiles) {
1446
+ const wsFolderRepos = WorkspaceManager.discoverVSCodeWorkspace(pathJoin(dir, f), dir);
1447
+ for (const p of wsFolderRepos) {
1448
+ if (!registeredPaths.has(resolve(dir, p.path))) {
1449
+ vscodeFolders.push({ name: p.name, path: p.path, source: "vscode-workspace", isInitialized: p.isInitialized });
1450
+ }
1451
+ }
1452
+ }
1453
+ const discovered = [];
1454
+ for (const s of submodules) {
1455
+ if (!registeredPaths.has(resolve(dir, s.path))) {
1456
+ discovered.push({ name: s.name, path: s.path, source: "submodule", isInitialized: s.isInitialized });
1457
+ }
1458
+ }
1459
+ for (const p of peers) {
1460
+ if (!registeredPaths.has(resolve(dir, p.path))) {
1461
+ discovered.push({ name: p.name, path: p.path, source: "peer", isInitialized: p.isInitialized });
1462
+ }
1463
+ }
1464
+ discovered.push(...vscodeFolders);
1465
+ if (action === "discover") {
1466
+ return {
1467
+ content: [{
1468
+ type: "text",
1469
+ text: JSON.stringify({ discovered }, null, 2)
1470
+ }]
1471
+ };
1472
+ }
1473
+ const repos = [];
1474
+ for (const r of ctx.config.repos) {
1475
+ const fileCount = ctx.store.getFileCount(r.name);
1476
+ const symbolCount = ctx.store.getSymbolCount(r.name);
1477
+ const edgeCount = ctx.store.getEdgeCount(r.name);
1478
+ const crossRepoEdges = ctx.store.raw.prepare(
1479
+ `SELECT COUNT(*) as cnt FROM edges WHERE repo = ? AND target_repo IS NOT NULL AND target_repo != ?`
1480
+ ).get(r.name, r.name);
1481
+ const lastScanned = ctx.store.getMeta("last_scan_time:" + r.name) || ctx.store.getMeta("last_scan_time") || null;
1482
+ repos.push({
1483
+ name: r.name,
1484
+ path: r.path,
1485
+ type: ctx.config.repo.name === r.name ? "primary" : "peer",
1486
+ fileCount,
1487
+ symbolCount,
1488
+ edgeCount,
1489
+ crossRepoEdgeCount: crossRepoEdges?.cnt || 0,
1490
+ lastScanned
1491
+ });
1492
+ }
1493
+ return {
1494
+ content: [{
1495
+ type: "text",
1496
+ text: JSON.stringify({ repos, discovered }, null, 2)
1497
+ }]
1498
+ };
1499
+ }
1500
+ default:
1501
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
1502
+ }
1503
+ } finally {
1504
+ try {
1505
+ activeStore?.close();
1506
+ } catch {
1507
+ }
1508
+ }
1509
+ };
1510
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1511
+ const startTime = Date.now();
1512
+ if (options?.debug) {
1513
+ process.stderr.write(`[mapx debug] Received tool call: ${request.params.name} with arguments: ${JSON.stringify(request.params.arguments || {})}
1514
+ `);
1515
+ }
1516
+ let result;
1517
+ let error;
1518
+ try {
1519
+ result = await executeTool(request);
1520
+ return result;
1521
+ } catch (e) {
1522
+ error = e;
1523
+ throw e;
1524
+ } finally {
1525
+ let isSuccess = !error;
1526
+ let errorMsg = error?.message;
1527
+ if (result && Array.isArray(result.content)) {
1528
+ const textContent = result.content.find((c) => c.type === "text")?.text;
1529
+ if (textContent && (textContent.startsWith("Error") || textContent.includes("failed"))) {
1530
+ isSuccess = false;
1531
+ errorMsg = textContent;
1532
+ }
1533
+ }
1534
+ const durationMs = Date.now() - startTime;
1535
+ if (options?.debug) {
1536
+ process.stderr.write(`[mapx debug] Completed tool call: ${request.params.name} in ${durationMs}ms (success: ${isSuccess})${errorMsg ? `, error: ${errorMsg}` : ""}
1537
+ `);
1538
+ }
1539
+ const eventBus = UiEventBus.getInstance();
1540
+ eventBus.emitToolCall({
1541
+ tool: request.params.name,
1542
+ input: request.params.arguments || {},
1543
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1544
+ durationMs,
1545
+ success: isSuccess,
1546
+ error: errorMsg
1547
+ });
1548
+ }
1549
+ });
1550
+ return server;
1551
+ }
1552
+ function generateConfigs(dir, transport, port) {
1553
+ const binPath = resolve(dir, "node_modules", ".bin", "mapx");
1554
+ const hasBin = existsSync(binPath);
1555
+ const cmd = hasBin ? "mapx" : `npx tsx ${resolve(dir, "src", "main.ts")}`;
1556
+ const lines = [
1557
+ "",
1558
+ " Mapx MCP server ready.",
1559
+ ""
1560
+ ];
1561
+ if (transport === "sse" && port) {
1562
+ lines.push(
1563
+ ` Transport: SSE (HTTP)`,
1564
+ ` URL: http://localhost:${port}/sse`,
1565
+ ` Messages: POST http://localhost:${port}/messages?sessionId=<id>`,
1566
+ ` Project dir: ${dir}`,
1567
+ "",
1568
+ " Claude Desktop (claude_desktop_config.json):",
1569
+ " ```json",
1570
+ " {",
1571
+ ' "mcpServers": {',
1572
+ ' "mapx": {',
1573
+ ` "url": "http://localhost:${port}/sse"`,
1574
+ " }",
1575
+ " }",
1576
+ " }",
1577
+ " ```",
1578
+ "",
1579
+ " Cursor / VS Code (.cursor/mcp.json or settings.json):",
1580
+ " ```json",
1581
+ " {",
1582
+ ' "mcp": {',
1583
+ ' "servers": {',
1584
+ ' "mapx": {',
1585
+ ` "url": "http://localhost:${port}/sse"`,
1586
+ " }",
1587
+ " }",
1588
+ " }",
1589
+ " }",
1590
+ " ```"
1591
+ );
1592
+ } else {
1593
+ lines.push(
1594
+ ` Transport: stdio`,
1595
+ ` Project dir: ${dir}`,
1596
+ "",
1597
+ " Claude Desktop (claude_desktop_config.json):",
1598
+ " ```json",
1599
+ " {",
1600
+ ' "mcpServers": {',
1601
+ ' "mapx": {',
1602
+ ` "command": "${cmd}",`,
1603
+ ` "args": ["serve", "--dir", "${dir}"]`,
1604
+ " }",
1605
+ " }",
1606
+ " }",
1607
+ " ```",
1608
+ "",
1609
+ " Cursor / VS Code (.cursor/mcp.json or settings.json):",
1610
+ " ```json",
1611
+ " {",
1612
+ ' "mcp": {',
1613
+ ' "servers": {',
1614
+ ' "mapx": {',
1615
+ ` "command": "${cmd}",`,
1616
+ ` "args": ["serve", "--dir", "${dir}"]`,
1617
+ " }",
1618
+ " }",
1619
+ " }",
1620
+ " }",
1621
+ " ```"
1622
+ );
1623
+ }
1624
+ lines.push(
1625
+ "",
1626
+ " Available tools: mapx_scan, mapx_sync, mapx_query, mapx_dependencies, mapx_export, mapx_status, mapx_metrics, mapx_edges,",
1627
+ " mapx_clusters, mapx_trace, mapx_sources, mapx_sinks, mapx_search, mapx_context, mapx_callers, mapx_callees,",
1628
+ " mapx_impact, mapx_node, mapx_files, mapx_routes, mapx_hooks, mapx_workspaces, mapx_agents_generate,",
1629
+ " mapx_lang_list, mapx_lang_install, mapx_lang_uninstall",
1630
+ ""
1631
+ );
1632
+ return lines.join("\n");
1633
+ }
1634
+ async function startMcpServer(dir, options) {
1635
+ if (dir) {
1636
+ defaultDir = resolve(dir);
1637
+ } else {
1638
+ const cwd = process.cwd();
1639
+ if (existsSync(resolve(cwd, ".mapx", "config.json"))) {
1640
+ defaultDir = cwd;
1641
+ }
1642
+ }
1643
+ if (defaultDir) {
1644
+ process.stderr.write(`[mapx] Default project directory: ${defaultDir}
1645
+ `);
1646
+ UiEventBus.getInstance().setMapxDir(defaultDir);
1647
+ } else {
1648
+ process.stderr.write(`[mapx] No default project directory set. Pass --dir /path/to/project or include "dir" in each tool call.
1649
+ `);
1650
+ }
1651
+ if (options?.sse) {
1652
+ const port = options.port || 3e3;
1653
+ const transports = /* @__PURE__ */ new Map();
1654
+ const httpServer = createServer(async (req, res) => {
1655
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
1656
+ if (req.method === "GET" && url.pathname === "/sse") {
1657
+ const transport = new SSEServerTransport("/messages", res);
1658
+ transports.set(transport.sessionId, transport);
1659
+ const server = buildServer(options);
1660
+ await server.connect(transport);
1661
+ res.on("close", () => {
1662
+ transports.delete(transport.sessionId);
1663
+ });
1664
+ return;
1665
+ }
1666
+ if (req.method === "POST" && url.pathname === "/messages") {
1667
+ const sessionId = url.searchParams.get("sessionId");
1668
+ if (!sessionId) {
1669
+ res.writeHead(400, { "Content-Type": "application/json" });
1670
+ res.end(JSON.stringify({ error: "Missing sessionId parameter" }));
1671
+ return;
1672
+ }
1673
+ const transport = transports.get(sessionId);
1674
+ if (!transport) {
1675
+ res.writeHead(404, { "Content-Type": "application/json" });
1676
+ res.end(JSON.stringify({ error: "Session not found. Connect via GET /sse first." }));
1677
+ return;
1678
+ }
1679
+ await transport.handlePostMessage(req, res);
1680
+ return;
1681
+ }
1682
+ res.writeHead(404);
1683
+ res.end("Not found");
1684
+ });
1685
+ await new Promise((resolve2) => {
1686
+ httpServer.listen(port, () => resolve2());
1687
+ });
1688
+ console.error(generateConfigs(defaultDir, "sse", port));
1689
+ } else {
1690
+ const server = buildServer(options);
1691
+ const transport = new StdioServerTransport();
1692
+ await server.connect(transport);
1693
+ console.error(generateConfigs(defaultDir, "stdio"));
1694
+ }
1695
+ }
1696
+ export {
1697
+ buildServer,
1698
+ startMcpServer
1699
+ };