@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
@@ -4,8 +4,6 @@ import { hasPendingAuth } from "./mcp-auth-flow.ts";
4
4
  export type ReconnectCallback = (serverName: string) => void;
5
5
  export type ReconnectFailureCallback = (serverName: string, error: unknown) => void;
6
6
  export declare class McpLifecycleManager {
7
- private readonly manager;
8
- private readonly hasPendingAuthForServer;
9
7
  private keepAliveServers;
10
8
  private allServers;
11
9
  private serverSettings;
@@ -23,6 +21,8 @@ export declare class McpLifecycleManager {
23
21
  pauseForCheckpoint(): () => void;
24
22
  private healthSignal;
25
23
  private removeHealthAbortListener;
24
+ private readonly manager;
25
+ private readonly hasPendingAuthForServer;
26
26
  constructor(manager: McpServerManager, hasPendingAuthForServer?: typeof hasPendingAuth);
27
27
  setReconnectCallback(callback: ReconnectCallback): void;
28
28
  setReconnectFailureCallback(callback: ReconnectFailureCallback): void;
package/dist/lifecycle.js CHANGED
@@ -3,8 +3,6 @@ import { hasPendingAuth } from "./mcp-auth-flow.js";
3
3
  import { logger } from "./logger.js";
4
4
  import { formatTerminalError, sanitizeTerminalText } from "./utils.js";
5
5
  export class McpLifecycleManager {
6
- manager;
7
- hasPendingAuthForServer;
8
6
  keepAliveServers = new Map();
9
7
  allServers = new Map();
10
8
  serverSettings = new Map();
@@ -31,6 +29,8 @@ export class McpLifecycleManager {
31
29
  }
32
30
  healthSignal;
33
31
  removeHealthAbortListener;
32
+ manager;
33
+ hasPendingAuthForServer;
34
34
  constructor(manager, hasPendingAuthForServer = hasPendingAuth) {
35
35
  this.manager = manager;
36
36
  this.hasPendingAuthForServer = hasPendingAuthForServer;
@@ -9,7 +9,7 @@ import { McpOAuthProvider, issuersMatch, loopbackRedirectsMatch, validateOAuthCl
9
9
  import { ensureCallbackServer, waitForCallback, cancelPendingCallback, stopCallbackServer, releaseCallbackServer, takeCallbackResult, } from "./mcp-callback-server.js";
10
10
  import { getAuthForUrl, clearAllCredentials, clearClientInfo, clearTokens, clearCodeVerifier, getOAuthState, clearOAuthState, getAuthBaseDir, } from "./mcp-auth.js";
11
11
  import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig } from "./types.js";
12
- import { formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.js";
12
+ import { ADAPTER_VERSION, formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.js";
13
13
  import { abortable, throwIfAborted } from "./abort.js";
14
14
  import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
15
15
  function applyConfiguredScope(discovery, config) {
@@ -255,7 +255,7 @@ async function probeAuthDiscovery(serverUrl, definition, signal) {
255
255
  const timeout = normalizeRequestTimeoutMs(definition?.requestTimeoutMs) ?? 5000;
256
256
  const discoverySignal = combineAbortSignals(signal, AbortSignal.timeout(Math.ceil(timeout)));
257
257
  let discovery = {};
258
- const client = new Client({ name: "pi-mcp-auth-discovery", version: "4.3.1" }, {
258
+ const client = new Client({ name: "pi-mcp-auth-discovery", version: ADAPTER_VERSION }, {
259
259
  versionNegotiation: { mode: definition?.protocolVersion ?? "auto" },
260
260
  ...(isNonInteractiveOAuth(definition?.oauth)
261
261
  ? { capabilities: { extensions: { [definition?.oauth && definition.oauth.crossAppAccess
@@ -51,8 +51,8 @@ export interface AuthStorageOptions {
51
51
  baseDir?: string;
52
52
  }
53
53
  export declare class OAuthCredentialStoreError extends Error {
54
- readonly operation: 'read' | 'write' | 'remove';
55
54
  readonly code = "OAUTH_CREDENTIAL_STORE_UNAVAILABLE";
55
+ readonly operation: 'read' | 'write' | 'remove';
56
56
  constructor(message: string, operation: 'read' | 'write' | 'remove', cause: unknown);
57
57
  }
58
58
  export type OAuthCredentialStatus = {
package/dist/mcp-auth.js CHANGED
@@ -15,8 +15,8 @@ import { createRequire } from 'module';
15
15
  import { readFileSync, existsSync, rmSync } from 'fs';
16
16
  import { dirname, join } from 'path';
17
17
  import { fileURLToPath } from 'url';
18
- import { getAdapterPath, getAgentPath } from "./agent-dir.js";
19
- import { resolveConfiguredOAuthDir } from "./config.js";
18
+ import { getAdapterPath, getAgentPath } from './agent-dir.js';
19
+ import { resolveConfiguredOAuthDir } from './config.js';
20
20
  const require = createRequire(import.meta.url);
21
21
  const AUTH_SECRET_SERVICE = 'fitch-mcp-adapter.oauth';
22
22
  const LEGACY_AUTH_SECRET_SERVICE = 'pi-mcp-adapter.oauth';
@@ -30,8 +30,8 @@ const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOV
30
30
  const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
31
31
  const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
32
32
  export class OAuthCredentialStoreError extends Error {
33
- operation;
34
33
  code = 'OAUTH_CREDENTIAL_STORE_UNAVAILABLE';
34
+ operation;
35
35
  constructor(message, operation, cause) {
36
36
  super(isAndroidKeyringLoadFailure(cause)
37
37
  ? `${message}. Android/Termux: @napi-rs/keyring could not load a native binding. Persistent OAuth requires a supported platform with an OS credential store; no plaintext fallback is used.`
@@ -1,11 +1,17 @@
1
1
  import type { ToolInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { McpExtensionState } from "./state.ts";
3
- import type { ContentBlock, McpOperationContext } from "./types.ts";
3
+ import { type ContentBlock, type McpOperationContext } from "./types.ts";
4
4
  export declare const DEFAULT_MCP_SCRIPT_TIMEOUT_MS = 30000;
5
5
  export declare const runMcpScript: typeof runMcpScriptOperation;
6
6
  declare function runMcpScriptOperation(state: McpExtensionState, code: string, timeoutMs?: number | null, getPiTools?: () => ToolInfo[], signal?: AbortSignal, toolCallId?: string, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>): Promise<{
7
7
  content: ContentBlock[];
8
8
  details: {
9
+ mode: string;
10
+ resultRefs?: string[];
11
+ error?: "aborted" | "call_capture_failed" | "script_error" | "timeout";
12
+ message?: string | undefined;
13
+ timeoutMs: number | null;
14
+ recovery?: {} | null;
9
15
  calls?: ({
10
16
  operation: "call";
11
17
  path: string;
@@ -41,11 +47,6 @@ declare function runMcpScriptOperation(state: McpExtensionState, code: string, t
41
47
  error: string;
42
48
  durationMs: number;
43
49
  })[];
44
- recovery?: {} | null;
45
- timeoutMs: number | null;
46
- error?: "aborted" | "timeout" | "call_capture_failed" | "script_error";
47
- message?: string | undefined;
48
- mode: string;
49
50
  };
50
51
  }>;
51
52
  export {};
package/dist/mcp-code.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { formatWithOptions } from "node:util";
2
2
  import { Worker } from "node:worker_threads";
3
- import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
4
- import { executeCall } from "./proxy-modes.js";
3
+ import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions, readMcpResult, formatMcpResultReference, formatMcpPayloadFile } from "./mcp-output-guard.js";
4
+ import { executeCall, executeConnect } from "./proxy-modes.js";
5
+ import { executeResourceList, executeResourceRead } from "./resource-tools.js";
5
6
  import { combineAbortSignals } from "./runtime-owner.js";
6
7
  import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
7
- import { findToolByName } from "./tool-metadata.js";
8
- import { renderTsShape } from "./ts-shape.js";
8
+ import { catalogCoverage, toToolDescriptor } from "./tool-metadata.js";
9
+ import { isServerDisabled } from "./types.js";
9
10
  export const DEFAULT_MCP_SCRIPT_TIMEOUT_MS = 30_000;
10
11
  class McpScriptTimeoutError extends Error {
11
12
  constructor(timeoutMs) {
@@ -73,11 +74,9 @@ function parseWorkerMessage(value) {
73
74
  if (message.type === "emit" && "block" in message)
74
75
  return { type: "emit", block: message.block };
75
76
  if (message.type === "call" && typeof message.id === "number" && typeof message.path === "string") {
76
- return "args" in message
77
- ? { type: "call", id: message.id, path: message.path, args: message.args }
78
- : { type: "call", id: message.id, path: message.path };
77
+ return { type: "call", id: message.id, path: message.path, ...(message.args !== undefined ? { args: message.args } : {}), ...(typeof message.server === "string" ? { server: message.server } : {}) };
79
78
  }
80
- if ((message.type === "search" || message.type === "describe") && typeof message.id === "number") {
79
+ if ((message.type === "search" || message.type === "describe" || message.type === "resources" || message.type === "readResource" || message.type === "readResult") && typeof message.id === "number") {
81
80
  return "input" in message
82
81
  ? { type: message.type, id: message.id, input: message.input }
83
82
  : { type: message.type, id: message.id };
@@ -123,18 +122,21 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
123
122
  ? Math.max(0, Date.now() - startedAt)
124
123
  : operation.durationMs,
125
124
  }));
126
- let callsSnapshot;
127
- const callTool = async (innerCallId, path, args) => {
125
+ const pendingOperations = new Set();
126
+ const resultRefs = new Set();
127
+ const payloadNotices = new Set();
128
+ const recordCall = async (path, dispatch) => {
128
129
  // Record before dispatch so calls still in flight at timeout/abort appear in the trace.
129
130
  const startedAt = Date.now();
130
131
  const index = calls.push({ operation: "call", path, ok: false, error: "incomplete", durationMs: 0, startedAt }) - 1;
131
- let rawResult;
132
- let hasRawResult = false;
133
- const result = await executeCall(state, path, args, undefined, getPiTools, callSignal, (raw) => {
134
- rawResult = raw;
135
- hasRawResult = true;
136
- }, { ...(toolCallId !== undefined ? { toolCallId } : {}), innerCallId }, beforeDispatch);
132
+ const result = await dispatch();
137
133
  const details = result.details;
134
+ if (typeof details.resultRef === "string")
135
+ resultRefs.add(details.resultRef);
136
+ if (typeof details.resultWriteError === "string")
137
+ payloadNotices.add(`[MCP result could not be saved: ${details.resultWriteError}]`);
138
+ for (const file of (details.payloadFiles ?? []))
139
+ payloadNotices.add(formatMcpPayloadFile(file));
138
140
  if (details.error !== undefined) {
139
141
  const errorCode = String(details.error);
140
142
  const suggestions = Array.isArray(details.suggestions)
@@ -147,37 +149,57 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
147
149
  : textFromContent(result.content);
148
150
  calls[index] = {
149
151
  operation: "call", path, ok: false, error: errorCode, durationMs: Date.now() - startedAt, startedAt,
150
- ...(errorCode === "ambiguous_outcome" ? { recovery: details.recovery } : {}),
152
+ ...(details.recovery !== undefined ? { recovery: details.recovery } : {}),
151
153
  };
152
154
  if (errorCode === "call_capture_failed")
153
155
  throw new McpScriptCaptureError(details.recovery, message);
154
- if (errorCode === "ambiguous_outcome")
156
+ if (details.recovery !== undefined)
155
157
  output.push({ type: "text", text: message });
156
158
  return {
157
159
  ok: false,
160
+ ...(details.mcpResult !== undefined ? { data: details.mcpResult } : {}),
161
+ ...(details.resultRef ? { resultRef: details.resultRef } : {}),
158
162
  error: { code: errorCode, message, ...(details.recovery ? { recovery: details.recovery } : {}) },
159
163
  };
160
164
  }
161
165
  calls[index] = { operation: "call", path, ok: true, durationMs: Date.now() - startedAt, startedAt };
162
166
  return {
163
167
  ok: true,
164
- data: hasRawResult ? rawResult : details.mcpResult !== undefined ? details.mcpResult : textFromContent(result.content),
168
+ data: details.mcpResult !== undefined ? details.mcpResult : textFromContent(result.content),
169
+ ...(details.resultRef ? { resultRef: details.resultRef } : {}),
165
170
  };
166
171
  };
167
- const searchTools = (input) => {
172
+ const callIdentity = (innerCallId) => ({ ...(toolCallId !== undefined ? { toolCallId } : {}), innerCallId });
173
+ const callTool = (innerCallId, path, args, server) => recordCall(path, () => executeCall(state, path, args, server, getPiTools, callSignal, callIdentity(innerCallId), beforeDispatch, { raw: true }));
174
+ const discoverServer = async (server) => {
175
+ if (!server)
176
+ return undefined;
177
+ if (isServerDisabled(state.config.mcpServers[server]))
178
+ return { code: "server_disabled", message: `Server "${server}" is disabled.` };
179
+ if (state.config.mcpServers[server] && state.toolMetadata.has(server))
180
+ return undefined;
181
+ const result = await executeConnect(state, server, callSignal);
182
+ return result.details.error ? { code: result.details.error, message: textFromContent(result.content) } : undefined;
183
+ };
184
+ const searchTools = async (input) => {
168
185
  const startedAt = Date.now();
169
186
  const query = typeof input?.query === "string" ? input.query : "";
170
187
  let error;
171
188
  try {
172
- if (query.trim() === "") {
173
- return { items: [], total: 0, hasMore: false, nextOffset: null };
174
- }
175
189
  const server = typeof input?.server === "string" ? input.server : undefined;
190
+ const discoveryError = await discoverServer(server);
191
+ const coverage = catalogCoverage(state, server);
192
+ if (discoveryError) {
193
+ error = discoveryError.code;
194
+ return { error: discoveryError, coverage };
195
+ }
196
+ if (query.trim() === "")
197
+ return { items: [], total: 0, hasMore: false, nextOffset: null, coverage };
176
198
  const limit = typeof input?.limit === "number" ? input.limit : 12;
177
199
  const offset = typeof input?.offset === "number" ? input.offset : 0;
178
- const page = paginate(rankToolMatches(state, query, server), offset, limit);
200
+ const page = paginate(rankToolMatches(state, query, server).filter(match => !match.tool.resourceUri), offset, limit);
179
201
  return {
180
- ...page,
202
+ ...page, coverage,
181
203
  items: page.items.map(({ server: matchServer, tool, score }) => ({
182
204
  path: tool.name,
183
205
  name: tool.originalName,
@@ -197,26 +219,31 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
197
219
  : { operation: "search", query, ok: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startedAt, startedAt });
198
220
  }
199
221
  };
200
- const describeTool = (input) => {
222
+ const describeTool = async (input) => {
201
223
  const startedAt = Date.now();
202
224
  const path = typeof input?.path === "string" ? input.path : "";
203
225
  let error;
204
226
  try {
205
- for (const [server, metadata] of state.toolMetadata) {
206
- const tool = findToolByName(metadata, path);
207
- if (!tool)
208
- continue;
209
- const inputTypeScript = tool.inputSchema ? renderTsShape(tool.inputSchema) : null;
210
- return {
211
- path: tool.name,
212
- name: tool.originalName,
213
- server,
214
- ...(tool.description ? { description: tool.description } : {}),
215
- ...(inputTypeScript ? { inputTypeScript } : tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
216
- ...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
217
- };
227
+ const selectedServer = typeof input?.server === "string" ? input.server : undefined;
228
+ const discoveryError = await discoverServer(selectedServer);
229
+ if (discoveryError) {
230
+ error = discoveryError.code;
231
+ return { path, error: discoveryError };
232
+ }
233
+ const matches = [...state.toolMetadata].flatMap(([server, metadata]) => {
234
+ if ((selectedServer && server !== selectedServer) || !state.config.mcpServers[server] || isServerDisabled(state.config.mcpServers[server]))
235
+ return [];
236
+ const tools = metadata.filter(tool => !tool.resourceUri);
237
+ const originals = selectedServer ? tools.filter(tool => tool.originalName === path) : [];
238
+ return (originals.length ? originals : tools.filter(tool => tool.name === path)).map(tool => toToolDescriptor(server, tool));
239
+ });
240
+ if (matches.length === 1)
241
+ return matches[0];
242
+ if (matches.length > 1) {
243
+ error = "ambiguous_tool";
244
+ return { path, error: { code: "ambiguous_tool", message: "Specify server and exact tool name." } };
218
245
  }
219
- const suggestions = path ? rankSuggestions(state, path, 5) : [];
246
+ const suggestions = path ? rankSuggestions(state, path, 5, selectedServer) : [];
220
247
  error = "tool_not_found";
221
248
  return {
222
249
  path,
@@ -274,20 +301,40 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
274
301
  reject(new Error(message.message));
275
302
  return;
276
303
  }
277
- void (async () => {
304
+ const operation = (async () => {
278
305
  let envelope;
279
306
  if (message.type === "call") {
280
- envelope = await callTool(message.id, message.path, message.args);
307
+ envelope = await callTool(message.id, message.path, message.args, message.server);
281
308
  }
282
309
  else if (message.type === "search") {
283
- envelope = searchTools(message.input);
310
+ envelope = await searchTools(message.input);
311
+ }
312
+ else if (message.type === "describe") {
313
+ envelope = await describeTool(message.input);
314
+ }
315
+ else if (message.type === "readResult") {
316
+ envelope = await readMcpResult(message.input, resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory));
284
317
  }
285
318
  else {
286
- envelope = describeTool(message.input);
319
+ const input = message.input;
320
+ if (typeof input?.server !== "string" || !input.server || (message.type === "readResource" && (typeof input.uri !== "string" || !input.uri))) {
321
+ envelope = { error: { code: "invalid_arguments", message: "Specify server and, for reads, resource uri." } };
322
+ }
323
+ else if (message.type === "resources") {
324
+ const result = await executeResourceList(state, input.server, typeof input.limit === "number" ? input.limit : 12, typeof input.offset === "number" ? input.offset : 0, callSignal);
325
+ envelope = result.details.error ? { error: { code: result.details.error, message: textFromContent(result.content) } } : result.details;
326
+ }
327
+ else {
328
+ const server = input.server;
329
+ const uri = input.uri;
330
+ envelope = await recordCall(`${server}:${uri}`, () => executeResourceRead(state, server, uri, callSignal, callIdentity(message.id), beforeDispatch, true));
331
+ }
287
332
  }
288
333
  const response = { type: "result", id: message.id, envelope };
289
334
  activeWorker.postMessage(response);
290
- })().catch(reject);
335
+ })();
336
+ pendingOperations.add(operation);
337
+ void operation.catch(reject).finally(() => pendingOperations.delete(operation));
291
338
  });
292
339
  activeWorker.once("error", reject);
293
340
  activeWorker.once("exit", (code) => {
@@ -300,7 +347,6 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
300
347
  return;
301
348
  const timeoutError = new McpScriptTimeoutError(resolvedTimeoutMs);
302
349
  timer = setTimeout(() => {
303
- callsSnapshot = snapshotCalls();
304
350
  timeoutController.abort(timeoutError);
305
351
  void activeWorker.terminate();
306
352
  reject(timeoutError);
@@ -309,7 +355,6 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
309
355
  const aborted = externalSignal
310
356
  ? new Promise((_resolve, reject) => {
311
357
  const onAbort = () => {
312
- callsSnapshot = snapshotCalls();
313
358
  void activeWorker.terminate();
314
359
  reject(abortReasonError(externalSignal.reason));
315
360
  };
@@ -342,20 +387,22 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
342
387
  finally {
343
388
  clearTimeout(timer);
344
389
  removeAbortListener();
345
- // "incomplete" means the call had not settled when the script finished
346
- // (deadline, abort, or early return). Snapshot before aborting stragglers.
347
- callsSnapshot ??= snapshotCalls();
390
+ worker?.removeAllListeners("message");
348
391
  // A script may finish without awaiting every call; abort leftovers so
349
392
  // parent-side dispatches do not outlive the script.
350
393
  timeoutController.abort(new Error("mcp_script finished"));
394
+ // Let cancelled dispatches report whether an outcome is known before taking the trace.
395
+ await Promise.allSettled(pendingOperations);
351
396
  await worker?.terminate();
352
397
  }
398
+ const callsSnapshot = snapshotCalls();
353
399
  // Snapshot before the asynchronous output guard; the terminated worker can no longer emit.
354
- const guarded = await guardMcpOutput(output.length > 0 ? [...output] : [{ type: "text", text: "(no output)" }], resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory));
400
+ const guarded = await guardMcpOutput([...(output.length > 0 ? output : [{ type: "text", text: "(no output)" }]), ...[...payloadNotices].map(text => ({ type: "text", text })), ...[...resultRefs].map(ref => ({ type: "text", text: formatMcpResultReference(ref) }))], resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory));
355
401
  return {
356
402
  content: guarded.content,
357
403
  details: {
358
404
  mode: "script",
405
+ ...(resultRefs.size ? { resultRefs: [...resultRefs] } : {}),
359
406
  ...(errorCode ? { error: errorCode, message: errorMessage } : {}),
360
407
  timeoutMs: resolvedTimeoutMs,
361
408
  ...(recovery !== undefined ? { recovery } : {}),
@@ -49,11 +49,6 @@ export interface McpOAuthCallbacks {
49
49
  * Implements the OAuthClientProvider interface from the MCP SDK.
50
50
  */
51
51
  export declare class McpOAuthProvider implements OAuthClientProvider {
52
- private serverName;
53
- private serverUrl;
54
- private config;
55
- private callbacks;
56
- private storageOptions;
57
52
  private readonly redirectUrlSnapshot;
58
53
  readonly clientMetadataUrl?: string;
59
54
  private readonly controller;
@@ -66,6 +61,11 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
66
61
  private flowDiscoveryState;
67
62
  private flowIssuerMismatch;
68
63
  private flowState;
64
+ private serverName;
65
+ private serverUrl;
66
+ private config;
67
+ private callbacks;
68
+ private storageOptions;
69
69
  constructor(serverName: string, serverUrl: string, config: McpOAuthConfig, callbacks: McpOAuthCallbacks, storageOptions?: AuthStorageOptions, runtimeSignal?: AbortSignal, initialState?: string);
70
70
  private get isNonInteractive();
71
71
  private get discoveredIssuer();
@@ -152,11 +152,6 @@ function addAuthorizationParams(authorizationUrl, params) {
152
152
  * Implements the OAuthClientProvider interface from the MCP SDK.
153
153
  */
154
154
  export class McpOAuthProvider {
155
- serverName;
156
- serverUrl;
157
- config;
158
- callbacks;
159
- storageOptions;
160
155
  redirectUrlSnapshot;
161
156
  clientMetadataUrl;
162
157
  controller = new AbortController();
@@ -169,6 +164,11 @@ export class McpOAuthProvider {
169
164
  flowDiscoveryState;
170
165
  flowIssuerMismatch = false;
171
166
  flowState;
167
+ serverName;
168
+ serverUrl;
169
+ config;
170
+ callbacks;
171
+ storageOptions;
172
172
  constructor(serverName, serverUrl, config, callbacks, storageOptions = {}, runtimeSignal, initialState) {
173
173
  this.serverName = serverName;
174
174
  this.serverUrl = serverUrl;
@@ -38,16 +38,29 @@ export interface McpOutputGuardOptions {
38
38
  /**
39
39
  * Raw MCP result to expose as details.mcpResult. Kept raw when its JSON
40
40
  * fits detailsMaxBytes (or when the guard is disabled); otherwise replaced
41
- * with a compact summary and spilled to a temp file. Omit for call sites
42
- * whose details never carried the raw result (e.g. resource reads).
41
+ * with a compact summary and spilled to a file. Payloads that are not fully
42
+ * visible as plain text also get a model-visible reference for local readback.
43
43
  */
44
44
  rawMcpResult?: unknown;
45
+ retainedMcpResult?: RetainedMcpResult;
45
46
  }
46
47
  export interface GuardedMcpOutput {
47
48
  content: ContentBlock[];
48
49
  outputGuard?: McpOutputGuardDetails;
49
50
  mcpResult?: unknown;
51
+ resultRef?: string;
52
+ resultWriteError?: string;
53
+ payloadFiles?: McpPayloadFile[];
54
+ }
55
+ export interface McpPayloadFile {
56
+ index: number;
57
+ kind: "audio" | "resource";
58
+ uri?: string;
59
+ mimeType?: string;
60
+ path?: string;
61
+ error?: string;
50
62
  }
63
+ export type RetainedMcpResult = Pick<GuardedMcpOutput, "mcpResult" | "resultRef" | "resultWriteError" | "payloadFiles">;
51
64
  export declare function resolveMcpOutputGuardOptions(settings?: McpSettings, outputDirectory?: string): Pick<McpOutputGuardOptions, "enabled" | "maxBytes" | "maxLines" | "detailsMaxBytes" | "outputDirectory">;
52
65
  /** Spread helper for tool-result details: includes mcpResult/outputGuard only when present. */
53
66
  export declare function guardedMcpDetails(guarded: GuardedMcpOutput): Record<string, unknown>;
@@ -57,3 +70,45 @@ export declare function guardedMcpDetails(guarded: GuardedMcpOutput): Record<str
57
70
  * they are delivered to the provider as native image content, not text context.
58
71
  */
59
72
  export declare function guardMcpOutput(content: ContentBlock[], options?: McpOutputGuardOptions): Promise<GuardedMcpOutput>;
73
+ /**
74
+ * Bound details.mcpResult: keep the raw result when its JSON fits within
75
+ * detailsMaxBytes; otherwise replace it with a compact summary and spill the
76
+ * raw JSON to a temp file.
77
+ */
78
+ export declare function retainMcpResult(result: unknown, options?: McpOutputGuardOptions, rawResult?: boolean): Promise<RetainedMcpResult>;
79
+ export declare function formatMcpPayloadFile(file: McpPayloadFile): string;
80
+ export declare function formatMcpResultReference(ref: string): string;
81
+ export interface ReadMcpResultInput {
82
+ ref: string;
83
+ /** RFC 6901 JSON Pointer. Omit to read the entire retained result. */
84
+ path?: string;
85
+ fields?: string[];
86
+ /** Zero-based character offset in the selected, pretty-printed value. */
87
+ offset?: number;
88
+ /** Maximum characters, additionally bounded by the configured output limits. */
89
+ limit?: number;
90
+ }
91
+ export declare function readMcpResult(input: ReadMcpResultInput, options?: McpOutputGuardOptions): Promise<{
92
+ content: {
93
+ type: "text";
94
+ text: string;
95
+ }[];
96
+ details: {
97
+ ref: string;
98
+ offset: number;
99
+ nextOffset: number | null;
100
+ totalCharacters: number;
101
+ path?: string;
102
+ error?: never;
103
+ message?: never;
104
+ };
105
+ } | {
106
+ content: {
107
+ type: "text";
108
+ text: string;
109
+ }[];
110
+ details: {
111
+ error: string;
112
+ message: string;
113
+ };
114
+ }>;