@fitchmultz/pi-mcp-adapter 7.1.0 → 8.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.
@@ -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,17 +10,15 @@ 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";
17
16
  import { MAX_PAGE_SIZE, paginate, rankToolMatches } from "./search-ranking.js";
18
- import { createToolLoader, supportsNativeAsync } from "./tool-loader.js";
17
+ import { createToolLoader } from "./tool-loader.js";
19
18
  import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.js";
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");
@@ -95,7 +93,6 @@ function installMcpAdapter(pi, options) {
95
93
  const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
96
94
  const toolLoader = createToolLoader(pi, () => state, () => initPromise, beforeExecute);
97
95
  let runtimeCwd;
98
- let nativeAsyncSupported = false;
99
96
  let proxyToolRegistered = false;
100
97
  let proxyToolDescription = null;
101
98
  let directToolsFrozen = false;
@@ -112,7 +109,7 @@ function installMcpAdapter(pi, options) {
112
109
  function syncToolSurface(ctx) {
113
110
  const config = state?.config ?? currentConfig;
114
111
  const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
115
- toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => syncGateways(config, cache, specs));
112
+ toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, specs => syncGateways(config, cache, specs));
116
113
  }
117
114
  const registeredPromptCommands = new Set();
118
115
  function registerPromptCommands(specs) {
@@ -202,7 +199,6 @@ function installMcpAdapter(pi, options) {
202
199
  }
203
200
  async function startSession(ctx) {
204
201
  runtimeCwd = ctx.cwd;
205
- nativeAsyncSupported = supportsNativeAsync(ctx);
206
202
  directToolsFrozen = false;
207
203
  const generation = ++lifecycleGeneration;
208
204
  const previousState = state;
@@ -249,7 +245,7 @@ function installMcpAdapter(pi, options) {
249
245
  toolLoader.restore(ctx);
250
246
  registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
251
247
  const runtimeCache = loadMetadataCache(metadataCacheEnabled);
252
- toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => syncGateways(runtimeConfig, runtimeCache, specs));
248
+ toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, specs => syncGateways(runtimeConfig, runtimeCache, specs));
253
249
  const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
254
250
  if (envRaw !== undefined && envRaw !== "__none__") {
255
251
  const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
@@ -264,7 +260,6 @@ function installMcpAdapter(pi, options) {
264
260
  syncToolSurface(ctx);
265
261
  });
266
262
  pi.on("model_select", (_event, ctx) => {
267
- nativeAsyncSupported = supportsNativeAsync(ctx);
268
263
  if (state)
269
264
  syncToolSurface(ctx);
270
265
  });
@@ -272,13 +267,6 @@ function installMcpAdapter(pi, options) {
272
267
  if (ctx.cwd !== runtimeCwd)
273
268
  await startSession(ctx);
274
269
  });
275
- onSessionCheckpoint(pi, async (event) => {
276
- if (initPromise)
277
- return { sleepReady: false, reason: "MCP initialization is active" };
278
- if (!state || !currentOwner?.isActive())
279
- return { sleepReady: false, reason: "MCP runtime is not initialized" };
280
- return prepareMcpCheckpoint(state, event);
281
- });
282
270
  pi.on("session_shutdown", async () => {
283
271
  // Cached tool selections exist before background server initialization finishes.
284
272
  toolLoader.persist();
@@ -303,8 +291,6 @@ function installMcpAdapter(pi, options) {
303
291
  if (failures.length)
304
292
  throw new AggregateError(failures, "MCP session shutdown persistence/cleanup failed");
305
293
  });
306
- // Re-flag returned MCP tool failures so pi registers them as errors (see toolErrorOverride).
307
- pi.on("tool_result", (event) => toolErrorOverride(event.details));
308
294
  function createCommandContext(ctx) {
309
295
  const owner = currentOwner;
310
296
  const hasUI = ctx.hasUI;
@@ -519,7 +505,7 @@ function installMcpAdapter(pi, options) {
519
505
  owner?.throwIfInactive();
520
506
  return state ?? { content: [{ type: "text", text: "MCP not initialized" }], details: { error: "not_initialized" } };
521
507
  }
522
- const unavailableFeatures = (enable = [], tools) => enable.filter(feature => !tools.some(tool => tool.name === (feature === "gateway" ? "mcp" : "mcp_script") && !tool.namespace));
508
+ const unavailableFeatures = (enable = [], tools) => enable.filter(feature => !tools.some(tool => tool.name === (feature === "gateway" ? "mcp" : "mcp_script")));
523
509
  async function searchAndLoad(params, signal, ctx) {
524
510
  throwIfAborted(signal);
525
511
  if (params.query === undefined && !params.enable?.length)
@@ -544,7 +530,7 @@ function installMcpAdapter(pi, options) {
544
530
  const page = paginate(matches, params.offset ?? 0, params.limit ?? 5);
545
531
  const tools = toolLoader.activate(page.items, params.enable);
546
532
  const unavailable = unavailableFeatures(params.enable, tools);
547
- const typedCount = tools.filter(tool => tool.namespace || (tool.name !== "mcp" && tool.name !== "mcp_script")).length;
533
+ const typedCount = tools.filter(tool => tool.name !== "mcp" && tool.name !== "mcp_script").length;
548
534
  const result = executeSearch(ready, params.query, params.server, typedCount < page.items.length, params.limit ?? 5, params.offset, "mcp_search");
549
535
  const guarded = await guardMcpOutput([
550
536
  ...result.content,
@@ -574,10 +560,7 @@ function installMcpAdapter(pi, options) {
574
560
  return searchAndLoad(params, signal, ctx);
575
561
  },
576
562
  };
577
- if (toolLoader.native)
578
- pi.registerToolSearch(searchDefinition);
579
- else
580
- pi.registerTool(searchDefinition);
563
+ pi.registerTool(searchDefinition);
581
564
  let scriptToolRegistered = false;
582
565
  function registerScriptTool() {
583
566
  if (scriptToolRegistered)
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);
@@ -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>;