@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7

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 (88) hide show
  1. package/CHANGELOG.md +140 -7
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.d.ts +3 -3
  73. package/dist/tool-result-renderer.js +65 -44
  74. package/dist/types.d.ts +2 -2
  75. package/dist/types.js +39 -18
  76. package/dist/ui-message-history.js +33 -13
  77. package/dist/ui-resource-handler.d.ts +3 -3
  78. package/dist/ui-resource-handler.js +37 -21
  79. package/dist/ui-server.js +101 -43
  80. package/dist/ui-session.js +66 -29
  81. package/dist/ui-stream-types.js +14 -4
  82. package/dist/ui-tool-visibility.js +12 -6
  83. package/dist/unix-socket-transport.d.ts +1 -2
  84. package/dist/unix-socket-transport.js +19 -12
  85. package/dist/utils.d.ts +2 -0
  86. package/dist/utils.js +60 -35
  87. package/package.json +29 -5
  88. package/skills/mcp-scripting/SKILL.md +6 -6
@@ -44,12 +44,19 @@ export async function guardMcpOutput(content, options = {}) {
44
44
  const normalizedContent = withEmptyTextFallback(content.length > 0
45
45
  ? sanitizeContent(content)
46
46
  : [{ type: "text", text: options.emptyTextFallback ?? "(empty result)" }], options.emptyTextFallback);
47
- const retained = options.retainedMcpResult ?? (options.rawMcpResult === undefined ? {} : await retainMcpResult(options.rawMcpResult, options));
48
- const reference = retained.resultRef ? formatMcpResultReference(retained.resultRef)
49
- : retained.resultWriteError ? `[MCP result could not be saved: ${retained.resultWriteError}]` : undefined;
47
+ const retained = options.retainedMcpResult ??
48
+ (options.rawMcpResult === undefined
49
+ ? {}
50
+ : await retainMcpResult(options.rawMcpResult, options));
51
+ const reference = retained.resultRef
52
+ ? formatMcpResultReference(retained.resultRef)
53
+ : retained.resultWriteError
54
+ ? `[MCP result could not be saved: ${retained.resultWriteError}]`
55
+ : undefined;
50
56
  const withReference = (blocks) => reference ? [...blocks, { type: "text", text: reference }] : blocks;
51
- if (options.enabled === false)
57
+ if (options.enabled === false) {
52
58
  return { content: withReference(addAffixes(normalizedContent, prefix, suffix)), ...retained };
59
+ }
53
60
  const imageBlocks = normalizedContent.filter((block) => block.type === "image");
54
61
  const textOutput = normalizedContent
55
62
  .filter((block) => block.type === "text")
@@ -58,7 +65,7 @@ export async function guardMcpOutput(content, options = {}) {
58
65
  const composedOutput = `${prefix}${textOutput}${suffix}`;
59
66
  const stats = textStats(composedOutput);
60
67
  const referenceSuffix = reference ? `\n${reference}` : "";
61
- const totalStats = textStats(composedOutput ? composedOutput + referenceSuffix : reference ?? "");
68
+ const totalStats = textStats(composedOutput ? composedOutput + referenceSuffix : (reference ?? ""));
62
69
  let guardedContent = addAffixes(normalizedContent, prefix, suffix);
63
70
  let outputGuard;
64
71
  if (totalStats.bytes > maxBytes || totalStats.lines > maxLines) {
@@ -72,19 +79,22 @@ export async function guardMcpOutput(content, options = {}) {
72
79
  };
73
80
  }
74
81
  function withEmptyTextFallback(content, fallback) {
75
- if (!fallback)
82
+ if (!fallback) {
76
83
  return content;
84
+ }
77
85
  const textOutput = content
78
86
  .filter((block) => block.type === "text")
79
87
  .map((block) => block.text)
80
88
  .join("\n");
81
- if (textOutput)
89
+ if (textOutput) {
82
90
  return content;
91
+ }
83
92
  return [{ type: "text", text: fallback }, ...content.filter((block) => block.type === "image")];
84
93
  }
85
94
  function addAffixes(content, prefix, suffix) {
86
- if (!prefix && !suffix)
95
+ if (!prefix && !suffix) {
87
96
  return content;
97
+ }
88
98
  const next = [...content];
89
99
  if (prefix) {
90
100
  const index = next.findIndex((block) => block.type === "text");
@@ -123,26 +133,47 @@ export async function retainMcpResult(result, options = {}, rawResult = false) {
123
133
  const raw = safeStringify(result);
124
134
  const rawBytes = byteLength(raw);
125
135
  const record = asRecord(result);
126
- const content = Array.isArray(record?.content) ? record.content
127
- : Array.isArray(record?.contents) ? record.contents.map(resource => ({ type: "resource", resource })) : [];
128
- const payloadFiles = (await Promise.all(content.map(async (block, index) => {
129
- const item = asRecord(block);
130
- const resource = asRecord(item?.resource);
131
- const data = item?.type === "audio" ? item.data : item?.type === "resource" ? resource?.blob : undefined;
132
- if (typeof data !== "string")
133
- return undefined;
134
- const mimeType = item?.mimeType ?? resource?.mimeType;
135
- return { index, kind: item.type, ...(typeof resource?.uri === "string" ? { uri: resource.uri } : {}), ...(typeof mimeType === "string" ? { mimeType } : {}), ...await saveArtifact("payload", Buffer.from(data, "base64"), options.outputDirectory) };
136
- }))).filter((file) => file !== undefined);
137
- const oversized = options.enabled !== false && rawBytes > (options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES);
138
- const shouldSave = oversized || !record || Object.keys(record).some(key => key !== "content" && key !== "isError")
139
- || content.some(block => {
136
+ const content = Array.isArray(record?.content)
137
+ ? record.content
138
+ : Array.isArray(record?.contents)
139
+ ? record.contents.map((resource) => ({ type: "resource", resource }))
140
+ : [];
141
+ const oversized = options.enabled !== false &&
142
+ rawBytes > (options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES);
143
+ const shouldSave = oversized ||
144
+ !record ||
145
+ Object.keys(record).some((key) => key !== "content" && key !== "isError") ||
146
+ content.some((block) => {
140
147
  const item = asRecord(block);
141
- return item?.type !== "text" || Object.keys(item).some(key => key !== "type" && key !== "text");
148
+ return (item?.type !== "text" || Object.keys(item).some((key) => key !== "type" && key !== "text"));
142
149
  });
143
- if (!shouldSave)
150
+ if (!shouldSave) {
144
151
  return { mcpResult: result };
145
- const artifact = await saveArtifact("mcp-result", raw, options.outputDirectory);
152
+ }
153
+ const [rawPayloadFiles, artifact] = await Promise.all([
154
+ Promise.all(content.map(async (block, index) => {
155
+ const item = asRecord(block);
156
+ const resource = asRecord(item?.resource);
157
+ const data = item?.type === "audio"
158
+ ? item.data
159
+ : item?.type === "resource"
160
+ ? resource?.blob
161
+ : undefined;
162
+ if (typeof data !== "string") {
163
+ return undefined;
164
+ }
165
+ const mimeType = item?.mimeType ?? resource?.mimeType;
166
+ return {
167
+ index,
168
+ kind: item.type,
169
+ ...(typeof resource?.uri === "string" ? { uri: resource.uri } : {}),
170
+ ...(typeof mimeType === "string" ? { mimeType } : {}),
171
+ ...(await saveArtifact("payload", Buffer.from(data, "base64"), options.outputDirectory)),
172
+ };
173
+ })),
174
+ saveArtifact("mcp-result", raw, options.outputDirectory),
175
+ ]);
176
+ const payloadFiles = rawPayloadFiles.filter((file) => file !== undefined);
146
177
  return {
147
178
  mcpResult: oversized && !rawResult ? summarizeMcpResult(result, rawBytes, artifact) : result,
148
179
  ...(artifact.path ? { resultRef: artifact.path } : {}),
@@ -181,26 +212,49 @@ function summarizeMcpResult(result, rawBytes, artifact) {
181
212
  const extraFields = Object.keys(record)
182
213
  .filter((key) => !standard.has(key))
183
214
  .slice(0, KEY_PREVIEW_LIMIT)
184
- .map((key) => ({ key: truncateKey(key), type: typeof record[key], estimatedBytes: estimateValueBytes(record[key]), omitted: true }));
185
- if (extraFields.length > 0)
215
+ .map((key) => ({
216
+ key: truncateKey(key),
217
+ type: typeof record[key],
218
+ estimatedBytes: estimateValueBytes(record[key]),
219
+ omitted: true,
220
+ }));
221
+ if (extraFields.length > 0) {
186
222
  summary.extraFields = extraFields;
223
+ }
187
224
  }
188
225
  return summary;
189
226
  }
190
227
  function summarizeContent(content) {
191
- const summaries = content.slice(0, CONTENT_SUMMARY_LIMIT).map((block) => {
228
+ const summaries = content
229
+ .slice(0, CONTENT_SUMMARY_LIMIT)
230
+ .map((block) => {
192
231
  const record = asRecord(block);
193
- if (!record)
232
+ if (!record) {
194
233
  return { type: typeof block, omitted: true };
234
+ }
195
235
  if (record.type === "text") {
196
236
  const text = typeof record.text === "string" ? record.text : "";
197
- return { type: "text", bytes: byteLength(text), lines: textStats(text).lines, textOmitted: true };
237
+ return {
238
+ type: "text",
239
+ bytes: byteLength(text),
240
+ lines: textStats(text).lines,
241
+ textOmitted: true,
242
+ };
198
243
  }
199
244
  if (record.type === "image") {
200
245
  const data = typeof record.data === "string" ? record.data : "";
201
- return { type: "image", mimeType: typeof record.mimeType === "string" ? record.mimeType : undefined, dataBytes: byteLength(data), dataOmitted: true };
246
+ return {
247
+ type: "image",
248
+ mimeType: typeof record.mimeType === "string" ? record.mimeType : undefined,
249
+ dataBytes: byteLength(data),
250
+ dataOmitted: true,
251
+ };
202
252
  }
203
- return { type: typeof record.type === "string" ? record.type : "unknown", estimatedBytes: estimateValueBytes(record), omitted: true };
253
+ return {
254
+ type: typeof record.type === "string" ? record.type : "unknown",
255
+ estimatedBytes: estimateValueBytes(record),
256
+ omitted: true,
257
+ };
204
258
  });
205
259
  if (content.length > CONTENT_SUMMARY_LIMIT) {
206
260
  summaries.push({ type: "omitted", count: content.length - CONTENT_SUMMARY_LIMIT });
@@ -210,7 +264,11 @@ function summarizeContent(content) {
210
264
  function summarizeValue(value) {
211
265
  const record = asRecord(value);
212
266
  if (!record) {
213
- return { type: value === null ? "null" : typeof value, estimatedBytes: estimateValueBytes(value), omitted: true };
267
+ return {
268
+ type: value === null ? "null" : typeof value,
269
+ estimatedBytes: estimateValueBytes(value),
270
+ omitted: true,
271
+ };
214
272
  }
215
273
  const keys = Object.keys(record);
216
274
  return {
@@ -222,16 +280,22 @@ function summarizeValue(value) {
222
280
  };
223
281
  }
224
282
  function estimateValueBytes(value, depth = 0) {
225
- if (value === null || value === undefined)
283
+ if (value === null || value === undefined) {
226
284
  return 0;
227
- if (typeof value === "string")
285
+ }
286
+ if (typeof value === "string") {
228
287
  return byteLength(value);
229
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
288
+ }
289
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
230
290
  return byteLength(String(value));
291
+ }
231
292
  const record = asRecord(value);
232
- if (!record || depth >= 2)
293
+ if (!record || depth >= 2) {
233
294
  return 0;
234
- const values = Array.isArray(value) ? value.slice(0, KEY_PREVIEW_LIMIT) : Object.values(record).slice(0, KEY_PREVIEW_LIMIT);
295
+ }
296
+ const values = Array.isArray(value)
297
+ ? value.slice(0, KEY_PREVIEW_LIMIT)
298
+ : Object.values(record).slice(0, KEY_PREVIEW_LIMIT);
235
299
  return values.reduce((total, item) => total + estimateValueBytes(item, depth + 1), 0);
236
300
  }
237
301
  function truncateKey(key) {
@@ -240,8 +304,9 @@ function truncateKey(key) {
240
304
  async function saveArtifact(kind, text, outputDirectory) {
241
305
  try {
242
306
  const parent = outputDirectory === undefined ? tmpdir() : resolve(outputDirectory);
243
- if (outputDirectory !== undefined)
307
+ if (outputDirectory !== undefined) {
244
308
  await mkdir(parent, { recursive: true, mode: 0o700 });
309
+ }
245
310
  const dir = await mkdtemp(join(parent, "pi-mcp-output-"));
246
311
  const path = join(dir, `${kind}-${randomBytes(4).toString("hex")}.${typeof text === "string" ? "txt" : "bin"}`);
247
312
  await writeFile(path, text, { encoding: "utf8", mode: 0o600 });
@@ -254,13 +319,15 @@ async function saveArtifact(kind, text, outputDirectory) {
254
319
  /** Stream histories through the same private output-artifact and paged-reader boundary. */
255
320
  export async function saveMcpOutputStream(chunks, outputDirectory) {
256
321
  const artifact = await saveArtifact("output", "", outputDirectory);
257
- if (!artifact.path)
322
+ if (!artifact.path) {
258
323
  return artifact;
324
+ }
259
325
  try {
260
326
  const file = await open(artifact.path, "a");
261
327
  try {
262
- for await (const chunk of chunks)
328
+ for await (const chunk of chunks) {
263
329
  await file.writeFile(chunk);
330
+ }
264
331
  }
265
332
  finally {
266
333
  await file.close();
@@ -273,47 +340,85 @@ export async function saveMcpOutputStream(chunks, outputDirectory) {
273
340
  }
274
341
  export async function readMcpResult(input, options = {}) {
275
342
  try {
276
- if (!input || typeof input.ref !== "string")
343
+ if (!input || typeof input.ref !== "string") {
277
344
  throw new Error("ref must be a retained MCP result path");
345
+ }
278
346
  // Script-only hosts deliberately have no general filesystem capability.
279
- const root = await realpath(options.outputDirectory === undefined ? tmpdir() : resolve(options.outputDirectory));
280
- const ref = await realpath(resolve(input.ref));
281
- if (dirname(dirname(ref)) !== root || !/^pi-mcp-output-/.test(basename(dirname(ref)))
282
- || !/^(mcp-result|output)-[a-f0-9]+\.txt$/.test(basename(ref)))
347
+ const [root, ref] = await Promise.all([
348
+ realpath(options.outputDirectory === undefined ? tmpdir() : resolve(options.outputDirectory)),
349
+ realpath(resolve(input.ref)),
350
+ ]);
351
+ if (dirname(dirname(ref)) !== root ||
352
+ !basename(dirname(ref)).startsWith("pi-mcp-output-") ||
353
+ !/^(mcp-result|output)-[a-f0-9]+\.txt$/.test(basename(ref))) {
283
354
  throw new Error("ref is not an MCP output artifact in this host's output directory");
355
+ }
284
356
  // Plain output pages without flattening a complete UI history into memory.
285
- if (basename(ref).startsWith("output-") && input.path === undefined && input.fields === undefined) {
357
+ if (basename(ref).startsWith("output-") &&
358
+ input.path === undefined &&
359
+ input.fields === undefined) {
286
360
  const offset = input.offset ?? 0;
287
361
  const limit = input.limit ?? 12_000;
288
- if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1)
362
+ if (!Number.isSafeInteger(offset) ||
363
+ offset < 0 ||
364
+ !Number.isSafeInteger(limit) ||
365
+ limit < 1) {
289
366
  throw new Error("offset must be a non-negative integer and limit a positive integer");
367
+ }
290
368
  const maxBytes = Math.max(1, (options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES) - 256);
291
369
  const maxLines = Math.max(1, (options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES) - 1);
292
370
  const { page, total } = await readPlainResult(ref, offset, limit, maxBytes, maxLines);
293
371
  const end = offset + page.length;
294
372
  const nextOffset = end < total ? end : null;
295
- return { content: [{ type: "text", text: page }, ...(nextOffset !== null ? [{ type: "text", text: `[MCP result page: ${offset}–${end} of ${total} characters. Continue with offset: ${end}.]` }] : [])], details: { ref, offset, nextOffset, totalCharacters: total } };
373
+ return {
374
+ content: [
375
+ { type: "text", text: page },
376
+ ...(nextOffset !== null
377
+ ? [
378
+ {
379
+ type: "text",
380
+ text: `[MCP result page: ${offset}–${end} of ${total} characters. Continue with offset: ${end}.]`,
381
+ },
382
+ ]
383
+ : []),
384
+ ],
385
+ details: { ref, offset, nextOffset, totalCharacters: total },
386
+ };
296
387
  }
297
388
  let rendered = await readFile(ref, "utf8");
298
- if (input.path !== undefined || input.fields !== undefined || basename(ref).startsWith("mcp-result-")) {
389
+ if (input.path !== undefined ||
390
+ input.fields !== undefined ||
391
+ basename(ref).startsWith("mcp-result-")) {
299
392
  let value = JSON.parse(rendered);
300
393
  if (input.path !== undefined && input.path !== "") {
301
- if (typeof input.path !== "string" || !input.path.startsWith("/") || /~(?:[^01]|$)/.test(input.path))
394
+ if (typeof input.path !== "string" ||
395
+ !input.path.startsWith("/") ||
396
+ /~(?:[^01]|$)/.test(input.path)) {
302
397
  throw new Error("path must be an RFC 6901 JSON Pointer");
398
+ }
303
399
  for (const token of input.path.slice(1).split("/")) {
304
400
  const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
305
- if (value === null || typeof value !== "object" || (Array.isArray(value) && !/^(0|[1-9]\d*)$/.test(key)) || !Object.hasOwn(value, key))
401
+ if (value === null ||
402
+ typeof value !== "object" ||
403
+ (Array.isArray(value) && !/^(0|[1-9]\d*)$/.test(key)) ||
404
+ !Object.hasOwn(value, key)) {
306
405
  throw new Error(`JSON Pointer does not exist: ${input.path}`);
406
+ }
307
407
  value = value[key];
308
408
  }
309
409
  }
310
410
  if (input.fields !== undefined) {
311
- if (!Array.isArray(input.fields) || input.fields.some(field => typeof field !== "string"))
411
+ if (!Array.isArray(input.fields) ||
412
+ input.fields.some((field) => typeof field !== "string")) {
312
413
  throw new Error("fields must be an array of keys");
414
+ }
313
415
  const select = (item) => {
314
- if (!item || typeof item !== "object" || Array.isArray(item))
416
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
315
417
  throw new Error("fields requires an object or array of objects");
316
- return Object.fromEntries(input.fields.filter(key => Object.hasOwn(item, key)).map(key => [key, item[key]]));
418
+ }
419
+ return Object.fromEntries(input
420
+ .fields.filter((key) => Object.hasOwn(item, key))
421
+ .map((key) => [key, item[key]]));
317
422
  };
318
423
  value = Array.isArray(value) ? value.map(select) : select(value);
319
424
  }
@@ -321,10 +426,12 @@ export async function readMcpResult(input, options = {}) {
321
426
  }
322
427
  const offset = input.offset ?? 0;
323
428
  const limit = input.limit ?? 12_000;
324
- if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1)
429
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1) {
325
430
  throw new Error("offset must be a non-negative integer and limit a positive integer");
326
- if (offset > 0 && rendered.codePointAt(offset - 1) > 0xffff)
431
+ }
432
+ if (offset > 0 && rendered.codePointAt(offset - 1) > 0xffff) {
327
433
  throw new Error("offset must not split a Unicode surrogate pair");
434
+ }
328
435
  const maxBytes = options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES;
329
436
  const maxLines = options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES;
330
437
  const notice = (end) => `[MCP result page: ${offset}–${end} of ${rendered.length} characters. Continue with offset: ${end}.]`;
@@ -336,11 +443,13 @@ export async function readMcpResult(input, options = {}) {
336
443
  const width = rendered.codePointAt(end) > 0xffff ? 2 : 1;
337
444
  const char = rendered.slice(end, end + width);
338
445
  bytes += byteLength(char);
339
- if (char === "\n")
446
+ if (char === "\n") {
340
447
  lines++;
448
+ }
341
449
  // Tiny caps still return one complete character plus recovery guidance.
342
- if (end > offset && (bytes > byteCap || lines > lineCap || end + width - offset > limit))
450
+ if (end > offset && (bytes > byteCap || lines > lineCap || end + width - offset > limit)) {
343
451
  break;
452
+ }
344
453
  end += width;
345
454
  }
346
455
  return end;
@@ -351,11 +460,26 @@ export async function readMcpResult(input, options = {}) {
351
460
  end = pageEnd(maxBytes - byteLength(notice(rendered.length)) - 1, maxLines - 1);
352
461
  }
353
462
  const nextOffset = end < rendered.length ? end : null;
354
- return { content: [{ type: "text", text: rendered.slice(offset, end) }, ...(nextOffset !== null ? [{ type: "text", text: notice(end) }] : [])], details: { ref, offset, nextOffset, totalCharacters: rendered.length, ...(input.path !== undefined ? { path: input.path } : {}) } };
463
+ return {
464
+ content: [
465
+ { type: "text", text: rendered.slice(offset, end) },
466
+ ...(nextOffset !== null ? [{ type: "text", text: notice(end) }] : []),
467
+ ],
468
+ details: {
469
+ ref,
470
+ offset,
471
+ nextOffset,
472
+ totalCharacters: rendered.length,
473
+ ...(input.path !== undefined ? { path: input.path } : {}),
474
+ },
475
+ };
355
476
  }
356
477
  catch (error) {
357
478
  const message = error instanceof Error ? error.message : String(error);
358
- return { content: [{ type: "text", text: message }], details: { error: "result_read_failed", message } };
479
+ return {
480
+ content: [{ type: "text", text: message }],
481
+ details: { error: "result_read_failed", message },
482
+ };
359
483
  }
360
484
  }
361
485
  function asRecord(value) {
@@ -371,18 +495,22 @@ function safeStringify(value) {
371
495
  }
372
496
  }
373
497
  function positiveInt(value) {
374
- if (typeof value !== "number" || !Number.isFinite(value))
498
+ if (typeof value !== "number" || !Number.isFinite(value)) {
375
499
  return undefined;
500
+ }
376
501
  const integer = Math.floor(value);
377
502
  return integer > 0 ? integer : undefined;
378
503
  }
379
504
  function envKillSwitch(name) {
380
505
  const value = process.env[name]?.trim().toLowerCase();
381
- if (!value)
506
+ if (!value) {
382
507
  return undefined;
383
- if (["0", "false", "no", "off"].includes(value))
508
+ }
509
+ if (["0", "false", "no", "off"].includes(value)) {
384
510
  return false;
385
- if (["1", "true", "yes", "on"].includes(value))
511
+ }
512
+ if (["1", "true", "yes", "on"].includes(value)) {
386
513
  return true;
514
+ }
387
515
  return undefined;
388
516
  }
@@ -20,14 +20,17 @@ export function createMcpOutputSpool(options) {
20
20
  let artifact = {};
21
21
  let fd;
22
22
  let pending = [], pendingLength = 0;
23
- const lines = () => bytes === 0 ? 0 : newlines + 1;
23
+ const lines = () => (bytes === 0 ? 0 : newlines + 1);
24
24
  const fail = (error) => {
25
25
  artifact = { error: error instanceof Error ? error.message : String(error) };
26
- if (fd !== undefined)
26
+ if (fd !== undefined) {
27
27
  try {
28
28
  closeSync(fd);
29
29
  }
30
- catch { /* the write error is the reported failure */ }
30
+ catch {
31
+ /* the write error is the reported failure */
32
+ }
33
+ }
31
34
  fd = undefined;
32
35
  };
33
36
  const flush = () => {
@@ -47,29 +50,34 @@ export function createMcpOutputSpool(options) {
47
50
  // Each UTF-16 unit is at least one UTF-8 byte, so the head exceeds every preview budget.
48
51
  let end = Math.min(piece.length, maxBytes + 1 - headBytes);
49
52
  const last = piece.charCodeAt(end - 1);
50
- if (end < piece.length && last >= 0xd800 && last <= 0xdbff)
53
+ if (end < piece.length && last >= 0xd800 && last <= 0xdbff) {
51
54
  end++;
55
+ }
52
56
  const part = piece.slice(0, end);
53
57
  head += part;
54
58
  headBytes += byteLength(part);
55
59
  }
56
- if (fd === undefined)
60
+ if (fd === undefined) {
57
61
  return;
58
- if (pendingLength + piece.length > SPOOL_WRITE_CHUNK)
62
+ }
63
+ if (pendingLength + piece.length > SPOOL_WRITE_CHUNK) {
59
64
  flush();
65
+ }
60
66
  pending.push(piece);
61
67
  pendingLength += piece.length;
62
68
  };
63
69
  const appendText = (text) => {
64
- if (written++ > 0)
70
+ if (written++ > 0) {
65
71
  append("\n");
72
+ }
66
73
  append(text);
67
74
  };
68
75
  const spill = (blocks) => {
69
76
  try {
70
77
  const parent = options.outputDirectory === undefined ? tmpdir() : resolve(options.outputDirectory);
71
- if (options.outputDirectory !== undefined)
78
+ if (options.outputDirectory !== undefined) {
72
79
  mkdirSync(parent, { recursive: true, mode: 0o700 });
80
+ }
73
81
  const path = join(mkdtempSync(join(parent, "pi-mcp-output-")), `output-${randomBytes(4).toString("hex")}.txt`);
74
82
  fd = openSync(path, "wx", 0o600);
75
83
  artifact = { path };
@@ -78,14 +86,18 @@ export function createMcpOutputSpool(options) {
78
86
  fail(error);
79
87
  }
80
88
  for (const block of blocks) {
81
- if (block.type === "image")
89
+ if (block.type === "image") {
82
90
  images.push(block);
83
- else
91
+ }
92
+ else {
84
93
  appendText(block.text);
94
+ }
85
95
  }
86
96
  };
87
97
  return {
88
- get empty() { return pushed === 0; },
98
+ get empty() {
99
+ return pushed === 0;
100
+ },
89
101
  push(block) {
90
102
  pushed++;
91
103
  if (block.type === "image") {
@@ -95,8 +107,9 @@ export function createMcpOutputSpool(options) {
95
107
  const separator = textBlocks++ > 0 ? 1 : 0;
96
108
  bytes += separator + byteLength(block.text);
97
109
  newlines += separator + countNewlines(block.text);
98
- if (!retained)
110
+ if (!retained) {
99
111
  appendText(block.text);
112
+ }
100
113
  else {
101
114
  retained.push(block);
102
115
  if (options.enabled !== false && (bytes > maxBytes || lines() > maxLines)) {
@@ -107,8 +120,9 @@ export function createMcpOutputSpool(options) {
107
120
  }
108
121
  },
109
122
  finish() {
110
- if (retained)
123
+ if (retained) {
111
124
  return { content: retained };
125
+ }
112
126
  flush();
113
127
  if (fd !== undefined) {
114
128
  try {
@@ -145,14 +159,16 @@ export function truncatedOutput(text, stats, { path: fullOutputPath, error: writ
145
159
  }
146
160
  function countNewlines(text) {
147
161
  let count = 0;
148
- for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1))
162
+ for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
149
163
  count++;
164
+ }
150
165
  return count;
151
166
  }
152
167
  export function sanitizeContent(content) {
153
168
  return content.map((block) => {
154
- if (block.type !== "image")
169
+ if (block.type !== "image") {
155
170
  return block;
171
+ }
156
172
  const mimeType = typeof block.mimeType === "string" && block.mimeType.trim()
157
173
  ? block.mimeType.trim().slice(0, 100)
158
174
  : "image/png";
@@ -171,8 +187,9 @@ function truncateHead(text, maxBytes, maxLines) {
171
187
  const output = [];
172
188
  let bytes = 0;
173
189
  for (const line of lines) {
174
- if (output.length >= maxLines)
190
+ if (output.length >= maxLines) {
175
191
  break;
192
+ }
176
193
  const separatorBytes = output.length > 0 ? 1 : 0;
177
194
  const lineBytes = byteLength(line);
178
195
  if (bytes + separatorBytes + lineBytes > maxBytes) {
@@ -190,12 +207,14 @@ function truncateHead(text, maxBytes, maxLines) {
190
207
  return { content, bytes: stats.bytes, lines: stats.lines };
191
208
  }
192
209
  function truncateStringToBytes(value, maxBytes) {
193
- if (byteLength(value) <= maxBytes)
210
+ if (byteLength(value) <= maxBytes) {
194
211
  return value;
212
+ }
195
213
  const buffer = Buffer.from(value, "utf8");
196
214
  let end = Number.isFinite(maxBytes) ? Math.max(0, Math.floor(maxBytes)) : 0;
197
- while (end > 0 && (buffer.readUInt8(end) & 0xc0) === 0x80)
215
+ while (end > 0 && (buffer.readUInt8(end) & 0xc0) === 0x80) {
198
216
  end--;
217
+ }
199
218
  return buffer.subarray(0, end).toString("utf8");
200
219
  }
201
220
  function formatTruncationNotice(stats, fullOutputPath, writeError) {
@@ -212,9 +231,11 @@ export function byteLength(text) {
212
231
  return Buffer.byteLength(text, "utf8");
213
232
  }
214
233
  function formatSize(bytes) {
215
- if (bytes < 1024)
234
+ if (bytes < 1024) {
216
235
  return `${bytes} B`;
217
- if (bytes < 1024 * 1024)
236
+ }
237
+ if (bytes < 1024 * 1024) {
218
238
  return `${(bytes / 1024).toFixed(1)} KiB`;
239
+ }
219
240
  return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
220
241
  }
@@ -2,9 +2,9 @@ import { type PanelKeybindings } from "./panel-keys.ts";
2
2
  import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.ts";
3
3
  import { type MetadataCache } from "./metadata-cache.ts";
4
4
  declare class McpPanel {
5
- private noticeLines;
6
- private prefix;
7
- private servers;
5
+ private readonly noticeLines;
6
+ private readonly prefix;
7
+ private readonly servers;
8
8
  private cursorIndex;
9
9
  private nameQuery;
10
10
  private descSearchActive;
@@ -17,14 +17,14 @@ declare class McpPanel {
17
17
  private authInFlight;
18
18
  private inactivityTimeout;
19
19
  private visibleItems;
20
- private tui;
21
- private t;
22
- private authOnly;
23
- private keys;
20
+ private readonly tui;
21
+ private readonly t;
22
+ private readonly authOnly;
23
+ private readonly keys;
24
24
  private static readonly MAX_VISIBLE;
25
25
  private static readonly INACTIVITY_MS;
26
- private callbacks;
27
- private done;
26
+ private readonly callbacks;
27
+ private readonly done;
28
28
  constructor(config: McpConfig, cache: MetadataCache | null, provenance: Map<string, ServerProvenance>, callbacks: McpPanelCallbacks, tui: {
29
29
  requestRender(): void;
30
30
  }, done: (result: McpPanelResult) => void, options?: {