@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.
- package/CHANGELOG.md +140 -7
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.d.ts +3 -3
- package/dist/tool-result-renderer.js +65 -44
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +29 -5
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/mcp-output-guard.js
CHANGED
|
@@ -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 ??
|
|
48
|
-
|
|
49
|
-
|
|
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)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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) => ({
|
|
185
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
285
|
+
}
|
|
286
|
+
if (typeof value === "string") {
|
|
228
287
|
return byteLength(value);
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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-") &&
|
|
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) ||
|
|
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 {
|
|
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 ||
|
|
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" ||
|
|
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 ||
|
|
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) ||
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
508
|
+
}
|
|
509
|
+
if (["0", "false", "no", "off"].includes(value)) {
|
|
384
510
|
return false;
|
|
385
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
84
93
|
appendText(block.text);
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
};
|
|
87
97
|
return {
|
|
88
|
-
get empty() {
|
|
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
|
-
|
|
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
|
}
|
package/dist/mcp-panel.d.ts
CHANGED
|
@@ -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?: {
|