@flowatlas/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,859 @@
1
+ import {
2
+ SCHEMA_VERSION,
3
+ loadConfig,
4
+ normalizePath,
5
+ openGraphDb,
6
+ pathAnswers
7
+ } from "./chunk-QFF3SJXQ.js";
8
+
9
+ // ../mcp/dist/server.js
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+
13
+ // ../mcp/dist/query/db.js
14
+ import { existsSync, statSync } from "node:fs";
15
+ import { isAbsolute, resolve } from "node:path";
16
+ var DbHandle = class {
17
+ path;
18
+ config;
19
+ #repoDirs = /* @__PURE__ */ new Map();
20
+ #db;
21
+ #openedAt = 0;
22
+ constructor(options = {}) {
23
+ let dbPath = options.dbPath;
24
+ if (options.configPath !== void 0) {
25
+ const loaded = loadConfig(options.configPath);
26
+ this.config = loaded.config;
27
+ for (const service of loaded.config.services) {
28
+ this.#repoDirs.set(service.name, loaded.repoDir(service));
29
+ }
30
+ dbPath ??= resolve(loaded.outputDir, "graph.db");
31
+ }
32
+ if (dbPath === void 0)
33
+ throw new Error("neither a database nor a configuration was given");
34
+ this.path = isAbsolute(dbPath) ? dbPath : resolve(dbPath);
35
+ }
36
+ repoDir(service) {
37
+ return this.#repoDirs.get(service);
38
+ }
39
+ /**
40
+ * The database, or a sentence saying why there is none.
41
+ *
42
+ * A missing or stale file is an ordinary answer rather than a crash: the
43
+ * server stays up so the next question can be asked after a rebuild.
44
+ */
45
+ open() {
46
+ if (!existsSync(this.path)) {
47
+ this.#close();
48
+ return { error: `graph.db not found at ${this.path}; run flowatlas build` };
49
+ }
50
+ const modified = statSync(this.path).mtimeMs;
51
+ if (this.#db !== void 0 && modified !== this.#openedAt)
52
+ this.#close();
53
+ if (this.#db === void 0) {
54
+ try {
55
+ this.#db = openGraphDb(this.path, { readonly: true });
56
+ this.#openedAt = modified;
57
+ } catch (cause) {
58
+ return { error: `cannot read ${this.path}: ${cause instanceof Error ? cause.message : String(cause)}` };
59
+ }
60
+ const found = this.#db.schemaVersion();
61
+ if (found !== SCHEMA_VERSION) {
62
+ this.#close();
63
+ return { error: `schema-mismatch ${found} vs ${SCHEMA_VERSION}; run flowatlas build` };
64
+ }
65
+ }
66
+ return { db: this.#db };
67
+ }
68
+ #close() {
69
+ this.#db?.close();
70
+ this.#db = void 0;
71
+ this.#openedAt = 0;
72
+ }
73
+ close() {
74
+ this.#close();
75
+ }
76
+ };
77
+
78
+ // ../mcp/dist/tools/channels.js
79
+ import { z as z2 } from "zod";
80
+
81
+ // ../mcp/dist/query/types.js
82
+ var truncationMessage = (count, exact) => `${exact ? "" : "\u2265"}${count} more nodes, increase depth or narrow scope`;
83
+
84
+ // ../mcp/dist/query/detail.js
85
+ var MAX_DETAIL = 2;
86
+ var CLAMP_NOTE = "detail clamped to 2; use get_source";
87
+ var clampDetail = (level) => level > MAX_DETAIL ? MAX_DETAIL : level;
88
+ var projectDetail = (node, level) => {
89
+ const compact = { id: node.id, type: node.type, label: node.label };
90
+ if (level <= 0)
91
+ return compact;
92
+ if (node.file !== void 0) {
93
+ compact.loc = node.line === void 0 ? node.file : `${node.file}:${node.line}`;
94
+ }
95
+ if (node.repo !== "")
96
+ compact.service = node.repo;
97
+ if (node.kind !== void 0)
98
+ compact.kind = node.kind;
99
+ if (level <= 1)
100
+ return compact;
101
+ if (node.meta !== void 0 && Object.keys(node.meta).length > 0)
102
+ compact.meta = node.meta;
103
+ return compact;
104
+ };
105
+ var projectEdge = (edge, level) => {
106
+ const projected = { type: edge.type, confidence: edge.confidence };
107
+ if (level <= 0)
108
+ return projected;
109
+ if (edge.params !== void 0 && edge.params.length > 0)
110
+ projected.params = [...edge.params];
111
+ if (edge.returns !== void 0)
112
+ projected.returns = edge.returns;
113
+ return projected;
114
+ };
115
+ var truncate = (items, maxNodes) => {
116
+ if (items.length <= maxNodes)
117
+ return { items: [...items] };
118
+ return {
119
+ items: items.slice(0, maxNodes),
120
+ truncated: truncationMessage(items.length - maxNodes, true)
121
+ };
122
+ };
123
+
124
+ // ../mcp/dist/tools/common.js
125
+ import { z } from "zod";
126
+ var commonInput = {
127
+ detail: z.number().int().min(0).max(3).default(1).describe("0 id only, 1 adds location, 2 adds metadata, 3 is clamped to 2 (use get_source)"),
128
+ maxNodes: z.number().int().positive().max(2e3).default(150).describe("most nodes to return")
129
+ };
130
+ var respond = (value) => ({
131
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
132
+ });
133
+ var bound = (input) => {
134
+ const asked = input.detail;
135
+ const detail = clampDetail(asked);
136
+ return {
137
+ detail,
138
+ maxNodes: input.maxNodes,
139
+ ...detail === asked ? {} : { note: CLAMP_NOTE }
140
+ };
141
+ };
142
+ var withDb = (handle, run) => {
143
+ const opened = handle.open();
144
+ if ("error" in opened)
145
+ return respond({ error: opened.error });
146
+ try {
147
+ return respond(run(opened.db));
148
+ } catch (cause) {
149
+ return respond({ error: cause instanceof Error ? cause.message : String(cause) });
150
+ }
151
+ };
152
+
153
+ // ../mcp/dist/tools/channels.js
154
+ var channelId = (ref) => {
155
+ const trimmed = ref.trim();
156
+ return trimmed.startsWith("channel:") ? trimmed : `channel:${trimmed}`;
157
+ };
158
+ var registerChannels = (server, ctx) => {
159
+ const ends = (name, title, description, key) => {
160
+ server.registerTool(name, {
161
+ title,
162
+ description,
163
+ inputSchema: {
164
+ channel: z2.string().describe('a channel name, with or without the "channel:" prefix'),
165
+ ...commonInput
166
+ }
167
+ }, (input) => withDb(ctx.handle, (db) => {
168
+ const { detail, maxNodes, note } = bound(input);
169
+ const id = channelId(input.channel);
170
+ const channel = db.node(id);
171
+ if (channel === void 0)
172
+ return { error: `no channel named ${JSON.stringify(id)}` };
173
+ const edges = key === "producers" ? db.edgesTo(id, ["emits"]) : db.edgesFrom(id, ["consumes"]);
174
+ const nodes = edges.map((edge) => db.node(key === "producers" ? edge.from : edge.to)).filter((node) => node !== void 0).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
175
+ const { items, truncated } = truncate(nodes, maxNodes);
176
+ return {
177
+ channel: projectDetail(channel, detail),
178
+ [key]: items.map((node) => projectDetail(node, detail)),
179
+ ...truncated === void 0 ? {} : { truncated },
180
+ ...note === void 0 ? {} : { note }
181
+ };
182
+ }));
183
+ };
184
+ ends("who_emits", "Who publishes to a channel", "Everything that publishes a message on a channel, across every repository.", "producers");
185
+ ends("who_consumes", "Who handles a channel", "Everything that handles messages from a channel, across every repository. An empty list means nothing handles it.", "consumers");
186
+ };
187
+
188
+ // ../mcp/dist/tools/contract.js
189
+ import { z as z3 } from "zod";
190
+ var isTypeId = (value) => value?.startsWith("type:") === true;
191
+ var classify = (db, left, right) => {
192
+ const both = { ...left === void 0 ? {} : { left }, ...right === void 0 ? {} : { right } };
193
+ if (!isTypeId(left) || !isTypeId(right)) {
194
+ const missing = !isTypeId(left) ? "sending" : "receiving";
195
+ return {
196
+ status: "unchecked",
197
+ ...both,
198
+ note: `the ${missing} side declares no named type`
199
+ };
200
+ }
201
+ if (left === right) {
202
+ const shared = db.type(left)?.meta?.["sharedPackage"];
203
+ return typeof shared === "string" ? { status: "shared", ...both, note: `both sides import it from ${shared}` } : { status: "identical", ...both };
204
+ }
205
+ const a = db.type(left);
206
+ const b = db.type(right);
207
+ if (a === void 0 || b === void 0) {
208
+ return { status: "unchecked", ...both, note: "a type id on the edge is not in the registry" };
209
+ }
210
+ return {
211
+ status: a.structuralHash === b.structuralHash ? "identical" : "hash_differs",
212
+ ...both
213
+ };
214
+ };
215
+ var RANK = {
216
+ shared: 0,
217
+ identical: 1,
218
+ unchecked: 2,
219
+ hash_differs: 3
220
+ };
221
+ var worse = (a, b) => RANK[a] >= RANK[b] ? a : b;
222
+ var sidesOf = (db, edge) => {
223
+ const handler = db.edgesFrom(edge.to, ["handles"])[0];
224
+ return {
225
+ request: classify(db, edge.params?.[0], handler?.params?.[0]),
226
+ response: classify(db, edge.returns, handler?.returns)
227
+ };
228
+ };
229
+ var parseEdgeRef = (ref) => {
230
+ const arrow = /^(.*?)\s*-([a-z_]+)->\s*(.*)$/.exec(ref.trim());
231
+ if (arrow !== null) {
232
+ return { from: (arrow[1] ?? "").trim(), type: arrow[2], to: (arrow[3] ?? "").trim() };
233
+ }
234
+ const plain = ref.split("->");
235
+ if (plain.length !== 2)
236
+ return void 0;
237
+ return { from: (plain[0] ?? "").trim(), to: (plain[1] ?? "").trim() };
238
+ };
239
+ var registerContract = (server, ctx) => {
240
+ server.registerTool("check_contract", {
241
+ title: "Check a contract",
242
+ description: 'Compare what one side of a cross-service edge sends against what the other side expects. Identifies an edge by its two ends, or by "from -http_calls-> to".',
243
+ inputSchema: {
244
+ edge: z3.string().optional().describe('"<from> -http_calls-> <to>"'),
245
+ from: z3.string().optional().describe("the calling node"),
246
+ to: z3.string().optional().describe("the node it reaches"),
247
+ ...commonInput
248
+ }
249
+ }, (input) => withDb(ctx.handle, (db) => {
250
+ const { note } = bound(input);
251
+ const parsed = input.edge !== void 0 ? parseEdgeRef(input.edge) : input.from !== void 0 && input.to !== void 0 ? { from: input.from, to: input.to } : void 0;
252
+ if (parsed === void 0) {
253
+ return { error: "give either edge, or both from and to" };
254
+ }
255
+ const candidates = db.edgesFrom(parsed.from).filter((edge2) => edge2.to === parsed.to).filter((edge2) => parsed.type === void 0 || edge2.type === parsed.type);
256
+ const edge = candidates[0];
257
+ if (edge === void 0) {
258
+ return { error: `no edge from ${parsed.from} to ${parsed.to}` };
259
+ }
260
+ const { request, response } = sidesOf(db, edge);
261
+ const status = worse(request.status, response.status);
262
+ const hash = (id) => id === void 0 ? void 0 : { id, structuralHash: db.type(id)?.structuralHash ?? "" };
263
+ const findings = ctx.contractChecker?.({
264
+ edge: { from: edge.from, to: edge.to, type: edge.type },
265
+ ...hash(response.left) === void 0 ? {} : { left: hash(response.left) },
266
+ ...hash(response.right) === void 0 ? {} : { right: hash(response.right) }
267
+ }) ?? [];
268
+ return {
269
+ edge: { from: edge.from, to: edge.to, type: edge.type, confidence: edge.confidence },
270
+ status,
271
+ request,
272
+ response,
273
+ findings,
274
+ ...note === void 0 ? {} : { note }
275
+ };
276
+ }));
277
+ };
278
+
279
+ // ../mcp/dist/tools/entries.js
280
+ import { z as z4 } from "zod";
281
+ var ENTRY_KINDS = [
282
+ "http",
283
+ "bot_command",
284
+ "bot_callback",
285
+ "bot_event",
286
+ "scene_step",
287
+ "event",
288
+ "rpc",
289
+ "cron"
290
+ ];
291
+ var cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
292
+ var inReadingOrder = (a, b) => cmp(a.repo, b.repo) || cmp(a.kind ?? "", b.kind ?? "") || cmp(a.label, b.label);
293
+ var initialsOf = (text) => (text.match(/[A-Z]|(?<=[^A-Za-z0-9])[a-z]/g) ?? []).join("").toLowerCase();
294
+ var rankMatches = (nodes, query) => {
295
+ const wanted = query.toLowerCase();
296
+ const ranked = [];
297
+ for (const node of nodes) {
298
+ const label = node.label.toLowerCase();
299
+ const id = node.id.toLowerCase();
300
+ if (label === wanted)
301
+ ranked.push({ node, score: 0 });
302
+ else if (label.startsWith(wanted))
303
+ ranked.push({ node, score: 1 });
304
+ else if (label.includes(wanted))
305
+ ranked.push({ node, score: 2 });
306
+ else if (initialsOf(node.label).includes(wanted))
307
+ ranked.push({ node, score: 3 });
308
+ else if (id.includes(wanted))
309
+ ranked.push({ node, score: 4 });
310
+ }
311
+ return ranked.sort((a, b) => a.score - b.score || a.node.label.length - b.node.label.length || cmp(a.node.id, b.node.id));
312
+ };
313
+ var registerEntries = (server, ctx) => {
314
+ server.registerTool("list_entries", {
315
+ title: "List entry points",
316
+ description: "Every way into the project: routes, bot commands, scheduled jobs, message handlers. Filter by service, kind or path prefix.",
317
+ inputSchema: {
318
+ service: z4.string().optional().describe("only this service"),
319
+ kind: z4.enum(ENTRY_KINDS).optional().describe("only this kind of entry"),
320
+ pathPrefix: z4.string().optional().describe("only routes whose path starts with this"),
321
+ ...commonInput
322
+ }
323
+ }, (input) => withDb(ctx.handle, (db) => {
324
+ const { detail, maxNodes, note } = bound(input);
325
+ const prefix = input.pathPrefix === void 0 ? void 0 : normalizePath(input.pathPrefix);
326
+ const all = db.nodesByType("entry", input.kind).filter((entry) => input.service === void 0 || entry.repo === input.service).filter((entry) => {
327
+ if (prefix === void 0)
328
+ return true;
329
+ const path = normalizePath(String(entry.meta?.["path"] ?? ""));
330
+ return path === prefix || path.startsWith(prefix === "/" ? "/" : `${prefix}/`);
331
+ }).sort(inReadingOrder);
332
+ const { items, truncated } = truncate(all, maxNodes);
333
+ return {
334
+ entries: items.map((entry) => projectDetail(entry, detail)),
335
+ total: all.length,
336
+ ...truncated === void 0 ? {} : { truncated },
337
+ ...note === void 0 ? {} : { note }
338
+ };
339
+ }));
340
+ server.registerTool("find_symbol", {
341
+ title: "Find a symbol",
342
+ description: 'Fuzzy search over every node by label and id. Accepts a substring or camel-case initials such as "ocs" for OrderCreationService.',
343
+ inputSchema: {
344
+ query: z4.string().describe("what to look for"),
345
+ types: z4.array(z4.string()).optional().describe("only these node types"),
346
+ service: z4.string().optional().describe("only this service"),
347
+ maxNodes: commonInput.maxNodes,
348
+ detail: commonInput.detail
349
+ }
350
+ }, (input) => withDb(ctx.handle, (db) => {
351
+ if (input.query.trim().length < 2)
352
+ return { matches: [], note: "query too short" };
353
+ const { detail, maxNodes, note } = bound(input);
354
+ const pool = db.search(input.query, {
355
+ ...input.types === void 0 ? {} : { types: input.types },
356
+ limit: Math.max(maxNodes * 4, 200)
357
+ });
358
+ const wider = pool.length > 0 ? pool : db.nodesByType("method");
359
+ const ranked = rankMatches(wider.filter((node) => input.service === void 0 || node.repo === input.service), input.query);
360
+ const { items, truncated } = truncate(ranked, maxNodes);
361
+ return {
362
+ matches: items.map((match) => projectDetail(match.node, detail)),
363
+ ...truncated === void 0 ? {} : { truncated },
364
+ ...note === void 0 ? {} : { note }
365
+ };
366
+ }));
367
+ };
368
+
369
+ // ../mcp/dist/tools/flow.js
370
+ import { z as z5 } from "zod";
371
+
372
+ // ../mcp/dist/query/entry-ref.js
373
+ var isResolved = (ref) => "id" in ref;
374
+ var asRoute = (text) => {
375
+ const match = /^([A-Za-z]+)\s+(\/.*)$/.exec(text.trim());
376
+ if (match === null)
377
+ return void 0;
378
+ return { method: (match[1] ?? "").toUpperCase(), path: normalizePath(match[2] ?? "/") };
379
+ };
380
+ var asBotKey = (text) => {
381
+ const match = /^bot:(.+)$/.exec(text.trim());
382
+ return match === null ? void 0 : (match[1] ?? "").trim();
383
+ };
384
+ var byId = (nodes) => [...nodes].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
385
+ var resolveEntryRef = (db, ref) => {
386
+ const trimmed = ref.trim();
387
+ const exact = db.node(trimmed);
388
+ if (exact !== void 0)
389
+ return { id: exact.id, node: exact };
390
+ const entries = db.nodesByType("entry");
391
+ const route = asRoute(trimmed);
392
+ if (route !== void 0) {
393
+ const matches = entries.filter((entry) => entry.kind === "http" && String(entry.meta?.["method"] ?? "") === route.method && pathAnswers(String(entry.meta?.["path"] ?? ""), route.path));
394
+ if (matches.length === 1)
395
+ return { id: matches[0].id, node: matches[0] };
396
+ if (matches.length > 1)
397
+ return { candidates: byId(matches) };
398
+ }
399
+ const key = asBotKey(trimmed);
400
+ if (key !== void 0) {
401
+ const matches = entries.filter((entry) => String(entry.meta?.["key"] ?? entry.label) === key && entry.kind?.startsWith("bot_") === true);
402
+ if (matches.length === 1)
403
+ return { id: matches[0].id, node: matches[0] };
404
+ if (matches.length > 1)
405
+ return { candidates: byId(matches) };
406
+ }
407
+ const named = entries.filter((entry) => entry.label === trimmed);
408
+ if (named.length === 1)
409
+ return { id: named[0].id, node: named[0] };
410
+ return { candidates: byId(named) };
411
+ };
412
+
413
+ // ../mcp/dist/query/flow.js
414
+ var FORWARD_EDGES = [
415
+ "handles",
416
+ "calls",
417
+ "queries",
418
+ "caches",
419
+ "emits",
420
+ "consumes",
421
+ "http_calls",
422
+ "hits",
423
+ "triggers",
424
+ "reads_config"
425
+ ];
426
+ var GUARD_EDGE = "guarded_by";
427
+ var REVERSE_EDGES = FORWARD_EDGES;
428
+ var SCAN_FACTOR = 4;
429
+ var orderOf = (edge) => {
430
+ const order = edge.meta?.["order"];
431
+ return typeof order === "number" ? order : Number.MAX_SAFE_INTEGER;
432
+ };
433
+ var inCallOrder = (a, b) => (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER) || (a.to < b.to ? -1 : a.to > b.to ? 1 : 0);
434
+ var guardsOf = (db, id, detail) => db.edgesFrom(id, [GUARD_EDGE]).sort((a, b) => orderOf(a) - orderOf(b)).map((edge, index) => {
435
+ const node = db.node(edge.to);
436
+ return {
437
+ id: edge.to,
438
+ label: node?.label ?? edge.to,
439
+ kind: String(edge.meta?.["kind"] ?? node?.kind ?? "guard"),
440
+ order: orderOf(edge) === Number.MAX_SAFE_INTEGER ? index : orderOf(edge),
441
+ ...detail >= 2 && node?.file !== void 0 ? { loc: `${node.file}:${node.line ?? 0}` } : {}
442
+ };
443
+ });
444
+ var missingNode = (id) => ({ id, type: "missing", label: id });
445
+ var buildFlowTree = (db, entryId, options = {}) => {
446
+ const depth = options.depth ?? 8;
447
+ const maxNodes = options.maxNodes ?? 150;
448
+ const detail = options.detail ?? 1;
449
+ const start = db.node(entryId);
450
+ const root = {
451
+ node: start === void 0 ? missingNode(entryId) : projectDetail(start, detail),
452
+ children: []
453
+ };
454
+ const rootGuards = guardsOf(db, entryId, detail);
455
+ if (rootGuards.length > 0)
456
+ root.guards = rootGuards;
457
+ const unresolvedIds = /* @__PURE__ */ new Set();
458
+ if (db.unresolvedFor(entryId).length > 0)
459
+ unresolvedIds.add(entryId);
460
+ let used = 1;
461
+ let scanned = 0;
462
+ let overflow = 0;
463
+ const scanLimit = maxNodes * SCAN_FACTOR;
464
+ const queue = [{ flow: root, id: entryId, path: /* @__PURE__ */ new Set([entryId]), level: 0 }];
465
+ while (queue.length > 0) {
466
+ const item = queue.shift();
467
+ if (item.level >= depth)
468
+ continue;
469
+ for (const edge of db.edgesFrom(item.id, FORWARD_EDGES).sort(inCallOrder)) {
470
+ scanned += 1;
471
+ if (scanned > scanLimit) {
472
+ overflow += 1;
473
+ continue;
474
+ }
475
+ const room = item.flow !== void 0 && used < maxNodes;
476
+ if (!room)
477
+ overflow += 1;
478
+ if (item.path.has(edge.to)) {
479
+ if (room) {
480
+ used += 1;
481
+ item.flow?.children.push({
482
+ node: { id: edge.to, type: "ref", label: edge.to, ref: true },
483
+ edge: projectEdge(edge, detail),
484
+ children: []
485
+ });
486
+ }
487
+ continue;
488
+ }
489
+ const target = db.node(edge.to);
490
+ let child;
491
+ if (room) {
492
+ if (target === void 0)
493
+ unresolvedIds.add(edge.to);
494
+ else if (db.unresolvedFor(edge.to).length > 0)
495
+ unresolvedIds.add(edge.to);
496
+ child = {
497
+ node: target === void 0 ? missingNode(edge.to) : projectDetail(target, detail),
498
+ edge: projectEdge(edge, detail),
499
+ children: []
500
+ };
501
+ const guards = guardsOf(db, edge.to, detail);
502
+ if (guards.length > 0)
503
+ child.guards = guards;
504
+ used += 1;
505
+ item.flow?.children.push(child);
506
+ }
507
+ queue.push({
508
+ flow: child,
509
+ id: edge.to,
510
+ path: /* @__PURE__ */ new Set([...item.path, edge.to]),
511
+ level: item.level + 1
512
+ });
513
+ }
514
+ }
515
+ const result = { root, unresolvedOnPath: unresolvedIds.size };
516
+ if (overflow > 0)
517
+ result.truncated = truncationMessage(overflow, scanned <= scanLimit);
518
+ return result;
519
+ };
520
+ var flatten = (node) => [
521
+ node,
522
+ ...node.children.flatMap((child) => flatten(child))
523
+ ];
524
+
525
+ // ../mcp/dist/tools/flow.js
526
+ var registerFlow = (server, ctx) => {
527
+ server.registerTool("get_flow", {
528
+ title: "Trace an entry point",
529
+ description: "Follow one entry point through every repository it reaches: handlers, calls, queries, channels and the routes it calls in other services. Returns a nested tree in call order, with the guard chain on each entry.",
530
+ inputSchema: {
531
+ entry: z5.string().describe('an entry: "POST /orders", "bot:order_confirm", or a full entry id'),
532
+ depth: z5.number().int().positive().max(32).default(8).describe("how many hops to follow"),
533
+ ...commonInput
534
+ }
535
+ }, (input) => withDb(ctx.handle, (db) => {
536
+ const { detail, maxNodes, note } = bound(input);
537
+ const ref = resolveEntryRef(db, input.entry);
538
+ if (!isResolved(ref)) {
539
+ if (ref.candidates.length > 0) {
540
+ return { candidates: ref.candidates.map((node) => projectDetail(node, detail)) };
541
+ }
542
+ const suggestions = rankMatches(db.nodesByType("entry"), input.entry).slice(0, 5).map((match) => projectDetail(match.node, detail));
543
+ return { error: `no entry matches ${JSON.stringify(input.entry)}`, suggestions };
544
+ }
545
+ const flow = buildFlowTree(db, ref.id, { depth: input.depth, maxNodes, detail });
546
+ return {
547
+ root: flow.root,
548
+ unresolvedOnPath: flow.unresolvedOnPath,
549
+ ...flow.truncated === void 0 ? {} : { truncated: flow.truncated },
550
+ ...note === void 0 ? {} : { note }
551
+ };
552
+ }));
553
+ };
554
+
555
+ // ../mcp/dist/tools/reach.js
556
+ import { z as z6 } from "zod";
557
+ var findTarget = (db, symbol) => {
558
+ const exact = db.node(symbol.trim());
559
+ if (exact !== void 0)
560
+ return exact;
561
+ const ranked = rankMatches(db.search(symbol, { limit: 200 }), symbol);
562
+ if (ranked.length === 0)
563
+ return [];
564
+ const best = ranked[0];
565
+ const tied = ranked.filter((match) => match.score === best.score);
566
+ return tied.length === 1 ? best.node : tied.slice(0, 8).map((match) => match.node);
567
+ };
568
+ var callersOf = (db, id, depth, maxNodes, detail) => {
569
+ const roots = [];
570
+ const queue = [
571
+ { id, level: 0, into: roots, path: /* @__PURE__ */ new Set([id]) }
572
+ ];
573
+ let used = 0;
574
+ let overflow = 0;
575
+ while (queue.length > 0) {
576
+ const item = queue.shift();
577
+ if (item.level >= depth)
578
+ continue;
579
+ for (const edge of db.edgesTo(item.id, REVERSE_EDGES)) {
580
+ if (item.path.has(edge.from))
581
+ continue;
582
+ if (used >= maxNodes) {
583
+ overflow += 1;
584
+ continue;
585
+ }
586
+ const source = db.node(edge.from);
587
+ if (source === void 0)
588
+ continue;
589
+ const flow = {
590
+ node: projectDetail(source, detail),
591
+ edge: projectEdge(edge, detail),
592
+ children: []
593
+ };
594
+ used += 1;
595
+ item.into.push(flow);
596
+ queue.push({
597
+ id: edge.from,
598
+ level: item.level + 1,
599
+ into: flow.children,
600
+ path: /* @__PURE__ */ new Set([...item.path, edge.from])
601
+ });
602
+ }
603
+ }
604
+ return {
605
+ callers: roots,
606
+ ...overflow > 0 ? { truncated: truncationMessage(overflow, true) } : {}
607
+ };
608
+ };
609
+ var registerReach = (server, ctx) => {
610
+ server.registerTool("who_calls", {
611
+ title: "Who calls this",
612
+ description: "Everything that reaches a symbol, following calls backward across repositories: direct callers, the routes that handle them, and requests from other services.",
613
+ inputSchema: {
614
+ symbol: z6.string().describe("a node id, or a name close enough to identify one"),
615
+ depth: z6.number().int().positive().max(16).default(3).describe("how many hops back"),
616
+ ...commonInput
617
+ }
618
+ }, (input) => withDb(ctx.handle, (db) => {
619
+ const { detail, maxNodes, note } = bound(input);
620
+ const found = findTarget(db, input.symbol);
621
+ if (Array.isArray(found)) {
622
+ return found.length === 0 ? { error: `no symbol matches ${JSON.stringify(input.symbol)}` } : { candidates: found.map((node) => projectDetail(node, detail)) };
623
+ }
624
+ const { callers, truncated } = callersOf(db, found.id, input.depth, maxNodes, detail);
625
+ return {
626
+ target: projectDetail(found, detail),
627
+ callers,
628
+ ...truncated === void 0 ? {} : { truncated },
629
+ ...note === void 0 ? {} : { note }
630
+ };
631
+ }));
632
+ server.registerTool("impact", {
633
+ title: "Blast radius",
634
+ description: "Every entry point that can reach a symbol, which services they belong to, and which services the chain runs into without an entry point above them. What would have to be retested if this changed.",
635
+ inputSchema: {
636
+ symbol: z6.string().describe("a node id, or a name close enough to identify one"),
637
+ ...commonInput
638
+ }
639
+ }, (input) => withDb(ctx.handle, (db) => {
640
+ const { detail, maxNodes, note } = bound(input);
641
+ const found = findTarget(db, input.symbol);
642
+ if (Array.isArray(found)) {
643
+ return found.length === 0 ? { error: `no symbol matches ${JSON.stringify(input.symbol)}` } : { candidates: found.map((node) => projectDetail(node, detail)) };
644
+ }
645
+ const reach = db.reverseReach(found.id, { edgeTypes: REVERSE_EDGES, maxDepth: 12, maxNodes: 4e3 });
646
+ const entries = [];
647
+ const channels = /* @__PURE__ */ new Set();
648
+ const entriesByService = {};
649
+ const reachedByService = {};
650
+ let reached = 0;
651
+ for (const row of reach.rows) {
652
+ if (row.id === found.id)
653
+ continue;
654
+ reached += 1;
655
+ const node = db.node(row.id);
656
+ if (node !== void 0) {
657
+ reachedByService[node.repo] = (reachedByService[node.repo] ?? 0) + 1;
658
+ }
659
+ if (row.type === "channel")
660
+ channels.add(row.id);
661
+ if (row.type !== "entry" || node === void 0)
662
+ continue;
663
+ entries.push(node);
664
+ entriesByService[node.repo] = (entriesByService[node.repo] ?? 0) + 1;
665
+ }
666
+ const servicesWithoutEntry = Object.keys(reachedByService).filter((repo) => entriesByService[repo] === void 0).sort();
667
+ entries.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
668
+ const { items, truncated } = truncate(entries, maxNodes);
669
+ return {
670
+ target: projectDetail(found, detail),
671
+ entries: items.map((entry) => projectDetail(entry, detail)),
672
+ // How many things reach it at all, so no entry points is telling
673
+ // rather than indistinguishable from nothing reaching it.
674
+ reached,
675
+ entriesByService,
676
+ reachedByService,
677
+ servicesWithoutEntry,
678
+ channels: [...channels].sort(),
679
+ ...truncated === void 0 ? {} : { truncated },
680
+ ...reach.truncated ? { note: "reachability stopped at the walk limit" } : {},
681
+ ...note === void 0 ? {} : { note }
682
+ };
683
+ }));
684
+ };
685
+
686
+ // ../mcp/dist/tools/source.js
687
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
688
+ import { join } from "node:path";
689
+ import { z as z7 } from "zod";
690
+ var endOfDeclaration = (lines, startIndex) => {
691
+ let depth = 0;
692
+ let seen = false;
693
+ for (let index = startIndex; index < lines.length; index += 1) {
694
+ for (const character of lines[index] ?? "") {
695
+ if (character === "{") {
696
+ depth += 1;
697
+ seen = true;
698
+ } else if (character === "}")
699
+ depth -= 1;
700
+ }
701
+ if (seen && depth <= 0)
702
+ return index;
703
+ if (!seen && index > startIndex + 40)
704
+ return index;
705
+ }
706
+ return lines.length - 1;
707
+ };
708
+ var registerSource = (server, ctx) => {
709
+ server.registerTool("get_source", {
710
+ title: "Read the source of a symbol",
711
+ description: "The actual code of one symbol. The only tool that returns source; ask for it by name once the graph has told you which name to ask for.",
712
+ inputSchema: {
713
+ symbol: z7.string().describe("a node id"),
714
+ context: z7.number().int().min(0).max(50).default(0).describe("extra lines either side")
715
+ }
716
+ }, (input) => withDb(ctx.handle, (db) => {
717
+ const node = db.node(input.symbol.trim());
718
+ if (node === void 0)
719
+ return { error: `no symbol with id ${JSON.stringify(input.symbol)}` };
720
+ if (node.file === void 0)
721
+ return { error: "source unavailable", reason: "the node records no file" };
722
+ const root = ctx.handle.repoDir(node.repo);
723
+ if (root === void 0) {
724
+ return { error: "source unavailable", reason: `no repository configured for ${node.repo}`, file: node.file };
725
+ }
726
+ const path = join(root, node.file);
727
+ if (!existsSync2(path))
728
+ return { error: "source unavailable", file: path };
729
+ const lines = readFileSync(path, "utf8").split("\n");
730
+ const start = Math.max((node.line ?? 1) - 1, 0);
731
+ const end = endOfDeclaration(lines, start);
732
+ const from = Math.max(start - input.context, 0);
733
+ const to = Math.min(end + input.context, lines.length - 1);
734
+ return {
735
+ id: node.id,
736
+ file: node.file,
737
+ line: from + 1,
738
+ endLine: to + 1,
739
+ code: lines.slice(from, to + 1).join("\n")
740
+ };
741
+ }));
742
+ };
743
+
744
+ // ../mcp/dist/tools/types.js
745
+ import { z as z8 } from "zod";
746
+ var referencedIds = (entry) => {
747
+ const found = /* @__PURE__ */ new Set();
748
+ for (const field of entry.fields ?? []) {
749
+ for (const match of field.type.matchAll(/type:[^\s,;()[\]{}<>|&]+/g))
750
+ found.add(match[0]);
751
+ }
752
+ for (const member of entry.members ?? []) {
753
+ for (const match of member.matchAll(/type:[^\s,;()[\]{}<>|&]+/g))
754
+ found.add(match[0]);
755
+ }
756
+ return [...found];
757
+ };
758
+ var expandType = (db, id, depth) => {
759
+ const nested = {};
760
+ let frontier = [id];
761
+ for (let level = 0; level < depth && frontier.length > 0; level += 1) {
762
+ const next = [];
763
+ for (const current of frontier) {
764
+ const entry = db.type(current);
765
+ if (entry === void 0)
766
+ continue;
767
+ for (const referenced of referencedIds(entry)) {
768
+ if (referenced === id || nested[referenced] !== void 0)
769
+ continue;
770
+ const found = db.type(referenced);
771
+ if (found === void 0)
772
+ continue;
773
+ nested[referenced] = found;
774
+ next.push(referenced);
775
+ }
776
+ }
777
+ frontier = next;
778
+ }
779
+ return nested;
780
+ };
781
+ var registerTypes = (server, ctx) => {
782
+ server.registerTool("get_type", {
783
+ title: "Read a type",
784
+ description: "The structure of a type from the registry, with the types it refers to expanded to a depth. Accepts a full type id or a bare name.",
785
+ inputSchema: {
786
+ type: z8.string().describe("a type id such as type:orders#OrderDto, or a bare name"),
787
+ depth: z8.number().int().min(0).max(6).default(3).describe("how deep to expand nesting"),
788
+ ...commonInput
789
+ }
790
+ }, (input) => withDb(ctx.handle, (db) => {
791
+ const { note } = bound(input);
792
+ const asked = input.type.trim();
793
+ let id = asked;
794
+ if (!asked.startsWith("type:")) {
795
+ const named = db.typesByName(asked);
796
+ if (named.length === 0)
797
+ return { error: `no type named ${JSON.stringify(asked)}` };
798
+ if (named.length > 1)
799
+ return { candidates: named.map((entry2) => entry2.id) };
800
+ id = named[0].id;
801
+ }
802
+ const entry = db.type(id);
803
+ if (entry === void 0)
804
+ return { error: `no type with id ${JSON.stringify(id)}` };
805
+ return {
806
+ type: entry,
807
+ nested: expandType(db, id, input.depth),
808
+ ...note === void 0 ? {} : { note }
809
+ };
810
+ }));
811
+ };
812
+
813
+ // ../mcp/dist/server.js
814
+ var SERVER_NAME = "flowatlas";
815
+ var createFlowatlasServer = (options = {}) => {
816
+ const handle = new DbHandle(options);
817
+ const context = {
818
+ handle,
819
+ ...options.contractChecker === void 0 ? {} : { contractChecker: options.contractChecker }
820
+ };
821
+ const server = new McpServer({ name: SERVER_NAME, version: options.version ?? "0.0.0" }, {
822
+ instructions: "A map of every repository in this project, already joined. Ask it what reaches what instead of reading files: list_entries to find a way in, get_flow to follow one through every service it touches, who_calls and impact to work backwards, who_emits and who_consumes for message channels, get_type for a shape, check_contract for whether two services still agree, find_symbol when you only half remember a name. Every answer is bounded; get_source is the only one that returns code."
823
+ });
824
+ registerEntries(server, context);
825
+ registerFlow(server, context);
826
+ registerReach(server, context);
827
+ registerChannels(server, context);
828
+ registerTypes(server, context);
829
+ registerContract(server, context);
830
+ registerSource(server, context);
831
+ return server;
832
+ };
833
+ var startStdioServer = async (options = {}) => {
834
+ const server = createFlowatlasServer(options);
835
+ await server.connect(new StdioServerTransport());
836
+ return server;
837
+ };
838
+
839
+ export {
840
+ DbHandle,
841
+ truncationMessage,
842
+ MAX_DETAIL,
843
+ CLAMP_NOTE,
844
+ clampDetail,
845
+ projectDetail,
846
+ projectEdge,
847
+ truncate,
848
+ isResolved,
849
+ resolveEntryRef,
850
+ FORWARD_EDGES,
851
+ REVERSE_EDGES,
852
+ buildFlowTree,
853
+ flatten,
854
+ endOfDeclaration,
855
+ expandType,
856
+ SERVER_NAME,
857
+ createFlowatlasServer,
858
+ startStdioServer
859
+ };