@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/OAUTH.md +3 -3
  3. package/README.md +160 -88
  4. package/dist/app-bridge.bundle.js +75 -33
  5. package/dist/commands.js +9 -6
  6. package/dist/config.js +41 -16
  7. package/dist/direct-tools.d.ts +2 -4
  8. package/dist/direct-tools.js +21 -319
  9. package/dist/gateway-arguments.d.ts +33 -0
  10. package/dist/gateway-arguments.js +63 -0
  11. package/dist/index.js +181 -276
  12. package/dist/init.js +6 -20
  13. package/dist/lifecycle.d.ts +2 -2
  14. package/dist/lifecycle.js +2 -2
  15. package/dist/mcp-auth-flow.js +2 -2
  16. package/dist/mcp-auth.d.ts +1 -1
  17. package/dist/mcp-auth.js +3 -3
  18. package/dist/mcp-code.d.ts +7 -6
  19. package/dist/mcp-code.js +98 -51
  20. package/dist/mcp-oauth-provider.d.ts +5 -5
  21. package/dist/mcp-oauth-provider.js +5 -5
  22. package/dist/mcp-output-guard.d.ts +57 -2
  23. package/dist/mcp-output-guard.js +129 -24
  24. package/dist/mcp-panel.d.ts +3 -3
  25. package/dist/mcp-panel.js +36 -99
  26. package/dist/mcp-script-worker.mjs +5 -2
  27. package/dist/mcp-setup-panel.d.ts +5 -4
  28. package/dist/mcp-setup-panel.js +15 -7
  29. package/dist/mcp-status.js +2 -1
  30. package/dist/mcp-trace.d.ts +1 -1
  31. package/dist/mcp-trace.js +1 -1
  32. package/dist/metadata-cache.d.ts +1 -1
  33. package/dist/metadata-cache.js +25 -88
  34. package/dist/npx-resolver.js +8 -4
  35. package/dist/prompts.d.ts +1 -1
  36. package/dist/prompts.js +1 -1
  37. package/dist/proxy-modes.d.ts +41 -7
  38. package/dist/proxy-modes.js +275 -490
  39. package/dist/resource-tools.d.ts +15 -0
  40. package/dist/resource-tools.js +26 -1
  41. package/dist/sampling-handler.js +5 -1
  42. package/dist/search-ranking.d.ts +0 -2
  43. package/dist/search-ranking.js +70 -137
  44. package/dist/server-manager.d.ts +2 -2
  45. package/dist/server-manager.js +44 -27
  46. package/dist/session-recovery.d.ts +7 -4
  47. package/dist/session-recovery.js +76 -10
  48. package/dist/state.d.ts +1 -1
  49. package/dist/tool-loader.d.ts +32 -0
  50. package/dist/tool-loader.js +152 -0
  51. package/dist/tool-metadata.d.ts +67 -0
  52. package/dist/tool-metadata.js +23 -17
  53. package/dist/tool-registrar.d.ts +4 -4
  54. package/dist/tool-registrar.js +18 -13
  55. package/dist/tool-result-renderer.d.ts +6 -3
  56. package/dist/tool-result-renderer.js +11 -6
  57. package/dist/types.d.ts +11 -36
  58. package/dist/ui-resource-handler.d.ts +2 -2
  59. package/dist/ui-resource-handler.js +2 -2
  60. package/dist/ui-server.js +4 -3
  61. package/dist/ui-stream-types.d.ts +8 -8
  62. package/dist/unix-socket-transport.d.ts +1 -1
  63. package/dist/unix-socket-transport.js +1 -1
  64. package/dist/utils.d.ts +1 -0
  65. package/dist/utils.js +16 -4
  66. package/package.json +37 -96
  67. package/skills/mcp-scripting/SKILL.md +67 -20
  68. package/abort.ts +0 -34
  69. package/agent-dir.ts +0 -24
  70. package/app-bridge.bundle.js +0 -67
  71. package/banner.png +0 -0
  72. package/checkpoint.ts +0 -55
  73. package/commands.ts +0 -619
  74. package/config.ts +0 -1204
  75. package/consent-manager.ts +0 -25
  76. package/direct-tools.ts +0 -408
  77. package/dist/ts-shape.d.ts +0 -2
  78. package/dist/ts-shape.js +0 -147
  79. package/elicitation-handler.ts +0 -349
  80. package/error-signal.ts +0 -21
  81. package/glimpse-ui.ts +0 -80
  82. package/host-html-template.ts +0 -456
  83. package/index.ts +0 -900
  84. package/init.ts +0 -627
  85. package/legacy-migration.ts +0 -47
  86. package/lifecycle.ts +0 -165
  87. package/logger.ts +0 -127
  88. package/mcp-auth-flow.ts +0 -975
  89. package/mcp-auth.ts +0 -793
  90. package/mcp-callback-server.ts +0 -450
  91. package/mcp-code.ts +0 -386
  92. package/mcp-keyring-helper.cjs +0 -89
  93. package/mcp-oauth-provider.ts +0 -774
  94. package/mcp-output-guard.ts +0 -412
  95. package/mcp-panel.ts +0 -978
  96. package/mcp-script-worker.mjs +0 -128
  97. package/mcp-setup-panel.ts +0 -666
  98. package/mcp-status.ts +0 -109
  99. package/mcp-trace.ts +0 -295
  100. package/metadata-cache.ts +0 -333
  101. package/npx-resolver.ts +0 -488
  102. package/onboarding-state.ts +0 -58
  103. package/panel-keys.ts +0 -37
  104. package/prompts.ts +0 -337
  105. package/proxy-modes.ts +0 -1361
  106. package/resource-tools.ts +0 -17
  107. package/runtime-owner.ts +0 -123
  108. package/sampling-handler.ts +0 -273
  109. package/scripts/build.mjs +0 -131
  110. package/scripts/prepare.mjs +0 -102
  111. package/search-ranking.ts +0 -196
  112. package/server-manager.ts +0 -1320
  113. package/session-recovery.ts +0 -316
  114. package/state.ts +0 -64
  115. package/tool-approval.ts +0 -78
  116. package/tool-metadata.ts +0 -235
  117. package/tool-registrar.ts +0 -69
  118. package/tool-result-renderer.ts +0 -286
  119. package/ts-shape.ts +0 -145
  120. package/tsconfig.build.json +0 -13
  121. package/tsconfig.json +0 -16
  122. package/types.ts +0 -810
  123. package/ui-resource-handler.ts +0 -249
  124. package/ui-server.ts +0 -808
  125. package/ui-session.ts +0 -568
  126. package/ui-stream-types.ts +0 -89
  127. package/ui-tool-visibility.ts +0 -25
  128. package/unix-socket-transport.ts +0 -85
  129. package/utils.ts +0 -309
@@ -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
- if (options.enabled === false) {
44
- return {
45
- content: addAffixes(normalizedContent, prefix, suffix),
46
- ...(options.rawMcpResult !== undefined ? { mcpResult: options.rawMcpResult } : {}),
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 (stats.bytes > maxBytes || stats.lines > maxLines) {
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
- ...(mcpResult !== undefined ? { mcpResult } : {}),
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 boundMcpResult(result, detailsMaxBytes, outputDirectory) {
191
+ export async function retainMcpResult(result, options = {}, rawResult = false) {
191
192
  const raw = safeStringify(result);
192
193
  const rawBytes = byteLength(raw);
193
- if (rawBytes <= detailsMaxBytes)
194
- return result;
195
- return summarizeMcpResult(result, raw, rawBytes, outputDirectory);
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
- async function summarizeMcpResult(result, raw, rawBytes, outputDirectory) {
198
- const { path: fullResultPath, error: resultWriteError } = await saveArtifact("mcp-result", raw, outputDirectory);
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")}.txt`);
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
  }
@@ -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 { MetadataCache } from "./metadata-cache.ts";
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
- this.servers.push({
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
- ...(definition.includeTools !== undefined ? { includeTools: definition.includeTools } : {}),
185
- ...(definition.excludeTools !== undefined ? { excludeTools: definition.excludeTools } : {}),
186
- exposeResources: definition.exposeResources !== false,
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
- for (const t of server.tools)
583
- existingState.set(t.name, t.isDirect);
584
- const newTools = [];
585
- for (const tool of entry.tools ?? []) {
586
- if (!isToolAllowed(tool.name, server.name, this.prefix, server.includeTools, server.excludeTools)) {
587
- continue;
588
- }
589
- const prev = existingState.get(tool.name);
590
- const isDirect = prev !== undefined ? prev : false;
591
- newTools.push({
592
- name: tool.name,
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
- if (server.exposeResources) {
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} direct ~${totalTokens.toLocaleString()} tokens` : "no direct tools";
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") + " toggle",
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, "(not cached)")}${statusLabel}`;
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
- let toolInfo = "";
796
- if (totalCount > 0) {
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
- return request("call", { path, args });
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;
@@ -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(), this.previewWidth(innerW));
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 preview = this.callbacks.previewImports(selected);
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) {
@@ -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,
@@ -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);
@@ -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[];