@mmerterden/multi-agent-toolkit-mcp 3.9.0 → 3.11.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,665 @@
1
+ /**
2
+ * code-intel - compiler-grade answers about Swift and Kotlin source.
3
+ *
4
+ * ONE FAMILY, NOT TWO. The capability set is genuinely symmetric - both sides
5
+ * are LSP servers answering the same handful of methods - so the language is
6
+ * inferred from the file extension rather than doubling the tool surface for no
7
+ * new capability. The asymmetry that does exist lives in the OUTPUT: every
8
+ * result carries `language`, `semantic` and, on the Kotlin side, `confidence`,
9
+ * so a caller can tell an index-backed Swift answer from an Alpha Kotlin one.
10
+ *
11
+ * NO SHELL, ANYWHERE. Every child here is `spawn(bin, argvArray)`. Nothing in
12
+ * this directory builds a command string, so none of the seven sanitizers
13
+ * applies and there is nothing for a quoting mistake to exploit. A gate asserts
14
+ * it rather than a comment claiming it.
15
+ *
16
+ * WHAT IS DELIBERATELY ABSENT. Nothing writes. `swift_replace_symbol_body` and
17
+ * its relatives were considered and dropped: an agent already has an editing
18
+ * tool, and giving a read-only family a write verb is how "read-only" stops
19
+ * meaning anything. `code_server_reset` is the one non-read-only entry and it
20
+ * discards a cache, not anyone's work.
21
+ *
22
+ * @module tools/code-intel
23
+ */
24
+
25
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
26
+ import { isAbsolute, sep } from "node:path";
27
+ import { pathToFileURL } from "node:url";
28
+ import { withServer, resetServers, poolStats, shutdownAllLsp } from "./pool.js";
29
+ import {
30
+ languageOf,
31
+ toLspPosition,
32
+ fromLspPosition,
33
+ findSymbol,
34
+ shapeLocations,
35
+ markdownFromHover,
36
+ } from "./positions.js";
37
+ import { resolveSwiftBin, resolveWorkspaceRoot, indexReport } from "./swift.js";
38
+ import {
39
+ probeKotlin,
40
+ resolveKotlinRoot,
41
+ kotlinReport,
42
+ KOTLIN_ARGV,
43
+ KOTLIN_INIT_TIMEOUT_MS,
44
+ KOTLIN_INSTALL,
45
+ } from "./kotlin.js";
46
+
47
+ const ERROR_PREFIX = "ERROR: ";
48
+ const DEFAULT_MAX_RESULTS = 200;
49
+ /** How long a cross-file question may wait for the background index. */
50
+ const DEFAULT_INDEX_WAIT_MS = 120000;
51
+
52
+ const FILE_ARG = {
53
+ file: { type: "string", description: "Absolute path to a .swift, .kt or .kts file" },
54
+ workspace_root: {
55
+ type: "string",
56
+ description:
57
+ "Package or project root. Resolved from the file when omitted; the resolved root and how it was found come back in every result",
58
+ },
59
+ };
60
+ const POSITION_ARGS = {
61
+ line: { type: "number", description: "1-based line, as an editor shows it" },
62
+ column: { type: "number", description: "1-based column (default 1)" },
63
+ symbol: {
64
+ type: "string",
65
+ description:
66
+ 'Name or dotted path ("Greeter.greet") instead of line/column. Resolved from the file\'s own symbol tree, which needs no build configuration',
67
+ },
68
+ };
69
+
70
+ export const CODE_TOOLS = [
71
+ {
72
+ name: "code_definition",
73
+ description:
74
+ "Where a symbol is declared. Give a 1-based line+column, or a symbol name and skip counting columns. Swift via sourcekit-lsp (bundled with Xcode), Kotlin via kotlin-lsp when installed. Every result says which build settings the answer came from, because a fallback root answers confidently and wrongly.",
75
+ inputSchema: { type: "object", properties: { ...FILE_ARG, ...POSITION_ARGS }, required: ["file"] },
76
+ },
77
+ {
78
+ name: "code_references",
79
+ description:
80
+ "Every reference to a symbol across the workspace. Needs a built index: this waits for the background index to settle and says so rather than returning an empty list, because '0 references' reads as 'unused' and that is the wrong thing to be wrong about. Measured: the same query answered 0 at 0.7s and 3 at 5.8s on a two-file package.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ ...FILE_ARG,
85
+ ...POSITION_ARGS,
86
+ include_declaration: { type: "boolean", description: "Include the declaration (default true)" },
87
+ max_results: { type: "number", description: `Cap (default ${DEFAULT_MAX_RESULTS})` },
88
+ index_wait_sec: { type: "number", description: "How long to wait for the index (default 120)" },
89
+ },
90
+ required: ["file"],
91
+ },
92
+ },
93
+ {
94
+ name: "code_hover",
95
+ description:
96
+ "The type and documentation a compiler has for a symbol: signature, inferred type, doc comment. Works without an index for symbols in the file and the standard library.",
97
+ inputSchema: { type: "object", properties: { ...FILE_ARG, ...POSITION_ARGS }, required: ["file"] },
98
+ },
99
+ {
100
+ name: "code_document_symbols",
101
+ description:
102
+ "The symbol tree of one file - types, members, nesting, with 1-based lines. The one capability that needs no build configuration at all, so it is the honest fallback when an index is missing.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: { ...FILE_ARG, max_depth: { type: "number", description: "Tree depth (default 3)" } },
106
+ required: ["file"],
107
+ },
108
+ },
109
+ {
110
+ name: "code_workspace_symbols",
111
+ description:
112
+ "Find a symbol by name across the whole workspace. Index-backed, so it reports an unbuilt index instead of an empty list.",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ query: { type: "string", description: "Name or fragment to search for" },
117
+ workspace_root: { type: "string", description: "Package or project root" },
118
+ language: { type: "string", enum: ["swift", "kotlin"], description: "Which server to ask" },
119
+ max_results: { type: "number", description: `Cap (default ${DEFAULT_MAX_RESULTS})` },
120
+ index_wait_sec: { type: "number", description: "How long to wait for the index (default 120)" },
121
+ },
122
+ required: ["query", "workspace_root"],
123
+ },
124
+ },
125
+ {
126
+ name: "code_diagnostics",
127
+ description:
128
+ "Compiler diagnostics for one file without running a full build. Reports how long it waited when the server published nothing, rather than returning 0 and letting that read as clean. On a root with fallback build settings the result is marked non-semantic, because 'no such module' there is an artefact of the missing configuration, not a finding.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: { ...FILE_ARG, wait_ms: { type: "number", description: "Budget (default 4000, max 15000)" } },
132
+ required: ["file"],
133
+ },
134
+ },
135
+ {
136
+ name: "code_index_status",
137
+ description:
138
+ "Whether this machine can answer code-intelligence questions at all, per language: is a server installed, which build system was found for the root, is an index on disk, and what to do about it if not. Always succeeds - it is the tool you run because something is missing.",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ workspace_root: { type: "string", description: "Root to report on" },
143
+ file: { type: "string", description: "A file to resolve the root from instead" },
144
+ },
145
+ },
146
+ },
147
+ {
148
+ name: "code_server_reset",
149
+ description:
150
+ "Stop the pooled language servers so the next question starts a fresh one. For when a server has wedged on stale build settings after a Package.swift edit. Discards a cache, never source.",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ workspace_root: { type: "string", description: "Only this root" },
155
+ language: { type: "string", enum: ["swift", "kotlin"], description: "Only this language" },
156
+ },
157
+ },
158
+ },
159
+ ];
160
+
161
+ export const CODE_READ_ONLY = CODE_TOOLS.map((t) => t.name).filter((n) => n !== "code_server_reset");
162
+ export const CODE_IDEMPOTENT = ["code_server_reset"];
163
+
164
+ const LOCATION_LIST = {
165
+ type: "array",
166
+ items: {
167
+ type: "object",
168
+ properties: {
169
+ file: { type: "string" },
170
+ line: { type: "number" },
171
+ column: { type: "number" },
172
+ preview: { type: "string" },
173
+ },
174
+ required: ["file", "line", "column"],
175
+ },
176
+ };
177
+
178
+ export const CODE_OUTPUT_SCHEMAS = {
179
+ code_definition: {
180
+ type: "object",
181
+ properties: {
182
+ language: { type: "string" },
183
+ root: { type: "string" },
184
+ buildSettingsSource: { type: "string" },
185
+ semantic: { type: "boolean" },
186
+ count: { type: "number" },
187
+ truncated: { type: "boolean" },
188
+ results: LOCATION_LIST,
189
+ reason: { type: "string" },
190
+ },
191
+ required: ["language", "root", "semantic", "count", "results"],
192
+ },
193
+ code_references: {
194
+ type: "object",
195
+ properties: {
196
+ language: { type: "string" },
197
+ root: { type: "string" },
198
+ buildSettingsSource: { type: "string" },
199
+ semantic: { type: "boolean" },
200
+ indexReady: { type: "boolean" },
201
+ count: { type: "number" },
202
+ truncated: { type: "boolean" },
203
+ results: LOCATION_LIST,
204
+ reason: { type: "string" },
205
+ },
206
+ required: ["language", "root", "semantic", "indexReady", "count", "results"],
207
+ },
208
+ code_document_symbols: {
209
+ type: "object",
210
+ properties: {
211
+ language: { type: "string" },
212
+ file: { type: "string" },
213
+ count: { type: "number" },
214
+ symbols: { type: "array", items: { type: "object" } },
215
+ },
216
+ required: ["language", "file", "count", "symbols"],
217
+ },
218
+ code_index_status: {
219
+ type: "object",
220
+ properties: {
221
+ swift: { type: "object" },
222
+ kotlin: { type: "object" },
223
+ servers: { type: "array", items: { type: "object" } },
224
+ },
225
+ required: ["swift", "kotlin"],
226
+ },
227
+ code_server_reset: {
228
+ type: "object",
229
+ properties: {
230
+ stopped: { type: "array", items: { type: "string" } },
231
+ remaining: { type: "number" },
232
+ },
233
+ required: ["stopped", "remaining"],
234
+ },
235
+ };
236
+
237
+ /** A path is not a shell string here, but a root mismatch still answers wrongly. */
238
+ function checkFile(file, root) {
239
+ if (!file || typeof file !== "string") return "file is required";
240
+ if (!isAbsolute(file)) return `file must be an absolute path, got "${file}"`;
241
+ if (!existsSync(file)) return `no such file: ${file}`;
242
+ try {
243
+ const real = realpathSync(file);
244
+ const realRoot = realpathSync(root);
245
+ if (real !== realRoot && !real.startsWith(realRoot + sep)) {
246
+ return `${file} is outside the workspace root ${root}; a mismatched root answers with an empty result rather than an error`;
247
+ }
248
+ } catch (e) {
249
+ return `cannot resolve ${file}: ${e.message}`;
250
+ }
251
+ return null;
252
+ }
253
+
254
+ function swiftSpec(root) {
255
+ const bin = resolveSwiftBin();
256
+ if (!bin) {
257
+ return {
258
+ error: `${ERROR_PREFIX}sourcekit-lsp not found. It ships with Xcode: install Xcode, or set SOURCEKIT_LSP_PATH.`,
259
+ };
260
+ }
261
+ const rootUri = pathToFileURL(root).href;
262
+ return {
263
+ language: "swift",
264
+ root,
265
+ bin,
266
+ argv: [],
267
+ initTimeoutMs: 120000,
268
+ initializeParams: {
269
+ processId: process.pid,
270
+ rootUri,
271
+ workspaceFolders: [{ uri: rootUri, name: root.split(sep).pop() }],
272
+ capabilities: {
273
+ window: { workDoneProgress: true },
274
+ workspace: { workspaceFolders: true, configuration: false },
275
+ textDocument: {
276
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true },
277
+ publishDiagnostics: { relatedInformation: false },
278
+ },
279
+ },
280
+ },
281
+ };
282
+ }
283
+
284
+ function kotlinSpec(root) {
285
+ const probe = probeKotlin();
286
+ if (!probe.available) {
287
+ return {
288
+ error: `${ERROR_PREFIX}${probe.reason}. Install: ${KOTLIN_INSTALL.join(" ; ")}`,
289
+ };
290
+ }
291
+ const rootUri = pathToFileURL(root).href;
292
+ return {
293
+ language: "kotlin",
294
+ root,
295
+ bin: probe.bin,
296
+ argv: KOTLIN_ARGV,
297
+ initTimeoutMs: KOTLIN_INIT_TIMEOUT_MS,
298
+ initializeParams: {
299
+ processId: process.pid,
300
+ rootUri,
301
+ workspaceFolders: [{ uri: rootUri, name: root.split(sep).pop() }],
302
+ capabilities: {
303
+ window: { workDoneProgress: true },
304
+ workspace: { workspaceFolders: true },
305
+ textDocument: { documentSymbol: { hierarchicalDocumentSymbolSupport: true } },
306
+ },
307
+ },
308
+ };
309
+ }
310
+
311
+ /** Resolve language, root and server spec for a file-shaped request. */
312
+ function contextFor(args) {
313
+ const file = args.file;
314
+ const language = args.language || languageOf(file);
315
+ if (!language) {
316
+ return { error: `${ERROR_PREFIX}${file}: not a Swift or Kotlin source file` };
317
+ }
318
+ const resolved =
319
+ language === "swift"
320
+ ? resolveWorkspaceRoot(file, args.workspace_root)
321
+ : resolveKotlinRoot(file, args.workspace_root);
322
+ const bad = checkFile(file, resolved.root);
323
+ if (bad) return { error: `${ERROR_PREFIX}${bad}` };
324
+ const spec = language === "swift" ? swiftSpec(resolved.root) : kotlinSpec(resolved.root);
325
+ if (spec.error) return spec;
326
+ return { language, root: resolved.root, source: resolved.source, spec };
327
+ }
328
+
329
+ /** line/column or symbol, to an LSP position. */
330
+ async function positionFor(args, client, uri, text) {
331
+ if (args.symbol) {
332
+ const res = await client.request("textDocument/documentSymbol", {
333
+ textDocument: { uri },
334
+ });
335
+ if (res.error) return { error: `${ERROR_PREFIX}${res.error.message}` };
336
+ const found = findSymbol(res.result, args.symbol);
337
+ if (found.error) return { error: `${ERROR_PREFIX}${found.error}` };
338
+ if (found.candidates) {
339
+ const list = found.candidates
340
+ .map((c) => `${c.path} (line ${(c.range?.start?.line ?? 0) + 1})`)
341
+ .join(", ");
342
+ return {
343
+ error: `${ERROR_PREFIX}"${args.symbol}" is ambiguous in this file: ${list}. Pass line and column, or a dotted path.`,
344
+ };
345
+ }
346
+ return { position: found.match.range.start };
347
+ }
348
+ const pos = toLspPosition(text, args.line, args.column);
349
+ if (pos.error) return { error: `${ERROR_PREFIX}${pos.error}` };
350
+ return { position: pos };
351
+ }
352
+
353
+ const readText = (p) => {
354
+ try {
355
+ return readFileSync(p, "utf8");
356
+ } catch {
357
+ return null;
358
+ }
359
+ };
360
+
361
+ async function locationTool(name, args, ctx) {
362
+ const c = contextFor(args);
363
+ if (c.error) return c.error;
364
+ const wantsIndex = name === "code_references";
365
+ const report = c.language === "swift" ? indexReport(c.root, c.source) : null;
366
+
367
+ return withServer(c.spec, async ({ client, openDoc, root }) => {
368
+ const doc = openDoc(args.file);
369
+ if (doc.error) return doc.error;
370
+ const p = await positionFor(args, client, doc.uri, doc.text);
371
+ if (p.error) return p.error;
372
+
373
+ let indexReady = true;
374
+ if (wantsIndex) {
375
+ const budget = Math.max(0, Number(args.index_wait_sec ?? DEFAULT_INDEX_WAIT_MS / 1000) * 1000);
376
+ indexReady = await client.waitForIndex(budget);
377
+ }
378
+
379
+ const method = wantsIndex ? "textDocument/references" : "textDocument/definition";
380
+ const params = {
381
+ textDocument: { uri: doc.uri },
382
+ position: p.position,
383
+ ...(wantsIndex
384
+ ? { context: { includeDeclaration: args.include_declaration !== false } }
385
+ : {}),
386
+ };
387
+ const res = await client.request(method, params, { signal: ctx?.signal });
388
+ if (res.error) return `${ERROR_PREFIX}${res.error.message}`;
389
+
390
+ const shaped = shapeLocations(res.result, {
391
+ root,
392
+ maxResults: Number(args.max_results) || DEFAULT_MAX_RESULTS,
393
+ readFile: readText,
394
+ });
395
+ const out = {
396
+ language: c.language,
397
+ root,
398
+ buildSettingsSource: c.source,
399
+ semantic: report ? report.semantic : true,
400
+ ...(wantsIndex ? { indexReady } : {}),
401
+ ...shaped,
402
+ };
403
+ if (c.language === "kotlin") out.confidence = "alpha";
404
+ if (wantsIndex && !indexReady) {
405
+ const idx = client.indexing();
406
+ out.reason = `The background index had not finished when this answered${idx?.message ? ` (${idx.message})` : ""}, so an empty or short list here means "not indexed yet", not "no references". Raise index_wait_sec or run code_index_status.`;
407
+ } else if (wantsIndex && shaped.count === 0 && report && !report.semantic) {
408
+ out.reason = `This root resolved to "${c.source}" build settings, which cannot produce cross-file references. ${report.remedy[0] || ""}`.trim();
409
+ }
410
+ return out;
411
+ });
412
+ }
413
+
414
+ async function hoverTool(args, ctx) {
415
+ const c = contextFor(args);
416
+ if (c.error) return c.error;
417
+ return withServer(c.spec, async ({ client, openDoc, root }) => {
418
+ const doc = openDoc(args.file);
419
+ if (doc.error) return doc.error;
420
+ const p = await positionFor(args, client, doc.uri, doc.text);
421
+ if (p.error) return p.error;
422
+ const res = await client.request(
423
+ "textDocument/hover",
424
+ { textDocument: { uri: doc.uri }, position: p.position },
425
+ { signal: ctx?.signal },
426
+ );
427
+ if (res.error) return `${ERROR_PREFIX}${res.error.message}`;
428
+ const text = markdownFromHover(res.result?.contents);
429
+ const range = res.result?.range;
430
+ const out = {
431
+ language: c.language,
432
+ root,
433
+ buildSettingsSource: c.source,
434
+ semantic: c.language === "swift" ? indexReport(c.root, c.source).semantic : true,
435
+ documentation: text,
436
+ ...(range ? { range: fromLspPosition(doc.text, range.start) } : {}),
437
+ };
438
+ if (!text) out.reason = "the server returned no hover information at this position";
439
+ if (c.language === "kotlin") out.confidence = "alpha";
440
+ return out;
441
+ });
442
+ }
443
+
444
+ async function documentSymbolsTool(args, ctx) {
445
+ const c = contextFor(args);
446
+ if (c.error) return c.error;
447
+ const maxDepth = Number(args.max_depth) || 3;
448
+ return withServer(c.spec, async ({ client, openDoc }) => {
449
+ const doc = openDoc(args.file);
450
+ if (doc.error) return doc.error;
451
+ const res = await client.request(
452
+ "textDocument/documentSymbol",
453
+ { textDocument: { uri: doc.uri } },
454
+ { signal: ctx?.signal },
455
+ );
456
+ if (res.error) return `${ERROR_PREFIX}${res.error.message}`;
457
+ let count = 0;
458
+ const shape = (nodes, depth) =>
459
+ (nodes || []).map((s) => {
460
+ count += 1;
461
+ const r = s.selectionRange || s.range || s.location?.range || {};
462
+ const node = {
463
+ name: s.name,
464
+ kind: s.kind,
465
+ line: (r.start?.line ?? 0) + 1,
466
+ endLine: ((s.range || r).end?.line ?? 0) + 1,
467
+ };
468
+ if (depth < maxDepth && Array.isArray(s.children) && s.children.length) {
469
+ node.children = shape(s.children, depth + 1);
470
+ }
471
+ return node;
472
+ });
473
+ const symbols = shape(res.result, 1);
474
+ return { language: c.language, file: args.file, count, symbols };
475
+ });
476
+ }
477
+
478
+ async function workspaceSymbolsTool(args, ctx) {
479
+ const root = args.workspace_root;
480
+ if (!root || !isAbsolute(root)) return `${ERROR_PREFIX}workspace_root must be an absolute path`;
481
+ const language = args.language || "swift";
482
+ const spec = language === "swift" ? swiftSpec(root) : kotlinSpec(root);
483
+ if (spec.error) return spec.error;
484
+ const report = language === "swift" ? indexReport(root, classifyRoot(root)) : null;
485
+
486
+ return withServer(spec, async ({ client }) => {
487
+ const budget = Math.max(0, Number(args.index_wait_sec ?? DEFAULT_INDEX_WAIT_MS / 1000) * 1000);
488
+ const indexReady = await client.waitForIndex(budget);
489
+ const res = await client.request(
490
+ "workspace/symbol",
491
+ { query: String(args.query || "") },
492
+ { signal: ctx?.signal },
493
+ );
494
+ if (res.error) return `${ERROR_PREFIX}${res.error.message}`;
495
+ const shaped = shapeLocations(
496
+ (res.result || []).map((s) => ({ uri: s.location?.uri, range: s.location?.range, name: s.name })),
497
+ { root, maxResults: Number(args.max_results) || DEFAULT_MAX_RESULTS, readFile: readText },
498
+ );
499
+ shaped.results = shaped.results.map((r, i) => ({ ...r, name: res.result[i]?.name }));
500
+ const out = {
501
+ language,
502
+ root,
503
+ query: args.query,
504
+ semantic: report ? report.semantic : true,
505
+ indexReady,
506
+ ...shaped,
507
+ };
508
+ if (!indexReady) {
509
+ out.reason =
510
+ "The background index had not finished, so this list is incomplete rather than exhaustive.";
511
+ } else if (shaped.count === 0 && report && !report.semantic) {
512
+ out.reason = `This root resolved to "${report.buildSettingsSource}" build settings and cannot answer workspace-wide questions. ${report.remedy[0] || ""}`.trim();
513
+ }
514
+ return out;
515
+ });
516
+ }
517
+
518
+ function classifyRoot(root) {
519
+ return resolveWorkspaceRoot(root, undefined).source;
520
+ }
521
+
522
+ async function diagnosticsTool(args, ctx) {
523
+ const c = contextFor(args);
524
+ if (c.error) return c.error;
525
+ const waitMs = Math.min(15000, Math.max(250, Number(args.wait_ms) || 4000));
526
+ const report = c.language === "swift" ? indexReport(c.root, c.source) : null;
527
+
528
+ return withServer(c.spec, async ({ client, openDoc }) => {
529
+ // Subscribe BEFORE opening the document: sourcekit-lsp publishes
530
+ // diagnostics unsolicited after didOpen, so a listener attached afterwards
531
+ // races the very message it is waiting for.
532
+ let resolveFirst;
533
+ const got = new Promise((r) => (resolveFirst = r));
534
+ const collected = [];
535
+ let uri = null;
536
+ const unsubscribe = client.subscribe((msg) => {
537
+ if (msg.method !== "textDocument/publishDiagnostics") return;
538
+ if (!uri || msg.params?.uri !== uri) return;
539
+ collected.length = 0;
540
+ collected.push(...(msg.params.diagnostics || []));
541
+ resolveFirst(true);
542
+ });
543
+ const timer = setTimeout(() => resolveFirst(false), waitMs);
544
+ if (ctx?.signal) ctx.signal.addEventListener("abort", () => resolveFirst(false), { once: true });
545
+
546
+ const doc = openDoc(args.file);
547
+ if (doc.error) {
548
+ clearTimeout(timer);
549
+ unsubscribe();
550
+ return doc.error;
551
+ }
552
+ uri = doc.uri;
553
+ const arrived = await got;
554
+ clearTimeout(timer);
555
+ unsubscribe();
556
+
557
+ const semantic = report ? report.semantic : true;
558
+ const diagnostics = collected.map((d) => ({
559
+ severity: ["", "error", "warning", "information", "hint"][d.severity] || "unknown",
560
+ line: (d.range?.start?.line ?? 0) + 1,
561
+ column: (d.range?.start?.character ?? 0) + 1,
562
+ message: d.message,
563
+ source: d.source,
564
+ code: typeof d.code === "object" ? d.code?.value : d.code,
565
+ }));
566
+ const out = {
567
+ language: c.language,
568
+ file: args.file,
569
+ root: c.root,
570
+ buildSettingsSource: c.source,
571
+ semantic,
572
+ count: diagnostics.length,
573
+ diagnostics,
574
+ };
575
+ if (!arrived) {
576
+ out.reason = `the server published no diagnostics within ${waitMs}ms - this is "did not look", not "clean"`;
577
+ }
578
+ if (!semantic) {
579
+ out.reason =
580
+ `This root resolved to "${c.source}" build settings, so the compiler is invoked without the project's module graph. Errors like "no such module" here are artefacts of that, not findings. ` +
581
+ (out.reason || "");
582
+ }
583
+ return out;
584
+ });
585
+ }
586
+
587
+ function indexStatusTool(args) {
588
+ const file = args.file;
589
+ // "Always succeeds" is a promise about a MISSING TOOLCHAIN, not about bad
590
+ // input, and implementing it as "never validates" made the two answers
591
+ // identical. A path that is relative, absent, or literally the string
592
+ // "undefined" resolved to source "fallback" and semantic:false - the same
593
+ // answer an unconfigured repo gets - and then printed a remedy telling the
594
+ // caller to point --workspace_root at a project root, sending them after a
595
+ // build-system problem that does not exist. Every other tool in this family
596
+ // refuses such a path by name; this one now does too.
597
+ for (const [value, label] of [
598
+ [file, "file"],
599
+ [args.workspace_root, "workspace_root"],
600
+ ]) {
601
+ if (value === undefined || value === null) continue;
602
+ if (typeof value !== "string" || !value.trim()) {
603
+ return `${ERROR_PREFIX}${label} must be a non-empty path`;
604
+ }
605
+ if (!isAbsolute(value)) {
606
+ return `${ERROR_PREFIX}${label} must be an absolute path, got "${value}"`;
607
+ }
608
+ if (!existsSync(value)) return `${ERROR_PREFIX}no such ${label}: ${value}`;
609
+ }
610
+ let swiftRoot = args.workspace_root;
611
+ let swiftSource = swiftRoot ? classifyRoot(swiftRoot) : null;
612
+ if (!swiftRoot && file) {
613
+ const r = resolveWorkspaceRoot(file, undefined);
614
+ swiftRoot = r.root;
615
+ swiftSource = r.source;
616
+ }
617
+ const bin = resolveSwiftBin();
618
+ const swift = bin
619
+ ? swiftRoot
620
+ ? { available: true, bin, ...indexReport(swiftRoot, swiftSource) }
621
+ : { available: true, bin, reason: "give a workspace_root or a file to report on a root" }
622
+ : {
623
+ available: false,
624
+ reason: "sourcekit-lsp not found",
625
+ install: ["install Xcode", "or set SOURCEKIT_LSP_PATH"],
626
+ };
627
+
628
+ const kotlinRoot = args.workspace_root || (file ? resolveKotlinRoot(file, undefined).root : null);
629
+ return { swift, kotlin: kotlinReport(kotlinRoot), servers: poolStats() };
630
+ }
631
+
632
+ /**
633
+ * @param {string} name
634
+ * @param {object} args
635
+ * @param {object} ctx the per-request context, merged with the family's own
636
+ */
637
+ export async function handleCode(name, args = {}, ctx = {}) {
638
+ try {
639
+ switch (name) {
640
+ case "code_definition":
641
+ case "code_references":
642
+ return await locationTool(name, args, ctx);
643
+ case "code_hover":
644
+ return await hoverTool(args, ctx);
645
+ case "code_document_symbols":
646
+ return await documentSymbolsTool(args, ctx);
647
+ case "code_workspace_symbols":
648
+ return await workspaceSymbolsTool(args, ctx);
649
+ case "code_diagnostics":
650
+ return await diagnosticsTool(args, ctx);
651
+ case "code_index_status":
652
+ return indexStatusTool(args);
653
+ case "code_server_reset":
654
+ return resetServers({ root: args.workspace_root, language: args.language });
655
+ default:
656
+ return null;
657
+ }
658
+ } catch (e) {
659
+ // A handler must not throw: an exception here is an MCP-level error with no
660
+ // useful text, and one bad path would take the whole call down.
661
+ return `${ERROR_PREFIX}${name}: ${e?.message || String(e)}`;
662
+ }
663
+ }
664
+
665
+ export { shutdownAllLsp };