@fitchmultz/pi-mcp-adapter 7.1.1 → 8.0.1

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/dist/config.js CHANGED
@@ -49,7 +49,7 @@ export const KNOWN_SERVER_PRESETS = [
49
49
  id: "chrome-devtools",
50
50
  name: "Chrome DevTools",
51
51
  summary: "Inspect and automate a local Chrome browser.",
52
- entry: { command: "npx", args: ["-y", "chrome-devtools-mcp@latest"] },
52
+ entry: { command: "npx", args: ["-y", "chrome-devtools-mcp"] },
53
53
  },
54
54
  ];
55
55
  const IMPORT_PATHS = {
@@ -3,6 +3,7 @@ import { parseDirectToolSelectors } from "./metadata-cache.js";
3
3
  export { getMissingConfiguredDirectToolServers } from "./metadata-cache.js";
4
4
  import { executeCall } from "./proxy-modes.js";
5
5
  import { isServerDisabled } from "./types.js";
6
+ import { nativeMcpResult } from "./tool-registrar.js";
6
7
  export function directToolSelection(config, serverName, envOverride) {
7
8
  if (envOverride) {
8
9
  const selection = parseDirectToolSelectors(envOverride);
@@ -33,20 +34,20 @@ export function createDirectToolExecutor(getState, getInitPromise, spec, beforeE
33
34
  catch (error) {
34
35
  throwIfAborted(signal);
35
36
  const message = error instanceof Error ? error.message : String(error);
36
- return {
37
+ return nativeMcpResult({
37
38
  content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
38
39
  details: { error: "init_failed", message },
39
- };
40
+ }, !!spec.outputSchema);
40
41
  }
41
42
  }
42
43
  if (!state) {
43
- return {
44
+ return nativeMcpResult({
44
45
  content: [{ type: "text", text: "MCP not initialized" }],
45
46
  details: { error: "not_initialized" },
46
- };
47
+ }, !!spec.outputSchema);
47
48
  }
48
49
  const result = await executeCall(state, spec.originalName, params, spec.serverName, undefined, signal, { toolCallId }, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined, { exactOriginalName: true, ...(spec.resourceUri ? { resourceUri: spec.resourceUri } : {}) });
49
50
  const { mode: _mode, ...details } = result.details;
50
- return { ...result, details };
51
+ return nativeMcpResult({ ...result, details }, !!spec.outputSchema);
51
52
  };
52
53
  }
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Decide the `isError` override for a finished tool result in the `tool_result` hook.
2
+ * Classify returned MCP execution failures for Pi's native result-level isError.
3
3
  *
4
4
  * A failed MCP tool call is *returned* (not thrown), tagged `details.error: "tool_error"` (the server
5
- * returned an error result) or `"call_failed"` (the call itself threw and was caught). pi never reads a
6
- * result-level `isError`, so without this such a call is recorded as a success. Returning
7
- * `{ isError: true }` (and nothing else) flips the flag; pi's field-by-field merge keeps the original
8
- * `content` and `details` intact.
5
+ * returned an error result) or `"call_failed"` (the call itself threw and was caught).
6
+ * Spread this onto the returned result without replacing human or structured content.
9
7
  *
10
8
  * Limited to those two codes: the adapter's other `details.error` values (`auth_required`, connection
11
9
  * states, search/validation feedback, ...) are not failed tool calls, so they get no override.
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Decide the `isError` override for a finished tool result in the `tool_result` hook.
2
+ * Classify returned MCP execution failures for Pi's native result-level isError.
3
3
  *
4
4
  * A failed MCP tool call is *returned* (not thrown), tagged `details.error: "tool_error"` (the server
5
- * returned an error result) or `"call_failed"` (the call itself threw and was caught). pi never reads a
6
- * result-level `isError`, so without this such a call is recorded as a success. Returning
7
- * `{ isError: true }` (and nothing else) flips the flag; pi's field-by-field merge keeps the original
8
- * `content` and `details` intact.
5
+ * returned an error result) or `"call_failed"` (the call itself threw and was caught).
6
+ * Spread this onto the returned result without replacing human or structured content.
9
7
  *
10
8
  * Limited to those two codes: the adapter's other `details.error` values (`auth_required`, connection
11
9
  * states, search/validation feedback, ...) are not failed tool calls, so they get no override.
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, exe
10
10
  import { formatTerminalError } from "./utils.js";
11
11
  import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.js";
12
12
  import { renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.js";
13
- import { toolErrorOverride } from "./error-signal.js";
14
13
  import { createMcpRuntimeOwner, createOwnedUi, isAbortError } from "./runtime-owner.js";
15
14
  import { publishMcpStatusShutdown } from "./mcp-status.js";
16
15
  import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.js";
@@ -20,7 +19,6 @@ import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.
20
19
  import { executeResourceList, executeResourceRead } from "./resource-tools.js";
21
20
  import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
22
21
  import { abortable, throwIfAborted } from "./abort.js";
23
- import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.js";
24
22
  export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
25
23
  const INIT_WAIT_TIMEOUT_MS = 30_000;
26
24
  const INIT_WAIT_TIMED_OUT = Symbol("init-wait-timed-out");
@@ -269,13 +267,6 @@ function installMcpAdapter(pi, options) {
269
267
  if (ctx.cwd !== runtimeCwd)
270
268
  await startSession(ctx);
271
269
  });
272
- onSessionCheckpoint(pi, async (event) => {
273
- if (initPromise)
274
- return { sleepReady: false, reason: "MCP initialization is active" };
275
- if (!state || !currentOwner?.isActive())
276
- return { sleepReady: false, reason: "MCP runtime is not initialized" };
277
- return prepareMcpCheckpoint(state, event);
278
- });
279
270
  pi.on("session_shutdown", async () => {
280
271
  // Cached tool selections exist before background server initialization finishes.
281
272
  toolLoader.persist();
@@ -300,8 +291,6 @@ function installMcpAdapter(pi, options) {
300
291
  if (failures.length)
301
292
  throw new AggregateError(failures, "MCP session shutdown persistence/cleanup failed");
302
293
  });
303
- // Re-flag returned MCP tool failures so pi registers them as errors (see toolErrorOverride).
304
- pi.on("tool_result", (event) => toolErrorOverride(event.details));
305
294
  function createCommandContext(ctx) {
306
295
  const owner = currentOwner;
307
296
  const hasUI = ctx.hasUI;
package/dist/init.js CHANGED
@@ -43,7 +43,6 @@ export function recordFailure(state, serverName, message) {
43
43
  getFailureExpiryTimers(state).delete(serverName);
44
44
  return;
45
45
  }
46
- state.owner.beforeActivity();
47
46
  if (state.failureTracker.get(serverName) === failedAt) {
48
47
  state.failureTracker.delete(serverName);
49
48
  state.failureMessages?.delete(serverName);
@@ -15,10 +15,6 @@ export declare class McpLifecycleManager {
15
15
  private activeHealthCheck;
16
16
  private shutdownPromise;
17
17
  private stopped;
18
- private checkpointPauses;
19
- hasActiveHealthCheck(): boolean;
20
- /** Skip ticks, without cancelling accepted work or permanently closing the manager. */
21
- pauseForCheckpoint(): () => void;
22
18
  private healthSignal;
23
19
  private removeHealthAbortListener;
24
20
  private readonly manager;
package/dist/lifecycle.js CHANGED
@@ -14,19 +14,6 @@ export class McpLifecycleManager {
14
14
  activeHealthCheck;
15
15
  shutdownPromise;
16
16
  stopped = false;
17
- checkpointPauses = 0;
18
- hasActiveHealthCheck() { return this.activeHealthCheck !== undefined; }
19
- /** Skip ticks, without cancelling accepted work or permanently closing the manager. */
20
- pauseForCheckpoint() {
21
- this.checkpointPauses++;
22
- let released = false;
23
- return () => {
24
- if (!released) {
25
- released = true;
26
- this.checkpointPauses--;
27
- }
28
- };
29
- }
30
17
  healthSignal;
31
18
  removeHealthAbortListener;
32
19
  manager;
@@ -81,7 +68,7 @@ export class McpLifecycleManager {
81
68
  signal?.addEventListener("abort", stop, { once: true });
82
69
  this.removeHealthAbortListener = () => signal?.removeEventListener("abort", stop);
83
70
  this.healthCheckInterval = setInterval(() => {
84
- if (this.stopped || signal?.aborted || this.activeHealthCheck || this.checkpointPauses > 0)
71
+ if (this.stopped || signal?.aborted || this.activeHealthCheck)
85
72
  return;
86
73
  const check = this.checkConnections(signal)
87
74
  .catch(error => {
@@ -9,7 +9,6 @@ import { type AuthStorageOptions } from "./mcp-auth.ts";
9
9
  import { type ServerEntry } from "./types.ts";
10
10
  /** Auth status for a server */
11
11
  export type AuthStatus = "authenticated" | "expired" | "not_authenticated";
12
- import type { McpCheckpointEvent } from "./checkpoint.ts";
13
12
  export interface McpOAuthRuntime {
14
13
  readonly signal: AbortSignal;
15
14
  }
@@ -20,8 +19,6 @@ export interface AuthenticateOptions {
20
19
  runtime?: McpOAuthRuntime;
21
20
  }
22
21
  export declare function createOAuthRuntime(signal?: AbortSignal): McpOAuthRuntime;
23
- export declare function holdOAuthCheckpoint(runtime: McpOAuthRuntime, event: McpCheckpointEvent): () => void;
24
- export declare function getOAuthCheckpointBlocker(runtime: McpOAuthRuntime): string | undefined;
25
22
  type OAuthRequest = {
26
23
  serverName: string;
27
24
  serverUrl: string;
@@ -46,8 +43,7 @@ export declare function extractOAuthConfig(definition: ServerEntry): McpOAuthCon
46
43
  * Start OAuth authentication flow for a server.
47
44
  * Returns the authorization URL when browser authorization is required.
48
45
  */
49
- export declare const startAuth: typeof startAuthOperation;
50
- declare function startAuthOperation(serverName: string, serverUrl: string, definition?: ServerEntry, options?: AuthenticateOptions): Promise<{
46
+ export declare function startAuth(serverName: string, serverUrl: string, definition?: ServerEntry, options?: AuthenticateOptions): Promise<{
51
47
  authorizationUrl: string;
52
48
  }>;
53
49
  /** Authorization code plus the optional RFC 9207 `iss` callback parameter. */
@@ -74,8 +70,7 @@ export declare function completeAuthFromInput(serverName: string, input?: string
74
70
  /**
75
71
  * Complete OAuth authentication with an explicit code or a captured callback.
76
72
  */
77
- export declare const completeAuth: typeof completeAuthOperation;
78
- declare function completeAuthOperation(serverName: string, authorizationCode?: string | AuthorizationResponseInput, options?: AuthenticateOptions): Promise<AuthStatus>;
73
+ export declare function completeAuth(serverName: string, authorizationCode?: string | AuthorizationResponseInput, options?: AuthenticateOptions): Promise<AuthStatus>;
79
74
  /**
80
75
  * Perform the complete OAuth authentication flow for a server.
81
76
  *
@@ -90,8 +85,7 @@ export declare function authenticate(serverName: string, serverUrl: string, defi
90
85
  *
91
86
  * @param serverName - The name of the MCP server
92
87
  */
93
- export declare const removeAuth: typeof removeAuthOperation;
94
- declare function removeAuthOperation(serverName: string, options?: AuthenticateOptions): Promise<void>;
88
+ export declare function removeAuth(serverName: string, options?: AuthenticateOptions): Promise<void>;
95
89
  /**
96
90
  * Check if OAuth is supported for a server configuration.
97
91
  * OAuth is supported for HTTP servers unless explicitly disabled.
@@ -42,7 +42,6 @@ export function createOAuthRuntime(signal) {
42
42
  runtimeStates.set(runtime, {
43
43
  controller,
44
44
  generation: 0,
45
- activeOperations: 0,
46
45
  pendingAuths: new Map(),
47
46
  pendingAuthStates: new Map(),
48
47
  pendingAuthCleanupTimers: new Map(),
@@ -71,36 +70,6 @@ function getRuntimeState(runtime) {
71
70
  throw new Error("Unknown OAuth runtime");
72
71
  return state;
73
72
  }
74
- export function holdOAuthCheckpoint(runtime, event) {
75
- const state = getRuntimeState(runtime);
76
- if (state.checkpoint)
77
- throw new Error("MCP OAuth checkpoint is already held");
78
- state.checkpoint = event;
79
- return () => { if (state.checkpoint === event)
80
- state.checkpoint = undefined; };
81
- }
82
- export function getOAuthCheckpointBlocker(runtime) {
83
- const state = getRuntimeState(runtime);
84
- if (state.activeOperations || state.pendingAuthentications.size)
85
- return "MCP OAuth operation is active";
86
- if (state.pendingAuths.size || state.pendingAuthStates.size || state.pendingAuthCleanupTimers.size)
87
- return "MCP browser OAuth callback/flow is pending";
88
- // requests caches server challenges/discovery, not a pending user callback.
89
- // Cold connect re-challenges; granted scopes and issuer bindings live in the
90
- // native credential store. Actual interactive/auth operations veto above.
91
- return undefined;
92
- }
93
- async function withOAuthOperation(runtime, run) {
94
- const state = getRuntimeState(runtime);
95
- state.checkpoint?.invalidate();
96
- state.activeOperations++;
97
- try {
98
- return await run();
99
- }
100
- finally {
101
- state.activeOperations--;
102
- }
103
- }
104
73
  function getPendingAuthKey(serverName, options) {
105
74
  return JSON.stringify([serverName, getAuthBaseDir(options)]);
106
75
  }
@@ -118,7 +87,6 @@ export function getOAuthRequest(serverName, serverUrl, storageBase, runtime = le
118
87
  export function recordOAuthChallenge(serverName, serverUrl, storageBase, error, runtime = legacyRuntime) {
119
88
  if (runtime.signal.aborted || !isOAuthChallenge(error))
120
89
  return;
121
- getRuntimeState(runtime).checkpoint?.invalidate();
122
90
  const previous = getOAuthRequest(serverName, serverUrl, storageBase, runtime);
123
91
  const request = { ...previous, serverName, serverUrl };
124
92
  if (error instanceof InsufficientScopeError) {
@@ -315,8 +283,7 @@ function parseOAuthRedirectUri(redirectUri) {
315
283
  * Start OAuth authentication flow for a server.
316
284
  * Returns the authorization URL when browser authorization is required.
317
285
  */
318
- export const startAuth = async (...args) => withOAuthOperation(getRuntime(args[3]), () => startAuthOperation(...args));
319
- async function startAuthOperation(serverName, serverUrl, definition, options = {}) {
286
+ export async function startAuth(serverName, serverUrl, definition, options = {}) {
320
287
  if (isServerDisabled(definition))
321
288
  throw new Error(`MCP server "${serverName}" is disabled`);
322
289
  const runtime = getRuntime(options);
@@ -479,10 +446,7 @@ async function setPendingAuth(runtime, serverName, pendingAuth, oauthState, sign
479
446
  cleanupTimer.unref?.();
480
447
  state.pendingAuthCleanupTimers.set(key, cleanupTimer);
481
448
  }
482
- function clearPendingAuth(...args) {
483
- return withOAuthOperation(args[0], () => clearPendingAuthOperation(...args));
484
- }
485
- async function clearPendingAuthOperation(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
449
+ async function clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
486
450
  const state = getRuntimeState(runtime);
487
451
  const key = getPendingAuthKey(serverName, fallbackStorageOptions);
488
452
  const pendingAuth = state.pendingAuths.get(key);
@@ -576,8 +540,7 @@ export async function completeAuthFromInput(serverName, input, options = {}) {
576
540
  /**
577
541
  * Complete OAuth authentication with an explicit code or a captured callback.
578
542
  */
579
- export const completeAuth = async (...args) => withOAuthOperation(getRuntime(args[2]), () => completeAuthOperation(...args));
580
- async function completeAuthOperation(serverName, authorizationCode, options = {}) {
543
+ export async function completeAuth(serverName, authorizationCode, options = {}) {
581
544
  const runtime = getRuntime(options);
582
545
  const runtimeState = getRuntimeState(runtime);
583
546
  const fallbackAuthStorageOptions = options.authStorageOptions ?? {};
@@ -743,8 +706,7 @@ export async function authenticate(serverName, serverUrl, definition, options =
743
706
  *
744
707
  * @param serverName - The name of the MCP server
745
708
  */
746
- export const removeAuth = async (...args) => withOAuthOperation(getRuntime(args[1]), () => removeAuthOperation(...args));
747
- async function removeAuthOperation(serverName, options = {}) {
709
+ export async function removeAuth(serverName, options = {}) {
748
710
  const runtime = getRuntime(options);
749
711
  const signal = combineAbortSignals(runtime.signal, options.signal);
750
712
  throwIfAborted(signal);
@@ -811,7 +773,6 @@ export async function shutdownOAuth(runtime = legacyRuntime) {
811
773
  const state = getRuntimeState(runtime);
812
774
  if (state.controller.signal.aborted)
813
775
  return;
814
- state.checkpoint?.invalidate();
815
776
  state.generation += 1;
816
777
  state.controller.abort(new Error("OAuth runtime stopped"));
817
778
  for (const callbackState of Array.from(state.pendingAuthStates.values()))
package/dist/mcp-code.js CHANGED
@@ -98,7 +98,6 @@ class McpScriptCaptureError extends Error {
98
98
  }
99
99
  export const runMcpScript = async (...args) => {
100
100
  const state = args[0];
101
- state.owner?.beforeActivity();
102
101
  state.activeScripts = (state.activeScripts ?? 0) + 1;
103
102
  try {
104
103
  return await runMcpScriptOperation(...args);
@@ -78,6 +78,11 @@ export declare function guardMcpOutput(content: ContentBlock[], options?: McpOut
78
78
  export declare function retainMcpResult(result: unknown, options?: McpOutputGuardOptions, rawResult?: boolean): Promise<RetainedMcpResult>;
79
79
  export declare function formatMcpPayloadFile(file: McpPayloadFile): string;
80
80
  export declare function formatMcpResultReference(ref: string): string;
81
+ /** Stream histories through the same private output-artifact and paged-reader boundary. */
82
+ export declare function saveMcpOutputStream(chunks: AsyncIterable<string>, outputDirectory?: string): Promise<{
83
+ path?: string;
84
+ error?: string;
85
+ }>;
81
86
  export interface ReadMcpResultInput {
82
87
  ref: string;
83
88
  /** RFC 6901 JSON Pointer. Omit to read the entire retained result. */
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { mkdir, mkdtemp, readFile, realpath, writeFile } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, open, readFile, realpath, writeFile } from "node:fs/promises";
3
+ import { createReadStream } from "node:fs";
3
4
  import { tmpdir } from "node:os";
4
5
  import { basename, dirname, join, resolve } from "node:path";
5
6
  export const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;
@@ -320,6 +321,26 @@ async function saveArtifact(kind, text, outputDirectory) {
320
321
  return { error: error instanceof Error ? error.message : String(error) };
321
322
  }
322
323
  }
324
+ /** Stream histories through the same private output-artifact and paged-reader boundary. */
325
+ export async function saveMcpOutputStream(chunks, outputDirectory) {
326
+ const artifact = await saveArtifact("output", "", outputDirectory);
327
+ if (!artifact.path)
328
+ return artifact;
329
+ try {
330
+ const file = await open(artifact.path, "a");
331
+ try {
332
+ for await (const chunk of chunks)
333
+ await file.writeFile(chunk);
334
+ }
335
+ finally {
336
+ await file.close();
337
+ }
338
+ return artifact;
339
+ }
340
+ catch (error) {
341
+ return { error: error instanceof Error ? error.message : String(error) };
342
+ }
343
+ }
323
344
  export async function readMcpResult(input, options = {}) {
324
345
  try {
325
346
  if (!input || typeof input.ref !== "string")
@@ -330,6 +351,39 @@ export async function readMcpResult(input, options = {}) {
330
351
  if (dirname(dirname(ref)) !== root || !/^pi-mcp-output-/.test(basename(dirname(ref)))
331
352
  || !/^(mcp-result|output)-[a-f0-9]+\.txt$/.test(basename(ref)))
332
353
  throw new Error("ref is not an MCP output artifact in this host's output directory");
354
+ // Plain output pages without flattening a complete UI history into memory.
355
+ if (basename(ref).startsWith("output-") && input.path === undefined && input.fields === undefined) {
356
+ const offset = input.offset ?? 0;
357
+ const limit = input.limit ?? 12_000;
358
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1)
359
+ throw new Error("offset must be a non-negative integer and limit a positive integer");
360
+ const maxBytes = Math.max(1, (options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES) - 256);
361
+ const maxLines = Math.max(1, (options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES) - 1);
362
+ let total = 0, bytes = 0, lines = 1;
363
+ let page = "";
364
+ let finished = false;
365
+ for await (const chunk of createReadStream(ref, { encoding: "utf8" })) {
366
+ for (const char of chunk) {
367
+ if (total < offset && total + char.length > offset)
368
+ throw new Error("offset must not split a Unicode surrogate pair");
369
+ if (total >= offset && !finished) {
370
+ const nextBytes = bytes + byteLength(char);
371
+ const nextLines = lines + (char === "\n" ? 1 : 0);
372
+ if (page && (nextBytes > maxBytes || nextLines > maxLines || page.length + char.length > limit))
373
+ finished = true;
374
+ else {
375
+ page += char;
376
+ bytes = nextBytes;
377
+ lines = nextLines;
378
+ }
379
+ }
380
+ total += char.length;
381
+ }
382
+ }
383
+ const end = offset + page.length;
384
+ const nextOffset = end < total ? end : null;
385
+ return { content: [{ type: "text", text: page }, ...(nextOffset !== null ? [{ type: "text", text: `[MCP result page: ${offset}–${end} of ${total} characters. Continue with offset: ${end}.]` }] : [])], details: { ref, offset, nextOffset, totalCharacters: total } };
386
+ }
333
387
  let rendered = await readFile(ref, "utf8");
334
388
  if (input.path !== undefined || input.fields !== undefined || basename(ref).startsWith("mcp-result-")) {
335
389
  let value = JSON.parse(rendered);
@@ -461,7 +461,7 @@ export class McpSetupPanel {
461
461
  ' "mcpServers": {',
462
462
  ' "chrome-devtools": {',
463
463
  ' "command": "npx",',
464
- ' "args": ["-y", "chrome-devtools-mcp@latest"]',
464
+ ' "args": ["-y", "chrome-devtools-mcp"]',
465
465
  " }",
466
466
  " }",
467
467
  "}",
@@ -4,7 +4,7 @@ import type { ServerConnection } from "./server-manager.ts";
4
4
  import type { ToolMetadata, McpOperationContext, McpToolCallIdentity } from "./types.ts";
5
5
  import { type SessionRecoveryDeps } from "./session-recovery.ts";
6
6
  type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
7
- export declare function executeUiMessages(state: McpExtensionState): ProxyToolResult;
7
+ export declare function executeUiMessages(state: McpExtensionState): Promise<ProxyToolResult>;
8
8
  export declare function executeStatus(state: McpExtensionState): ProxyToolResult;
9
9
  export declare function executeAuthStart(state: McpExtensionState, serverName: string, signal?: AbortSignal): Promise<ProxyToolResult>;
10
10
  export declare function executeAuthComplete(state: McpExtensionState, serverName: string, input?: string, signal?: AbortSignal): Promise<ProxyToolResult>;
@@ -6,13 +6,14 @@ import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
6
6
  import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToolDescriptor } from "./tool-metadata.js";
7
7
  import { reconstructPromptMetadata } from "./metadata-cache.js";
8
8
  import { renderMcpResultContent } from "./tool-registrar.js";
9
- import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference } from "./mcp-output-guard.js";
9
+ import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference, saveMcpOutputStream } from "./mcp-output-guard.js";
10
10
  import { maybeStartUiSession, summarizeUiSessionResult } from "./ui-session.js";
11
11
  import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
12
12
  import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.js";
13
13
  import { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
14
14
  import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
15
15
  import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
16
+ import { materializeUiMessages, uiMessages } from "./ui-message-history.js";
16
17
  const INSTRUCTIONS_PREVIEW_LENGTH = 300;
17
18
  function disabledResult(mode, serverName) {
18
19
  const message = `Server "${serverName}" is disabled. Run /mcp enable ${serverName} and /reload to enable it.`;
@@ -120,8 +121,80 @@ async function attemptAutoAuth(state, serverName, signal) {
120
121
  };
121
122
  }
122
123
  }
123
- export function executeUiMessages(state) {
124
- const sessions = state.completedUiSessions;
124
+ async function* uiSessionText(session) {
125
+ yield `\n## ${session.serverName} / ${session.toolName} (${session.completedAt.toLocaleTimeString()}, ${session.reason})`;
126
+ let heading = false;
127
+ for await (const prompt of uiMessages(session.messages, "prompts")) {
128
+ if (parseUiPromptHandoff(prompt))
129
+ continue;
130
+ if (!heading) {
131
+ yield "\n\n### Prompts:";
132
+ heading = true;
133
+ }
134
+ yield `\n- ${prompt}`;
135
+ }
136
+ heading = false;
137
+ for await (const value of uiMessages(session.messages, "intents")) {
138
+ const intent = value;
139
+ if (!heading) {
140
+ yield "\n\n### Intents:";
141
+ heading = true;
142
+ }
143
+ yield `\n- ${intent.intent}${intent.params ? ` (${JSON.stringify(intent.params)})` : ""}`;
144
+ }
145
+ for await (const prompt of uiMessages(session.messages, "prompts")) {
146
+ const handoff = parseUiPromptHandoff(prompt);
147
+ if (!handoff)
148
+ continue;
149
+ if (!heading) {
150
+ yield "\n\n### Intents:";
151
+ heading = true;
152
+ }
153
+ yield `\n- ${handoff.intent} (${JSON.stringify(handoff.params)})`;
154
+ }
155
+ if (session.messages.contexts.length) {
156
+ yield "\n\n### Context updates:";
157
+ for (const context of session.messages.contexts)
158
+ yield `\n- ${context.summary}${context.truncated ? " (truncated)" : ""}`;
159
+ }
160
+ heading = false;
161
+ for await (const notification of uiMessages(session.messages, "notifications")) {
162
+ if (!heading) {
163
+ yield "\n\n### Notifications:";
164
+ heading = true;
165
+ }
166
+ yield `\n- ${notification}`;
167
+ }
168
+ }
169
+ export async function executeUiMessages(state) {
170
+ const pending = [...state.completedUiSessions];
171
+ const options = resolveMcpOutputGuardOptions(state.config?.settings, state.outputDirectory);
172
+ // Bound both inline output and details. Large retrieval streams; it never flattens all events.
173
+ const size = pending.reduce((bytes, session) => bytes + (session.messages.historyBytes ?? 0) + Buffer.byteLength(JSON.stringify(session.messages)), 0);
174
+ const large = size > Math.min(options.maxBytes, options.detailsMaxBytes);
175
+ const warnings = pending.flatMap(session => session.messages.historyError ? [session.messages.historyError] : []);
176
+ const paged = async () => {
177
+ async function* output() {
178
+ yield `UI Session Messages (${pending.length} sessions):\n`;
179
+ for (const session of pending)
180
+ yield* uiSessionText(session);
181
+ }
182
+ const artifact = await saveMcpOutputStream(output(), state.outputDirectory);
183
+ if (!artifact.path)
184
+ return { content: [{ type: "text", text: `UI messages remain available; retrieval could not be saved: ${artifact.error}` }], isError: true, details: { error: "ui_history_delivery_failed", cleared: false } };
185
+ // Newly completed sessions during I/O are not part of this drain.
186
+ state.completedUiSessions = state.completedUiSessions.filter(session => !pending.includes(session));
187
+ return { content: [{ type: "text", text: `UI Session Messages (${pending.length} sessions). ${formatMcpResultReference(artifact.path)}${warnings.length ? `\nHistory persistence warning: ${warnings.join("; ")}` : ""}` }], details: { sessions: pending.length, resultRef: artifact.path, historyRefs: pending.flatMap(session => session.messages.historyRef ? [session.messages.historyRef] : []), cleared: true } };
188
+ };
189
+ if (large)
190
+ return paged();
191
+ let sessions;
192
+ try {
193
+ sessions = await Promise.all(pending.map(async (session) => ({ ...session, messages: await materializeUiMessages(session.messages) })));
194
+ }
195
+ catch (error) {
196
+ return { content: [{ type: "text", text: `UI messages remain available; history could not be read: ${String(error)}` }], isError: true, details: { error: "ui_history_delivery_failed", cleared: false } };
197
+ }
125
198
  if (sessions.length === 0) {
126
199
  return {
127
200
  content: [{ type: "text", text: "No UI session messages available." }],
@@ -183,9 +256,8 @@ export function executeUiMessages(state) {
183
256
  }
184
257
  }
185
258
  const count = sessions.length;
186
- state.completedUiSessions = [];
187
- return {
188
- content: [{ type: "text", text: output.join("\n") }],
259
+ const inline = {
260
+ content: [{ type: "text", text: output.join("\n") + (warnings.length ? `\nHistory persistence warning: ${warnings.join("; ")}` : "") }],
189
261
  details: {
190
262
  sessions: count,
191
263
  prompts: allPrompts,
@@ -195,6 +267,11 @@ export function executeUiMessages(state) {
195
267
  cleared: true,
196
268
  },
197
269
  };
270
+ const text = inline.content.map(block => block.type === "text" ? block.text : "").join("\n");
271
+ if (Buffer.byteLength(JSON.stringify(inline.details)) > options.detailsMaxBytes || Buffer.byteLength(text) > options.maxBytes || text.split("\n").length > options.maxLines)
272
+ return paged();
273
+ state.completedUiSessions = state.completedUiSessions.filter(session => !pending.includes(session));
274
+ return inline;
198
275
  }
199
276
  export function executeStatus(state) {
200
277
  const servers = [];
@@ -699,7 +776,7 @@ export async function runToolCall(state, serverName, target, args, options) {
699
776
  beforeDispatchPending = false;
700
777
  }
701
778
  throwIfAborted(callerSignal);
702
- // Host checkpoints and UI preparation do not consume the service deadline.
779
+ // Approval and UI preparation do not consume the service deadline.
703
780
  const ownedSignal = combineAbortSignals(callerSignal, AbortSignal.timeout(Math.ceil(configuredOptions?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC)));
704
781
  const requestOptions = { ...configuredOptions, signal: ownedSignal };
705
782
  const recovery = { manager: state.manager, config: state.config, signal: ownedSignal, onNeedsAuth: recoverAuthConnection };
@@ -735,7 +812,7 @@ export async function runToolCall(state, serverName, target, args, options) {
735
812
  if (options.raw) {
736
813
  const message = isError
737
814
  ? (Array.isArray(record.content) ? record.content.filter(block => block.type === "text").map(block => block.text).join("\n") : "") || "Tool execution failed" : undefined;
738
- return { content: [], details: { ...details, ...retained, ...(message ? { message } : {}) } };
815
+ return { content: [], ...(isError ? { isError: true } : {}), details: { ...details, ...retained, ...(message ? { message } : {}) } };
739
816
  }
740
817
  const content = renderMcpResultContent(record, retained.payloadFiles);
741
818
  const guarded = await guardMcpOutput(content.length ? content : [{ type: "text", text: target.resourceUri ? "(empty resource)" : "(empty result)" }], {
@@ -743,7 +820,7 @@ export async function runToolCall(state, serverName, target, args, options) {
743
820
  ...(isError ? { prefix: "Error: ", suffix: schemaSuffix(), emptyTextFallback: "Tool execution failed" }
744
821
  : uiSummary ? { suffix: `\n\n${uiSummary.message}` } : {}),
745
822
  });
746
- return { content: guarded.content, details: { ...details, ...guardedMcpDetails(guarded) } };
823
+ return { content: guarded.content, ...(isError ? { isError: true } : {}), details: { ...details, ...guardedMcpDetails(guarded) } };
747
824
  }
748
825
  catch (error) {
749
826
  // Direct/proxy host errors throw; scripts retain their failed-call envelope.
@@ -759,6 +836,7 @@ export async function runToolCall(state, serverName, target, args, options) {
759
836
  ? await retainMcpResult(captureFailure.result, { ...outputGuardOptions, enabled: true, detailsMaxBytes: 1 }) : {};
760
837
  return {
761
838
  content: [{ type: "text", text: message }, ...(retained.resultRef ? [{ type: "text", text: formatMcpResultReference(retained.resultRef) }] : [])],
839
+ isError: true,
762
840
  details: { ...detailsBase, ...retained, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
763
841
  };
764
842
  }
@@ -767,6 +845,7 @@ export async function runToolCall(state, serverName, target, args, options) {
767
845
  uiSession?.sendToolCancelled(message);
768
846
  return {
769
847
  content: [{ type: "text", text: message }],
848
+ isError: true,
770
849
  details: { ...detailsBase, error: "ambiguous_outcome", message, recovery: { ...identity, action: "readback" } },
771
850
  };
772
851
  }
@@ -794,6 +873,7 @@ export async function runToolCall(state, serverName, target, args, options) {
794
873
  const guarded = await guardMcpOutput([{ type: "text", text: message }], { ...outputGuardOptions, prefix: "Failed to call tool: ", suffix: schemaSuffix() });
795
874
  return {
796
875
  content: guarded.content,
876
+ isError: true,
797
877
  details: {
798
878
  ...detailsBase,
799
879
  error: isAbortError(error, callerSignal) ? "aborted" : "call_failed",
@@ -1,4 +1,3 @@
1
- import type { McpCheckpointEvent } from "./checkpoint.ts";
2
1
  import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
3
2
  export interface McpRuntimeOwner {
4
3
  readonly signal: AbortSignal;
@@ -6,10 +5,7 @@ export interface McpRuntimeOwner {
6
5
  addCleanup(cleanup: () => void | Promise<void>): void;
7
6
  stop(reason?: string): Promise<void>;
8
7
  throwIfInactive(): void;
9
- beforeActivity(): void;
10
- holdCheckpoint(event: McpCheckpointEvent): () => void;
11
8
  runCallback<T>(kind: "beforeExecute" | "onToolCall", run: () => Promise<T>): Promise<T>;
12
- getCheckpointBlocker(): string | undefined;
13
9
  }
14
10
  export declare function createMcpRuntimeOwner(): McpRuntimeOwner;
15
11
  export declare function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined;