@fitchmultz/pi-mcp-adapter 5.0.0 → 7.0.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.
- package/CHANGELOG.md +50 -0
- package/OAUTH.md +3 -3
- package/README.md +160 -88
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/config.js +41 -16
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/mcp-auth-flow.js +2 -2
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/mcp-auth.js +3 -3
- package/dist/mcp-code.d.ts +7 -6
- package/dist/mcp-code.js +98 -51
- package/dist/mcp-oauth-provider.d.ts +5 -5
- package/dist/mcp-oauth-provider.js +5 -5
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +3 -3
- package/dist/mcp-panel.js +36 -99
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-setup-panel.d.ts +5 -4
- package/dist/mcp-setup-panel.js +15 -7
- package/dist/mcp-status.js +2 -1
- package/dist/mcp-trace.d.ts +1 -1
- package/dist/mcp-trace.js +1 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/npx-resolver.js +8 -4
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +41 -7
- package/dist/proxy-modes.js +275 -490
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +5 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +70 -137
- package/dist/server-manager.d.ts +2 -2
- package/dist/server-manager.js +44 -27
- package/dist/session-recovery.d.ts +7 -4
- package/dist/session-recovery.js +76 -10
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +6 -3
- package/dist/tool-result-renderer.js +11 -6
- package/dist/types.d.ts +11 -36
- package/dist/ui-resource-handler.d.ts +2 -2
- package/dist/ui-resource-handler.js +2 -2
- package/dist/ui-server.js +4 -3
- package/dist/ui-stream-types.d.ts +8 -8
- package/dist/unix-socket-transport.d.ts +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +16 -4
- package/package.json +37 -96
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/abort.ts +0 -34
- package/agent-dir.ts +0 -24
- package/app-bridge.bundle.js +0 -67
- package/banner.png +0 -0
- package/checkpoint.ts +0 -55
- package/commands.ts +0 -619
- package/config.ts +0 -1204
- package/consent-manager.ts +0 -25
- package/direct-tools.ts +0 -408
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/elicitation-handler.ts +0 -349
- package/error-signal.ts +0 -21
- package/glimpse-ui.ts +0 -80
- package/host-html-template.ts +0 -456
- package/index.ts +0 -900
- package/init.ts +0 -627
- package/legacy-migration.ts +0 -47
- package/lifecycle.ts +0 -165
- package/logger.ts +0 -127
- package/mcp-auth-flow.ts +0 -975
- package/mcp-auth.ts +0 -793
- package/mcp-callback-server.ts +0 -450
- package/mcp-code.ts +0 -386
- package/mcp-keyring-helper.cjs +0 -89
- package/mcp-oauth-provider.ts +0 -774
- package/mcp-output-guard.ts +0 -412
- package/mcp-panel.ts +0 -978
- package/mcp-script-worker.mjs +0 -128
- package/mcp-setup-panel.ts +0 -666
- package/mcp-status.ts +0 -109
- package/mcp-trace.ts +0 -295
- package/metadata-cache.ts +0 -333
- package/npx-resolver.ts +0 -488
- package/onboarding-state.ts +0 -58
- package/panel-keys.ts +0 -37
- package/prompts.ts +0 -337
- package/proxy-modes.ts +0 -1361
- package/resource-tools.ts +0 -17
- package/runtime-owner.ts +0 -123
- package/sampling-handler.ts +0 -273
- package/scripts/build.mjs +0 -131
- package/scripts/prepare.mjs +0 -102
- package/search-ranking.ts +0 -196
- package/server-manager.ts +0 -1320
- package/session-recovery.ts +0 -316
- package/state.ts +0 -64
- package/tool-approval.ts +0 -78
- package/tool-metadata.ts +0 -235
- package/tool-registrar.ts +0 -69
- package/tool-result-renderer.ts +0 -286
- package/ts-shape.ts +0 -145
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -16
- package/types.ts +0 -810
- package/ui-resource-handler.ts +0 -249
- package/ui-server.ts +0 -808
- package/ui-session.ts +0 -568
- package/ui-stream-types.ts +0 -89
- package/ui-tool-visibility.ts +0 -25
- package/unix-socket-transport.ts +0 -85
- package/utils.ts +0 -309
package/dist/mcp-output-guard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, realpath, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
export const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;
|
|
6
6
|
export const DEFAULT_MCP_OUTPUT_MAX_LINES = 2000;
|
|
7
7
|
export const DEFAULT_MCP_DETAILS_MAX_BYTES = 16 * 1024;
|
|
@@ -24,6 +24,9 @@ export function guardedMcpDetails(guarded) {
|
|
|
24
24
|
return {
|
|
25
25
|
...(guarded.mcpResult !== undefined ? { mcpResult: guarded.mcpResult } : {}),
|
|
26
26
|
...(guarded.outputGuard ? { outputGuard: guarded.outputGuard } : {}),
|
|
27
|
+
...(guarded.resultRef ? { resultRef: guarded.resultRef } : {}),
|
|
28
|
+
...(guarded.resultWriteError ? { resultWriteError: guarded.resultWriteError } : {}),
|
|
29
|
+
...(guarded.payloadFiles?.length ? { payloadFiles: guarded.payloadFiles } : {}),
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
@@ -34,18 +37,17 @@ export function guardedMcpDetails(guarded) {
|
|
|
34
37
|
export async function guardMcpOutput(content, options = {}) {
|
|
35
38
|
const maxBytes = options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES;
|
|
36
39
|
const maxLines = options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES;
|
|
37
|
-
const detailsMaxBytes = options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES;
|
|
38
40
|
const prefix = options.prefix ?? "";
|
|
39
41
|
const suffix = options.suffix ?? "";
|
|
40
42
|
const normalizedContent = withEmptyTextFallback(content.length > 0
|
|
41
43
|
? sanitizeContent(content)
|
|
42
44
|
: [{ type: "text", text: options.emptyTextFallback ?? "(empty result)" }], options.emptyTextFallback);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const retained = options.retainedMcpResult ?? (options.rawMcpResult === undefined ? {} : await retainMcpResult(options.rawMcpResult, options));
|
|
46
|
+
const reference = retained.resultRef ? formatMcpResultReference(retained.resultRef)
|
|
47
|
+
: retained.resultWriteError ? `[MCP result could not be saved: ${retained.resultWriteError}]` : undefined;
|
|
48
|
+
const withReference = (blocks) => reference ? [...blocks, { type: "text", text: reference }] : blocks;
|
|
49
|
+
if (options.enabled === false)
|
|
50
|
+
return { content: withReference(addAffixes(normalizedContent, prefix, suffix)), ...retained };
|
|
49
51
|
const imageBlocks = normalizedContent.filter((block) => block.type === "image");
|
|
50
52
|
const textOutput = normalizedContent
|
|
51
53
|
.filter((block) => block.type === "text")
|
|
@@ -53,15 +55,17 @@ export async function guardMcpOutput(content, options = {}) {
|
|
|
53
55
|
.join("\n");
|
|
54
56
|
const composedOutput = `${prefix}${textOutput}${suffix}`;
|
|
55
57
|
const stats = textStats(composedOutput);
|
|
58
|
+
const referenceSuffix = reference ? `\n${reference}` : "";
|
|
59
|
+
const totalStats = textStats(composedOutput ? composedOutput + referenceSuffix : reference ?? "");
|
|
56
60
|
let guardedContent = addAffixes(normalizedContent, prefix, suffix);
|
|
57
61
|
let outputGuard;
|
|
58
|
-
if (
|
|
62
|
+
if (totalStats.bytes > maxBytes || totalStats.lines > maxLines) {
|
|
59
63
|
const { path: fullOutputPath, error: writeError } = await saveArtifact("output", composedOutput, options.outputDirectory);
|
|
60
64
|
const notice = formatTruncationNotice(stats, fullOutputPath, writeError);
|
|
61
|
-
const previewBudget = reserveBudget(maxBytes, maxLines, notice);
|
|
65
|
+
const previewBudget = reserveBudget(maxBytes, maxLines, notice + referenceSuffix);
|
|
62
66
|
const preview = truncateHead(composedOutput, previewBudget.maxBytes, previewBudget.maxLines);
|
|
63
67
|
const finalText = `${preview.content}\n\n${notice}`;
|
|
64
|
-
const finalStats = textStats(finalText);
|
|
68
|
+
const finalStats = textStats(finalText + referenceSuffix);
|
|
65
69
|
guardedContent = [{ type: "text", text: finalText }, ...imageBlocks];
|
|
66
70
|
outputGuard = {
|
|
67
71
|
truncated: true,
|
|
@@ -74,13 +78,10 @@ export async function guardMcpOutput(content, options = {}) {
|
|
|
74
78
|
...(writeError !== undefined ? { writeError } : {}),
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
|
-
const mcpResult = options.rawMcpResult === undefined
|
|
78
|
-
? undefined
|
|
79
|
-
: await boundMcpResult(options.rawMcpResult, detailsMaxBytes, options.outputDirectory);
|
|
80
81
|
return {
|
|
81
|
-
content: guardedContent,
|
|
82
|
+
content: withReference(guardedContent),
|
|
82
83
|
...(outputGuard ? { outputGuard } : {}),
|
|
83
|
-
...
|
|
84
|
+
...retained,
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
function sanitizeContent(content) {
|
|
@@ -187,15 +188,45 @@ function formatTruncationNotice(stats, fullOutputPath, writeError) {
|
|
|
187
188
|
* detailsMaxBytes; otherwise replace it with a compact summary and spill the
|
|
188
189
|
* raw JSON to a temp file.
|
|
189
190
|
*/
|
|
190
|
-
async function
|
|
191
|
+
export async function retainMcpResult(result, options = {}, rawResult = false) {
|
|
191
192
|
const raw = safeStringify(result);
|
|
192
193
|
const rawBytes = byteLength(raw);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
const record = asRecord(result);
|
|
195
|
+
const content = Array.isArray(record?.content) ? record.content
|
|
196
|
+
: Array.isArray(record?.contents) ? record.contents.map(resource => ({ type: "resource", resource })) : [];
|
|
197
|
+
const payloadFiles = (await Promise.all(content.map(async (block, index) => {
|
|
198
|
+
const item = asRecord(block);
|
|
199
|
+
const resource = asRecord(item?.resource);
|
|
200
|
+
const data = item?.type === "audio" ? item.data : item?.type === "resource" ? resource?.blob : undefined;
|
|
201
|
+
if (typeof data !== "string")
|
|
202
|
+
return undefined;
|
|
203
|
+
const mimeType = item?.mimeType ?? resource?.mimeType;
|
|
204
|
+
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) };
|
|
205
|
+
}))).filter((file) => file !== undefined);
|
|
206
|
+
const oversized = options.enabled !== false && rawBytes > (options.detailsMaxBytes ?? DEFAULT_MCP_DETAILS_MAX_BYTES);
|
|
207
|
+
const shouldSave = oversized || !record || Object.keys(record).some(key => key !== "content" && key !== "isError")
|
|
208
|
+
|| content.some(block => {
|
|
209
|
+
const item = asRecord(block);
|
|
210
|
+
return item?.type !== "text" || Object.keys(item).some(key => key !== "type" && key !== "text");
|
|
211
|
+
});
|
|
212
|
+
if (!shouldSave)
|
|
213
|
+
return { mcpResult: result };
|
|
214
|
+
const artifact = await saveArtifact("mcp-result", raw, options.outputDirectory);
|
|
215
|
+
return {
|
|
216
|
+
mcpResult: oversized && !rawResult ? summarizeMcpResult(result, rawBytes, artifact) : result,
|
|
217
|
+
...(artifact.path ? { resultRef: artifact.path } : {}),
|
|
218
|
+
...(artifact.error ? { resultWriteError: artifact.error } : {}),
|
|
219
|
+
...(payloadFiles.length ? { payloadFiles } : {}),
|
|
220
|
+
};
|
|
196
221
|
}
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
export function formatMcpPayloadFile(file) {
|
|
223
|
+
return `[MCP ${file.kind === "audio" ? "audio" : "binary resource"}${file.uri ? ` (${file.uri})` : ""}: ${file.mimeType ?? "unknown MIME type"}; not rendered. ${file.path ? `Saved file: ${file.path}` : `Could not save: ${file.error}`}]`;
|
|
224
|
+
}
|
|
225
|
+
export function formatMcpResultReference(ref) {
|
|
226
|
+
return `[MCP result saved: ${JSON.stringify(ref)}. Inspect without repeating the call using mcp action "read-result" or tools.readResult with this ref. Optionally select a JSON Pointer with path.]`;
|
|
227
|
+
}
|
|
228
|
+
function summarizeMcpResult(result, rawBytes, artifact) {
|
|
229
|
+
const { path: fullResultPath, error: resultWriteError } = artifact;
|
|
199
230
|
const record = asRecord(result);
|
|
200
231
|
const content = Array.isArray(record?.content) ? record.content : [];
|
|
201
232
|
const summary = {
|
|
@@ -281,7 +312,7 @@ async function saveArtifact(kind, text, outputDirectory) {
|
|
|
281
312
|
if (outputDirectory !== undefined)
|
|
282
313
|
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
283
314
|
const dir = await mkdtemp(join(parent, "pi-mcp-output-"));
|
|
284
|
-
const path = join(dir, `${kind}-${randomBytes(4).toString("hex")}
|
|
315
|
+
const path = join(dir, `${kind}-${randomBytes(4).toString("hex")}.${typeof text === "string" ? "txt" : "bin"}`);
|
|
285
316
|
await writeFile(path, text, { encoding: "utf8", mode: 0o600 });
|
|
286
317
|
return { path };
|
|
287
318
|
}
|
|
@@ -289,6 +320,80 @@ async function saveArtifact(kind, text, outputDirectory) {
|
|
|
289
320
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
290
321
|
}
|
|
291
322
|
}
|
|
323
|
+
export async function readMcpResult(input, options = {}) {
|
|
324
|
+
try {
|
|
325
|
+
if (!input || typeof input.ref !== "string")
|
|
326
|
+
throw new Error("ref must be a retained MCP result path");
|
|
327
|
+
// Script-only hosts deliberately have no general filesystem capability.
|
|
328
|
+
const root = await realpath(options.outputDirectory === undefined ? tmpdir() : resolve(options.outputDirectory));
|
|
329
|
+
const ref = await realpath(resolve(input.ref));
|
|
330
|
+
if (dirname(dirname(ref)) !== root || !/^pi-mcp-output-/.test(basename(dirname(ref)))
|
|
331
|
+
|| !/^(mcp-result|output)-[a-f0-9]+\.txt$/.test(basename(ref)))
|
|
332
|
+
throw new Error("ref is not an MCP output artifact in this host's output directory");
|
|
333
|
+
let rendered = await readFile(ref, "utf8");
|
|
334
|
+
if (input.path !== undefined || input.fields !== undefined || basename(ref).startsWith("mcp-result-")) {
|
|
335
|
+
let value = JSON.parse(rendered);
|
|
336
|
+
if (input.path !== undefined && input.path !== "") {
|
|
337
|
+
if (typeof input.path !== "string" || !input.path.startsWith("/") || /~(?:[^01]|$)/.test(input.path))
|
|
338
|
+
throw new Error("path must be an RFC 6901 JSON Pointer");
|
|
339
|
+
for (const token of input.path.slice(1).split("/")) {
|
|
340
|
+
const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
341
|
+
if (value === null || typeof value !== "object" || (Array.isArray(value) && !/^(0|[1-9]\d*)$/.test(key)) || !Object.hasOwn(value, key))
|
|
342
|
+
throw new Error(`JSON Pointer does not exist: ${input.path}`);
|
|
343
|
+
value = value[key];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (input.fields !== undefined) {
|
|
347
|
+
if (!Array.isArray(input.fields) || input.fields.some(field => typeof field !== "string"))
|
|
348
|
+
throw new Error("fields must be an array of keys");
|
|
349
|
+
const select = (item) => {
|
|
350
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
351
|
+
throw new Error("fields requires an object or array of objects");
|
|
352
|
+
return Object.fromEntries(input.fields.filter(key => Object.hasOwn(item, key)).map(key => [key, item[key]]));
|
|
353
|
+
};
|
|
354
|
+
value = Array.isArray(value) ? value.map(select) : select(value);
|
|
355
|
+
}
|
|
356
|
+
rendered = JSON.stringify(value, null, 2);
|
|
357
|
+
}
|
|
358
|
+
const offset = input.offset ?? 0;
|
|
359
|
+
const limit = input.limit ?? 12_000;
|
|
360
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1)
|
|
361
|
+
throw new Error("offset must be a non-negative integer and limit a positive integer");
|
|
362
|
+
if (offset > 0 && rendered.codePointAt(offset - 1) > 0xffff)
|
|
363
|
+
throw new Error("offset must not split a Unicode surrogate pair");
|
|
364
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES;
|
|
365
|
+
const maxLines = options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES;
|
|
366
|
+
const notice = (end) => `[MCP result page: ${offset}–${end} of ${rendered.length} characters. Continue with offset: ${end}.]`;
|
|
367
|
+
const pageEnd = (byteCap, lineCap) => {
|
|
368
|
+
let end = offset;
|
|
369
|
+
let bytes = 0;
|
|
370
|
+
let lines = 1;
|
|
371
|
+
while (end < rendered.length && end - offset < limit) {
|
|
372
|
+
const width = rendered.codePointAt(end) > 0xffff ? 2 : 1;
|
|
373
|
+
const char = rendered.slice(end, end + width);
|
|
374
|
+
bytes += byteLength(char);
|
|
375
|
+
if (char === "\n")
|
|
376
|
+
lines++;
|
|
377
|
+
// Tiny caps still return one complete character plus recovery guidance.
|
|
378
|
+
if (end > offset && (bytes > byteCap || lines > lineCap || end + width - offset > limit))
|
|
379
|
+
break;
|
|
380
|
+
end += width;
|
|
381
|
+
}
|
|
382
|
+
return end;
|
|
383
|
+
};
|
|
384
|
+
let end = pageEnd(maxBytes, maxLines);
|
|
385
|
+
if (end < rendered.length) {
|
|
386
|
+
// The final offset cannot need more digits than the total length. Include the block separator.
|
|
387
|
+
end = pageEnd(maxBytes - byteLength(notice(rendered.length)) - 1, maxLines - 1);
|
|
388
|
+
}
|
|
389
|
+
const nextOffset = end < rendered.length ? end : null;
|
|
390
|
+
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 } : {}) } };
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
394
|
+
return { content: [{ type: "text", text: message }], details: { error: "result_read_failed", message } };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
292
397
|
function asRecord(value) {
|
|
293
398
|
return typeof value === "object" && value !== null ? value : undefined;
|
|
294
399
|
}
|
package/dist/mcp-panel.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { type PanelKeybindings } from "./panel-keys.ts";
|
|
2
2
|
import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type MetadataCache } from "./metadata-cache.ts";
|
|
4
4
|
declare class McpPanel {
|
|
5
|
-
private callbacks;
|
|
6
|
-
private done;
|
|
7
5
|
private noticeLines;
|
|
8
6
|
private prefix;
|
|
9
7
|
private servers;
|
|
@@ -25,6 +23,8 @@ declare class McpPanel {
|
|
|
25
23
|
private keys;
|
|
26
24
|
private static readonly MAX_VISIBLE;
|
|
27
25
|
private static readonly INACTIVITY_MS;
|
|
26
|
+
private callbacks;
|
|
27
|
+
private 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?: {
|
package/dist/mcp-panel.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { createPanelKeys } from "./panel-keys.js";
|
|
4
|
-
import { isServerDisabled, isToolAllowed } from "./types.js";
|
|
5
|
-
import { resourceNameToToolName } from "./resource-tools.js";
|
|
6
4
|
import { sanitizeTerminalText, stripOscSequences } from "./utils.js";
|
|
5
|
+
import { reconstructToolMetadata } from "./metadata-cache.js";
|
|
7
6
|
const DEFAULT_THEME = {
|
|
8
7
|
border: "2",
|
|
9
8
|
title: "2",
|
|
@@ -94,8 +93,6 @@ function estimateTokens(tool) {
|
|
|
94
93
|
return Math.ceil((tool.name.length + descLen + schemaLen) / 4) + 10;
|
|
95
94
|
}
|
|
96
95
|
class McpPanel {
|
|
97
|
-
callbacks;
|
|
98
|
-
done;
|
|
99
96
|
noticeLines;
|
|
100
97
|
prefix;
|
|
101
98
|
servers = [];
|
|
@@ -117,6 +114,8 @@ class McpPanel {
|
|
|
117
114
|
keys;
|
|
118
115
|
static MAX_VISIBLE = 12;
|
|
119
116
|
static INACTIVITY_MS = 60_000;
|
|
117
|
+
callbacks;
|
|
118
|
+
done;
|
|
120
119
|
constructor(config, cache, provenance, callbacks, tui, done, options = {}) {
|
|
121
120
|
this.callbacks = callbacks;
|
|
122
121
|
this.done = done;
|
|
@@ -138,57 +137,24 @@ class McpPanel {
|
|
|
138
137
|
else if (globalDirect) {
|
|
139
138
|
toolFilter = globalDirect;
|
|
140
139
|
}
|
|
141
|
-
const tools = [];
|
|
142
|
-
if (serverCache && !this.authOnly && !isServerDisabled(definition)) {
|
|
143
|
-
for (const tool of serverCache.tools ?? []) {
|
|
144
|
-
if (!isToolAllowed(tool.name, serverName, this.prefix, definition.includeTools, definition.excludeTools)) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(tool.name));
|
|
148
|
-
tools.push({
|
|
149
|
-
name: tool.name,
|
|
150
|
-
description: tool.description ?? "",
|
|
151
|
-
isDirect,
|
|
152
|
-
wasDirect: isDirect,
|
|
153
|
-
estimatedTokens: estimateTokens(tool),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if (definition.exposeResources !== false) {
|
|
157
|
-
for (const resource of serverCache.resources ?? []) {
|
|
158
|
-
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
159
|
-
if (!isToolAllowed(baseName, serverName, this.prefix, definition.includeTools, definition.excludeTools)) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const isDirect = toolFilter === true || (Array.isArray(toolFilter) && toolFilter.includes(baseName));
|
|
163
|
-
const ct = {
|
|
164
|
-
name: baseName,
|
|
165
|
-
...(resource.description !== undefined ? { description: resource.description } : {}),
|
|
166
|
-
};
|
|
167
|
-
tools.push({
|
|
168
|
-
name: baseName,
|
|
169
|
-
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
170
|
-
isDirect,
|
|
171
|
-
wasDirect: isDirect,
|
|
172
|
-
estimatedTokens: estimateTokens(ct),
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
140
|
const status = callbacks.getConnectionStatus(serverName);
|
|
178
141
|
const failureMessage = callbacks.getFailureMessage?.(serverName) ?? null;
|
|
179
|
-
|
|
142
|
+
const server = {
|
|
180
143
|
name: serverName,
|
|
181
144
|
expanded: false,
|
|
182
145
|
source: prov?.kind ?? "user",
|
|
183
146
|
...(prov?.importKind !== undefined ? { importKind: prov.importKind } : {}),
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
147
|
+
definition,
|
|
148
|
+
toolFilter,
|
|
149
|
+
resourceCount: 0,
|
|
187
150
|
connectionStatus: status,
|
|
188
151
|
failureMessage,
|
|
189
|
-
tools,
|
|
152
|
+
tools: [],
|
|
190
153
|
hasCachedData: !!serverCache,
|
|
191
|
-
}
|
|
154
|
+
};
|
|
155
|
+
this.servers.push(server);
|
|
156
|
+
if (serverCache && !this.authOnly)
|
|
157
|
+
this.rebuildServerTools(server, serverCache);
|
|
192
158
|
}
|
|
193
159
|
this.rebuildVisibleItems();
|
|
194
160
|
this.resetInactivityTimeout();
|
|
@@ -503,7 +469,6 @@ class McpPanel {
|
|
|
503
469
|
if (entry) {
|
|
504
470
|
this.rebuildServerTools(server, entry);
|
|
505
471
|
}
|
|
506
|
-
server.hasCachedData = true;
|
|
507
472
|
}
|
|
508
473
|
if (options.afterAuth) {
|
|
509
474
|
this.authNotice = connected && server.connectionStatus === "connected"
|
|
@@ -578,46 +543,22 @@ class McpPanel {
|
|
|
578
543
|
this.cursorIndex = Math.max(0, Math.min(this.visibleItems.length - 1, this.cursorIndex + delta));
|
|
579
544
|
}
|
|
580
545
|
rebuildServerTools(server, entry) {
|
|
581
|
-
const existingState = new Map();
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
description: tool.description ?? "",
|
|
594
|
-
isDirect,
|
|
595
|
-
wasDirect: prev !== undefined ? server.tools.find((t) => t.name === tool.name)?.wasDirect ?? false : false,
|
|
546
|
+
const existingState = new Map(server.tools.map(tool => [tool.name, tool]));
|
|
547
|
+
const metadata = reconstructToolMetadata(server.name, entry, this.prefix, server.definition);
|
|
548
|
+
server.resourceCount = metadata.filter(tool => tool.resourceUri !== undefined).length;
|
|
549
|
+
server.tools = metadata.filter(tool => tool.resourceUri === undefined).map(tool => {
|
|
550
|
+
const previous = existingState.get(tool.originalName);
|
|
551
|
+
const pinned = server.toolFilter === true
|
|
552
|
+
|| (Array.isArray(server.toolFilter) && server.toolFilter.includes(tool.originalName));
|
|
553
|
+
return {
|
|
554
|
+
name: tool.originalName,
|
|
555
|
+
description: tool.description,
|
|
556
|
+
isDirect: previous?.isDirect ?? pinned,
|
|
557
|
+
wasDirect: previous?.wasDirect ?? pinned,
|
|
596
558
|
estimatedTokens: estimateTokens(tool),
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
for (const resource of entry.resources ?? []) {
|
|
601
|
-
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
602
|
-
if (!isToolAllowed(baseName, server.name, this.prefix, server.includeTools, server.excludeTools)) {
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
const prev = existingState.get(baseName);
|
|
606
|
-
const isDirect = prev !== undefined ? prev : false;
|
|
607
|
-
const ct = {
|
|
608
|
-
name: baseName,
|
|
609
|
-
...(resource.description !== undefined ? { description: resource.description } : {}),
|
|
610
|
-
};
|
|
611
|
-
newTools.push({
|
|
612
|
-
name: baseName,
|
|
613
|
-
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
614
|
-
isDirect,
|
|
615
|
-
wasDirect: prev !== undefined ? server.tools.find((t) => t.name === baseName)?.wasDirect ?? false : false,
|
|
616
|
-
estimatedTokens: estimateTokens(ct),
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
server.tools = newTools;
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
server.hasCachedData = true;
|
|
621
562
|
this.rebuildVisibleItems();
|
|
622
563
|
this.updateDirty();
|
|
623
564
|
}
|
|
@@ -677,6 +618,9 @@ class McpPanel {
|
|
|
677
618
|
continue;
|
|
678
619
|
if (item.type === "server") {
|
|
679
620
|
lines.push(row(this.renderServerRow(server, isCursor)));
|
|
621
|
+
if (isCursor && !this.authOnly && server.resourceCount > 0) {
|
|
622
|
+
lines.push(row(fg(t.hint, " Resources: mcp actions resources / read-resource")));
|
|
623
|
+
}
|
|
680
624
|
if (isCursor && server.connectionStatus === "failed" && server.failureMessage) {
|
|
681
625
|
for (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerW - 6)) {
|
|
682
626
|
lines.push(row(` ${fg(t.cancel, line)}`));
|
|
@@ -722,8 +666,9 @@ class McpPanel {
|
|
|
722
666
|
else {
|
|
723
667
|
const directCount = this.servers.reduce((sum, s) => sum + s.tools.filter((t) => t.isDirect).length, 0);
|
|
724
668
|
const totalTokens = this.servers.reduce((sum, s) => sum + s.tools.filter((t) => t.isDirect).reduce((ts, t) => ts + t.estimatedTokens, 0), 0);
|
|
725
|
-
const stats = directCount > 0 ? `${directCount}
|
|
669
|
+
const stats = directCount > 0 ? `${directCount} pinned ~${totalTokens.toLocaleString()} tokens` : "no pinned tools";
|
|
726
670
|
lines.push(row(fg(t.description, stats + (this.dirty ? fg(t.needsAuth, " (unsaved)") : ""))));
|
|
671
|
+
lines.push(row(fg(t.hint, "Pins load at startup; discover more with mcp_search.")));
|
|
727
672
|
}
|
|
728
673
|
}
|
|
729
674
|
lines.push(emptyRow());
|
|
@@ -737,7 +682,7 @@ class McpPanel {
|
|
|
737
682
|
]
|
|
738
683
|
: [
|
|
739
684
|
italic("↑↓") + " navigate",
|
|
740
|
-
italic("space") + "
|
|
685
|
+
italic("space") + " pin/unpin",
|
|
741
686
|
italic("⏎") + " expand/auth",
|
|
742
687
|
italic("ctrl+a") + " auth",
|
|
743
688
|
italic("ctrl+r") + " reconnect",
|
|
@@ -781,7 +726,7 @@ class McpPanel {
|
|
|
781
726
|
const importLabel = server.source === "import" ? fg(t.description, ` (${importKind})`) : "";
|
|
782
727
|
const statusLabel = this.renderConnectionStatus(server);
|
|
783
728
|
if (!server.hasCachedData && !this.authOnly) {
|
|
784
|
-
return `${prefix} ${nameStr}${importLabel} ${fg(t.description, "(
|
|
729
|
+
return `${prefix} ${nameStr}${importLabel}${statusLabel} ${fg(t.description, "(undiscovered)")}`;
|
|
785
730
|
}
|
|
786
731
|
const directCount = server.tools.filter((t) => t.isDirect).length;
|
|
787
732
|
const totalCount = server.tools.length;
|
|
@@ -792,16 +737,8 @@ class McpPanel {
|
|
|
792
737
|
else if (directCount > 0) {
|
|
793
738
|
toggleIcon = fg(t.needsAuth, "◐");
|
|
794
739
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
toolInfo = `${directCount}/${totalCount}`;
|
|
798
|
-
if (directCount > 0) {
|
|
799
|
-
const tokens = server.tools.filter((t) => t.isDirect).reduce((s, t) => s + t.estimatedTokens, 0);
|
|
800
|
-
toolInfo += ` ~${tokens.toLocaleString()}`;
|
|
801
|
-
}
|
|
802
|
-
toolInfo = fg(t.description, toolInfo);
|
|
803
|
-
}
|
|
804
|
-
return `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}${statusLabel}`;
|
|
740
|
+
const toolInfo = this.authOnly ? "" : fg(t.description, `${totalCount} tool${totalCount === 1 ? "" : "s"}, ${directCount} pinned${server.resourceCount > 0 ? `, ${server.resourceCount} resource${server.resourceCount === 1 ? "" : "s"}` : ""}`);
|
|
741
|
+
return `${prefix} ${toggleIcon} ${nameStr}${importLabel}${statusLabel} ${toolInfo}`;
|
|
805
742
|
}
|
|
806
743
|
selectedServerHasFailureMessage() {
|
|
807
744
|
const item = this.visibleItems[this.cursorIndex];
|
|
@@ -69,7 +69,7 @@ const tools = new Proxy(Object.create(null), {
|
|
|
69
69
|
return async (input) => request("search", { input });
|
|
70
70
|
}
|
|
71
71
|
if (property === "call") {
|
|
72
|
-
return async (path, args) => {
|
|
72
|
+
return async (path, args, server) => {
|
|
73
73
|
// Invalid paths never reach dispatch and therefore never appear in the call trace.
|
|
74
74
|
if (typeof path !== "string" || path.trim() === "") {
|
|
75
75
|
return {
|
|
@@ -80,9 +80,12 @@ const tools = new Proxy(Object.create(null), {
|
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
if (server !== undefined && (typeof server !== "string" || !server)) return { ok: false, error: { code: "invalid_arguments", message: "server must be a non-empty string" } };
|
|
84
|
+
return request("call", { path, args, server });
|
|
84
85
|
};
|
|
85
86
|
}
|
|
87
|
+
if (property === "resources" || property === "listResources") return async (input) => request("resources", { input });
|
|
88
|
+
if (property === "readResource" || property === "readResult") return async (input) => request(property, { input });
|
|
86
89
|
if (property === "describe") {
|
|
87
90
|
return async (input) => request("describe", { input });
|
|
88
91
|
}
|
|
@@ -31,10 +31,6 @@ export interface SetupPanelOptions {
|
|
|
31
31
|
keybindings?: PanelKeybindings;
|
|
32
32
|
}
|
|
33
33
|
export declare class McpSetupPanel {
|
|
34
|
-
private discovery;
|
|
35
|
-
private callbacks;
|
|
36
|
-
private options;
|
|
37
|
-
private done;
|
|
38
34
|
private screen;
|
|
39
35
|
private actionCursor;
|
|
40
36
|
private importCursor;
|
|
@@ -47,6 +43,10 @@ export declare class McpSetupPanel {
|
|
|
47
43
|
private keys;
|
|
48
44
|
private inactivityTimeout;
|
|
49
45
|
private static readonly INACTIVITY_MS;
|
|
46
|
+
private discovery;
|
|
47
|
+
private callbacks;
|
|
48
|
+
private options;
|
|
49
|
+
private done;
|
|
50
50
|
constructor(discovery: McpDiscoverySummary, callbacks: SetupPanelCallbacks, options: SetupPanelOptions, tui: {
|
|
51
51
|
requestRender(): void;
|
|
52
52
|
}, done: () => void);
|
|
@@ -72,6 +72,7 @@ export declare class McpSetupPanel {
|
|
|
72
72
|
private previewWidth;
|
|
73
73
|
private getActionPreview;
|
|
74
74
|
private formatPreview;
|
|
75
|
+
private previewOrError;
|
|
75
76
|
private formatWritePreview;
|
|
76
77
|
private padLine;
|
|
77
78
|
invalidate(): void;
|
package/dist/mcp-setup-panel.js
CHANGED
|
@@ -38,10 +38,6 @@ function wrapText(text, width) {
|
|
|
38
38
|
return lines.length > 0 ? lines : [""];
|
|
39
39
|
}
|
|
40
40
|
export class McpSetupPanel {
|
|
41
|
-
discovery;
|
|
42
|
-
callbacks;
|
|
43
|
-
options;
|
|
44
|
-
done;
|
|
45
41
|
screen;
|
|
46
42
|
actionCursor = 0;
|
|
47
43
|
importCursor = 0;
|
|
@@ -54,6 +50,10 @@ export class McpSetupPanel {
|
|
|
54
50
|
keys;
|
|
55
51
|
inactivityTimeout = null;
|
|
56
52
|
static INACTIVITY_MS = 60_000;
|
|
53
|
+
discovery;
|
|
54
|
+
callbacks;
|
|
55
|
+
options;
|
|
56
|
+
done;
|
|
57
57
|
constructor(discovery, callbacks, options, tui, done) {
|
|
58
58
|
this.discovery = discovery;
|
|
59
59
|
this.callbacks = callbacks;
|
|
@@ -361,7 +361,7 @@ export class McpSetupPanel {
|
|
|
361
361
|
lines.push(this.padLine(fg(this.t.muted, `… ${actions.length - end} more below`), innerW));
|
|
362
362
|
}
|
|
363
363
|
lines.push(this.padLine("", innerW));
|
|
364
|
-
const preview = this.getActionPreview(this.getSelectedAction(),
|
|
364
|
+
const preview = this.previewOrError(width => this.getActionPreview(this.getSelectedAction(), width), innerW);
|
|
365
365
|
for (const line of preview) {
|
|
366
366
|
lines.push(this.padLine(line, innerW));
|
|
367
367
|
}
|
|
@@ -384,8 +384,7 @@ export class McpSetupPanel {
|
|
|
384
384
|
}
|
|
385
385
|
lines.push(this.padLine("", innerW));
|
|
386
386
|
const selected = this.discovery.imports.filter((entry) => this.selectedImports.has(entry.kind)).map((entry) => entry.kind);
|
|
387
|
-
const
|
|
388
|
-
for (const line of this.formatWritePreview("Compatibility import write preview", preview, [], this.previewWidth(innerW))) {
|
|
387
|
+
for (const line of this.previewOrError(width => this.formatWritePreview("Compatibility import write preview", this.callbacks.previewImports(selected), [], width), innerW)) {
|
|
389
388
|
lines.push(this.padLine(line, innerW));
|
|
390
389
|
}
|
|
391
390
|
return lines;
|
|
@@ -522,6 +521,15 @@ export class McpSetupPanel {
|
|
|
522
521
|
}
|
|
523
522
|
return preview;
|
|
524
523
|
}
|
|
524
|
+
previewOrError(preview, innerW) {
|
|
525
|
+
const width = this.previewWidth(innerW);
|
|
526
|
+
try {
|
|
527
|
+
return preview(width);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
return this.formatPreview([`Cannot preview write: ${error instanceof Error ? error.message : String(error)}`], width);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
525
533
|
formatWritePreview(title, preview, intro = [], width = DESKTOP_PREVIEW_WIDTH) {
|
|
526
534
|
const lines = [];
|
|
527
535
|
for (const line of intro) {
|
package/dist/mcp-status.js
CHANGED
|
@@ -21,7 +21,7 @@ export function createMcpStatusSnapshot(state) {
|
|
|
21
21
|
const disabled = definition?.disabled === true;
|
|
22
22
|
const connection = disabled ? undefined : state.manager.getConnection(name);
|
|
23
23
|
const metadata = disabled ? undefined : state.toolMetadata.get(name);
|
|
24
|
-
const toolCount = metadata?.length ?? (connection?.status === "connected" ? connection.tools.length : 0);
|
|
24
|
+
const toolCount = metadata?.filter(tool => !tool.resourceUri).length ?? (connection?.status === "connected" ? connection.tools.length : 0);
|
|
25
25
|
const resourceCount = disabled
|
|
26
26
|
? undefined
|
|
27
27
|
: state.resourceCounts?.get(name) ?? (connection?.status === "connected" ? connection.resources.length : undefined);
|
|
@@ -51,6 +51,7 @@ export function createMcpStatusSnapshot(state) {
|
|
|
51
51
|
name,
|
|
52
52
|
status,
|
|
53
53
|
toolCount,
|
|
54
|
+
catalogKnown: metadata !== undefined || connection?.status === "connected",
|
|
54
55
|
...(resourceCount !== undefined ? { resourceCount } : {}),
|
|
55
56
|
...(status === "failed" && failedAgoSeconds !== undefined ? { failedAgoSeconds } : {}),
|
|
56
57
|
disabled,
|
package/dist/mcp-trace.d.ts
CHANGED
|
@@ -52,7 +52,6 @@ export declare function createMcpTraceEvent(direction: McpTraceDirection, server
|
|
|
52
52
|
durationMs?: number;
|
|
53
53
|
}): McpTraceEvent;
|
|
54
54
|
export declare class McpTraceWriter {
|
|
55
|
-
private readonly options;
|
|
56
55
|
private readonly maxBytes;
|
|
57
56
|
private readonly maxEvents;
|
|
58
57
|
private readonly append;
|
|
@@ -65,6 +64,7 @@ export declare class McpTraceWriter {
|
|
|
65
64
|
private initializationFailed;
|
|
66
65
|
private persistenceError;
|
|
67
66
|
private readonly fileReady;
|
|
67
|
+
private readonly options;
|
|
68
68
|
constructor(options: McpTraceWriterOptions);
|
|
69
69
|
get filePath(): string;
|
|
70
70
|
get isDisabled(): boolean;
|
package/dist/mcp-trace.js
CHANGED
|
@@ -73,7 +73,6 @@ export function createMcpTraceEvent(direction, server, transport, message, statu
|
|
|
73
73
|
return event;
|
|
74
74
|
}
|
|
75
75
|
export class McpTraceWriter {
|
|
76
|
-
options;
|
|
77
76
|
maxBytes;
|
|
78
77
|
maxEvents;
|
|
79
78
|
append;
|
|
@@ -86,6 +85,7 @@ export class McpTraceWriter {
|
|
|
86
85
|
initializationFailed = false;
|
|
87
86
|
persistenceError;
|
|
88
87
|
fileReady;
|
|
88
|
+
options;
|
|
89
89
|
constructor(options) {
|
|
90
90
|
this.options = options;
|
|
91
91
|
this.maxBytes = boundedPositiveInteger(options.maxBytes, DEFAULT_MCP_TRACE_MAX_BYTES);
|
package/dist/metadata-cache.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare function parseDirectToolSelectors(selectors: string[]): {
|
|
|
11
11
|
tools: Map<string, Set<string>>;
|
|
12
12
|
};
|
|
13
13
|
export declare function getMissingConfiguredDirectToolServers(config: McpConfig, cache: MetadataCache | null, envOverride?: string[]): string[];
|
|
14
|
-
export declare function reconstructToolMetadata(serverName: string, entry: ServerCacheEntry, prefix: ToolPrefix, definition: Pick<ServerEntry, "exposeResources" | "includeTools" | "excludeTools" | "toolPrefix">): ToolMetadata[];
|
|
14
|
+
export declare function reconstructToolMetadata(serverName: string, entry: ServerCacheEntry, prefix: ToolPrefix, definition: Pick<ServerEntry, "exposeResources" | "includeTools" | "excludeTools" | "toolPrefix" | "disabled">): ToolMetadata[];
|
|
15
15
|
export declare function serializeTools(tools: McpTool[]): CachedTool[];
|
|
16
16
|
export declare function serializeResources(resources: McpResource[]): CachedResource[];
|
|
17
17
|
export declare function serializePrompts(prompts: McpPrompt[]): CachedPrompt[];
|