@fitchmultz/pi-mcp-adapter 7.1.1 → 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.
@@ -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;
@@ -3,12 +3,10 @@ export function createMcpRuntimeOwner() {
3
3
  const controller = new AbortController();
4
4
  const cleanups = [];
5
5
  let stopPromise;
6
- let checkpoint;
7
- const beforeActivity = () => checkpoint?.invalidate();
8
6
  // These two host hooks can outlive abortable() at their call sites. Count their underlying
9
7
  // promises through settlement, without cancelling, serializing, or replaying callback work.
10
8
  const callbacks = { beforeExecute: 0, onToolCall: 0 };
11
- const getCheckpointBlocker = () => {
9
+ const pendingCallback = () => {
12
10
  if (callbacks.beforeExecute)
13
11
  return "MCP beforeExecute callback is pending";
14
12
  if (callbacks.onToolCall)
@@ -20,10 +18,7 @@ export function createMcpRuntimeOwner() {
20
18
  };
21
19
  return {
22
20
  signal: controller.signal,
23
- beforeActivity,
24
- getCheckpointBlocker,
25
21
  runCallback: async (kind, run) => {
26
- beforeActivity();
27
22
  // Invocation/cancellation policy stays at the call site. In particular, an accepted
28
23
  // tool's outcome callback may still need to persist its result during shutdown.
29
24
  callbacks[kind]++;
@@ -34,13 +29,6 @@ export function createMcpRuntimeOwner() {
34
29
  callbacks[kind]--;
35
30
  }
36
31
  },
37
- holdCheckpoint: event => {
38
- if (checkpoint)
39
- throw new Error("MCP checkpoint is already held");
40
- checkpoint = event;
41
- return () => { if (checkpoint === event)
42
- checkpoint = undefined; };
43
- },
44
32
  isActive: () => !controller.signal.aborted,
45
33
  addCleanup: cleanup => {
46
34
  if (controller.signal.aborted) {
@@ -52,14 +40,13 @@ export function createMcpRuntimeOwner() {
52
40
  stop: (reason = "MCP extension runtime stopped") => {
53
41
  if (stopPromise)
54
42
  return stopPromise;
55
- beforeActivity();
56
43
  controller.abort(new Error(reason));
57
44
  const pendingCleanups = cleanups.splice(0).reverse().map(cleanup => Promise.resolve().then(cleanup));
58
45
  stopPromise = Promise.allSettled(pendingCleanups).then(results => {
59
46
  const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
60
- const pendingCallback = getCheckpointBlocker();
61
- if (pendingCallback)
62
- failures.push(new Error(pendingCallback));
47
+ const callback = pendingCallback();
48
+ if (callback)
49
+ failures.push(new Error(callback));
63
50
  if (failures.length > 0) {
64
51
  const aggregate = new AggregateError(failures, "MCP runtime cleanup failed");
65
52
  console.error(`MCP: runtime cleanup failed: ${formatTerminalError(aggregate)}`);
@@ -68,7 +55,7 @@ export function createMcpRuntimeOwner() {
68
55
  });
69
56
  return stopPromise;
70
57
  },
71
- throwIfInactive: () => { beforeActivity(); controller.signal.throwIfAborted(); },
58
+ throwIfInactive: () => { controller.signal.throwIfAborted(); },
72
59
  };
73
60
  }
74
61
  export function combineAbortSignals(...signals) {
@@ -1,4 +1,3 @@
1
- import type { McpCheckpointEvent } from "./checkpoint.ts";
2
1
  import { Client, type RequestOptions, type GetPromptResult, type ReadResourceResult, type UrlElicitationRequiredError } from "@modelcontextprotocol/client";
3
2
  import { type McpTool, type McpResource, type McpPrompt, type ServerDefinition, type ServerStreamResultPatchNotification, type Transport, type McpTraceSettings } from "./types.ts";
4
3
  import { type McpOAuthRuntime } from "./mcp-auth-flow.ts";
@@ -45,15 +44,9 @@ export declare class McpServerManager {
45
44
  private traceSettings;
46
45
  private traceWriter;
47
46
  private stopped;
48
- private checkpoint;
49
- private readonly beforeActivity;
50
- holdCheckpoint(event: McpCheckpointEvent): () => void;
51
- getCheckpointBlocker(): string | undefined;
52
- flushForCheckpoint(): Promise<void>;
53
47
  private releaseClientWhenSettled;
54
- /** Fence before SDK callbacks AND before trace observers; retain native transport identity.
55
- * Preserve existing property setters (including tracing) rather than replacing their behavior. */
56
- private fenceTransport;
48
+ /** Own native send/auth tails even after a public request has been cancelled. */
49
+ private ownTransportSends;
57
50
  /** Default cwd for stdio servers without an explicit config `cwd`. */
58
51
  private readonly defaultCwd;
59
52
  constructor(defaultCwd?: string);
@@ -41,8 +41,6 @@ async function drainClientOperations(client) {
41
41
  class ManagedClient extends Client {
42
42
  serverName = "";
43
43
  oauthProvider;
44
- beforeActivity = () => { };
45
- resourceSubscriptions = new Set();
46
44
  own(operation, kind) {
47
45
  let pending = clientOperations.get(this);
48
46
  if (!pending)
@@ -53,21 +51,17 @@ class ManagedClient extends Client {
53
51
  return operation;
54
52
  }
55
53
  ownNative(run) {
56
- this.beforeActivity();
57
54
  return this.own(run(), "request/refresh");
58
55
  }
59
56
  ownSend(run) {
60
- this.beforeActivity();
61
57
  // SDK-generated callback replies also perform HTTP/auth work, without calling callTool().
62
58
  return this.own(Promise.resolve().then(run), "request/refresh");
63
59
  }
64
60
  ownCallback(kind, run) {
65
- this.beforeActivity();
66
61
  // Insert before dispatch. Cancellation/close of the SDK response does not settle this promise.
67
62
  return this.own(Promise.resolve().then(run), kind);
68
63
  }
69
64
  async track(start, signal) {
70
- this.beforeActivity();
71
65
  const controller = this.oauthProvider ? new AbortController() : undefined;
72
66
  let removeAbortListener = () => { };
73
67
  const operation = this.oauthProvider
@@ -119,19 +113,11 @@ class ManagedClient extends Client {
119
113
  subscribeResource(...args) {
120
114
  return this.track(async (signal) => {
121
115
  throwIfAborted(signal);
122
- // A cancelled/lost response does not prove the server rejected the subscription.
123
- // Retain the attempted URI until an explicit unsubscribe succeeds or the connection is discarded.
124
- if (this.getServerCapabilities()?.resources?.subscribe)
125
- this.resourceSubscriptions.add(args[0].uri);
126
116
  return super.subscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) });
127
117
  }, args[1]?.signal);
128
118
  }
129
119
  unsubscribeResource(...args) {
130
- return this.track(async (signal) => {
131
- const result = await super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) });
132
- this.resourceSubscriptions.delete(args[0].uri);
133
- return result;
134
- }, args[1]?.signal);
120
+ return this.track(signal => super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
135
121
  }
136
122
  }
137
123
  function boundedStderrChunk(chunk) {
@@ -184,85 +170,15 @@ export class McpServerManager {
184
170
  traceSettings;
185
171
  traceWriter;
186
172
  stopped = false;
187
- checkpoint;
188
- beforeActivity = () => this.checkpoint?.invalidate();
189
- holdCheckpoint(event) {
190
- if (this.checkpoint)
191
- throw new Error("MCP manager checkpoint is already held");
192
- this.checkpoint = event;
193
- return () => { if (this.checkpoint === event)
194
- this.checkpoint = undefined; };
195
- }
196
- getCheckpointBlocker() {
197
- if (this.stopped)
198
- return "MCP manager is stopped";
199
- if (this.connectPromises.size || this.reconnectPromises.size || this.closePromises.size || this.retiredConnections.size)
200
- return "MCP connection/discovery/retirement is active";
201
- if (this.acceptedUrlElicitations.size || this.uiStreamListeners.size)
202
- return "MCP browser elicitation/UI stream is pending";
203
- for (const client of this.ownedClients) {
204
- // Only the SDK's exact auto-opened catalog handle reconstructs during connect().
205
- // An explicit consumer listen(), even with the same filter, still owns private state.
206
- const kinds = [...(clientOperations.get(client) ?? [])]
207
- .filter(([operation, kind]) => kind !== "subscription" || operation !== client.autoOpenedSubscription?.closed)
208
- .map(([, kind]) => kind);
209
- const callback = kinds.find(kind => kind === "sampling callback" || kind === "elicitation callback");
210
- if (callback)
211
- return `MCP ${client.serverName}: ${callback} is active`;
212
- if (kinds.includes("request/refresh"))
213
- return `MCP ${client.serverName}: request/refresh is active`;
214
- if (kinds.includes("subscription"))
215
- return `MCP ${client.serverName}: subscription is active`;
216
- }
217
- for (const [name, connection] of this.connections) {
218
- if (connection.inFlight)
219
- return `MCP ${name}: request/refresh is active`;
220
- if (connection.client instanceof ManagedClient && connection.client.resourceSubscriptions.size)
221
- return `MCP ${name}: resource subscription is active or unresolved`;
222
- if (!(connection.transport instanceof StreamableHTTPClientTransport))
223
- return `MCP ${name}: only stateless Streamable HTTP is checkpoint-supported`;
224
- if (connection.transport.sessionId !== undefined)
225
- return `MCP ${name}: HTTP session state is not reconstructible`;
226
- const capabilities = connection.client.getServerCapabilities();
227
- if (capabilities?.tasks)
228
- return `MCP ${name}: negotiated remote task state is not checkpoint-supported`;
229
- const opaque = Object.keys(capabilities ?? {}).find(key => !["tools", "resources", "prompts", "logging", "completions"].includes(key)
230
- && !(key === "experimental" && Object.keys(capabilities?.experimental ?? {}).length === 0));
231
- if (opaque)
232
- return `MCP ${name}: unsupported negotiated capability ${opaque}`;
233
- }
234
- return undefined;
235
- }
236
- // Diagnostics are best-effort, not session authority or a clean-exit prerequisite.
237
- async flushForCheckpoint() { await this.traceWriter?.flush(); }
238
173
  releaseClientWhenSettled(client) {
239
174
  if (client instanceof ManagedClient)
240
175
  void drainClientOperations(client).then(() => this.ownedClients.delete(client));
241
176
  }
242
- /** Fence before SDK callbacks AND before trace observers; retain native transport identity.
243
- * Preserve existing property setters (including tracing) rather than replacing their behavior. */
244
- fenceTransport(transport, client) {
245
- for (const key of ["onmessage", "onerror", "onclose"]) {
246
- const descriptor = Object.getOwnPropertyDescriptor(transport, key);
247
- let value = transport[key];
248
- Object.defineProperty(transport, key, {
249
- configurable: true,
250
- get: () => {
251
- const handler = descriptor?.get ? descriptor.get.call(transport) : value;
252
- return handler ? (...args) => { this.beforeActivity(); return handler(...args); } : undefined;
253
- },
254
- set: next => { if (descriptor?.set)
255
- descriptor.set.call(transport, next);
256
- else
257
- value = next; },
258
- });
259
- }
260
- if (typeof transport.send === "function") {
177
+ /** Own native send/auth tails even after a public request has been cancelled. */
178
+ ownTransportSends(transport, client) {
179
+ if (typeof transport.send === "function" && client instanceof ManagedClient) {
261
180
  const send = transport.send.bind(transport);
262
- transport.send = (...args) => {
263
- this.beforeActivity();
264
- return client instanceof ManagedClient ? client.ownSend(() => send(...args)) : send(...args);
265
- };
181
+ transport.send = (...args) => client.ownSend(() => send(...args));
266
182
  }
267
183
  }
268
184
  /** Default cwd for stdio servers without an explicit config `cwd`. */
@@ -316,7 +232,6 @@ export class McpServerManager {
316
232
  };
317
233
  }
318
234
  async connect(name, definition, signal) {
319
- this.beforeActivity();
320
235
  if (isServerDisabled(definition))
321
236
  throw new Error(`MCP server "${name}" is disabled`);
322
237
  if (this.stopped)
@@ -425,7 +340,6 @@ export class McpServerManager {
425
340
  }
426
341
  /** Retire only the expected current client; accepted native work drains before close. */
427
342
  retire(name, connection) {
428
- this.beforeActivity();
429
343
  if (this.connections.get(name) !== connection)
430
344
  return;
431
345
  connection.status = "closed";
@@ -644,7 +558,7 @@ export class McpServerManager {
644
558
  }
645
559
  async connectClientWithAbort(client, transport, requestOptions, signal) {
646
560
  throwIfAborted(signal);
647
- this.fenceTransport(transport, client);
561
+ this.ownTransportSends(transport, client);
648
562
  let abortCleanup;
649
563
  const closeTransport = () => {
650
564
  abortCleanup = Promise.resolve().then(() => transport.close());
@@ -714,7 +628,6 @@ export class McpServerManager {
714
628
  });
715
629
  client.serverName = serverName;
716
630
  this.ownedClients.add(client);
717
- client.beforeActivity = this.beforeActivity;
718
631
  if (this.samplingConfig) {
719
632
  registerSamplingHandler(client, { ...this.samplingConfig, serverName }, run => client.ownCallback("sampling callback", run));
720
633
  }
@@ -740,7 +653,6 @@ export class McpServerManager {
740
653
  return client;
741
654
  }
742
655
  handleToolsListChanged(serverName, client, error, tools) {
743
- this.beforeActivity();
744
656
  if (error) {
745
657
  logger.debug(`MCP: tools/list_changed refresh failed for ${serverName}: ${error.message}`);
746
658
  return;
@@ -754,7 +666,6 @@ export class McpServerManager {
754
666
  this.metadataListChangedListener?.(serverName, "tools-list-changed");
755
667
  }
756
668
  handlePromptsListChanged(serverName, client, error, prompts) {
757
- this.beforeActivity();
758
669
  if (error) {
759
670
  logger.debug(`MCP: prompts/list_changed refresh failed for ${serverName}: ${error.message}`);
760
671
  return;
@@ -769,7 +680,6 @@ export class McpServerManager {
769
680
  this.metadataListChangedListener?.(serverName, "prompts-list-changed");
770
681
  }
771
682
  handleResourcesListChanged(serverName, client, error, resources) {
772
- this.beforeActivity();
773
683
  if (error) {
774
684
  logger.debug(`MCP: resources/list_changed refresh failed for ${serverName}: ${error.message}`);
775
685
  return;
@@ -920,7 +830,6 @@ export class McpServerManager {
920
830
  const timer = setTimeout(() => {
921
831
  if (transportClosed)
922
832
  return;
923
- this.beforeActivity();
924
833
  reconnect();
925
834
  }, delay);
926
835
  return () => clearTimeout(timer);
@@ -1037,7 +946,6 @@ export class McpServerManager {
1037
946
  }
1038
947
  }
1039
948
  async close(name) {
1040
- this.beforeActivity();
1041
949
  this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
1042
950
  this.connectAttempts.get(name)?.controller.abort(new Error(`MCP connection ${name} was closed`));
1043
951
  const pendingClose = this.closePromises.get(name);
@@ -1116,7 +1024,7 @@ export class McpServerManager {
1116
1024
  this.elicitationConfig = undefined;
1117
1025
  await Promise.all([...this.ownedClients].map(client => drainClientOperations(client)));
1118
1026
  try {
1119
- await this.flushForCheckpoint();
1027
+ await this.traceWriter?.flush();
1120
1028
  }
1121
1029
  catch (error) {
1122
1030
  failures.push(error);
@@ -4,6 +4,8 @@ import { isServerCacheValid, reconstructToolMetadata } from "./metadata-cache.js
4
4
  import { createMcpDirectToolCallRenderer, renderMcpToolResult } from "./tool-result-renderer.js";
5
5
  import { isServerDisabled } from "./types.js";
6
6
  import { normalizeDirectToolInputSchema } from "./utils.js";
7
+ import { nativeMcpOutputSchema } from "./tool-registrar.js";
8
+ import { toolErrorOverride } from "./error-signal.js";
7
9
  const featureNames = { gateway: "mcp", script: "mcp_script" };
8
10
  const ENTRY = "mcp-tool-selection";
9
11
  const key = (value) => JSON.stringify([value.server, value.tool]);
@@ -27,7 +29,10 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
27
29
  pi.setActiveTools(unique);
28
30
  };
29
31
  function registerFeature(feature, definition) {
30
- pi.registerTool(definition);
32
+ pi.registerTool({ defaultActive: false, ...definition, async execute(...args) {
33
+ const result = await definition.execute(...args);
34
+ return { ...result, ...toolErrorOverride(result.details) };
35
+ } });
31
36
  registeredFeatures.add(feature);
32
37
  }
33
38
  function capture() {
@@ -68,10 +73,25 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
68
73
  inactive = new Map();
69
74
  selectedFeatures = new Set();
70
75
  inactiveFeatures = new Set();
71
- for (const entry of ctx.sessionManager.getBranch()) {
72
- if (entry.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object")
76
+ const manager = ctx.sessionManager;
77
+ function* snapshots() {
78
+ let id = manager.getLeafId();
79
+ while (id) {
80
+ const entry = manager.getEntry(id);
81
+ if (!entry)
82
+ break;
83
+ yield entry;
84
+ id = entry.parentId;
85
+ }
86
+ }
87
+ for (const entry of snapshots()) {
88
+ if (entry?.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object")
73
89
  continue;
74
90
  const data = entry.data;
91
+ if (!Array.isArray(data.selected) || [data.inactive, data.features, data.inactiveFeatures].some(value => value !== undefined && !Array.isArray(value)))
92
+ continue;
93
+ if ([...data.selected, ...(data.inactive ?? [])].some(value => !value || typeof value.server !== "string" || typeof value.tool !== "string"))
94
+ continue;
75
95
  selectedFeatures = new Set(Array.isArray(data.features) ? data.features.filter(value => value === "gateway" || value === "script") : []);
76
96
  inactiveFeatures = new Set(Array.isArray(data.inactiveFeatures) ? data.inactiveFeatures.filter(value => value === "gateway" || value === "script") : []);
77
97
  for (const [field, target] of [[data.selected, selected], [data.inactive, inactive]]) {
@@ -81,6 +101,7 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
81
101
  target.set(key(selection), selection);
82
102
  }
83
103
  }
104
+ break;
84
105
  }
85
106
  restoring = true;
86
107
  }
@@ -114,11 +135,19 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
114
135
  if (reserved.has(name) || counts.get(name) > 1 || (existing.has(name) && !previousNames.has(name)))
115
136
  continue;
116
137
  available.add(id);
117
- const fingerprint = JSON.stringify({ spec, name });
138
+ const instructions = getState()?.serverInstructions.get(spec.serverName) ?? cache?.servers[spec.serverName]?.instructions;
139
+ const fingerprint = JSON.stringify({ spec, name, instructions });
118
140
  if (registered.get(id)?.fingerprint !== fingerprint) {
119
141
  pi.registerTool({
120
142
  name,
121
- namespace: { name: `mcp_${spec.serverName}` },
143
+ namespace: { name: `mcp_${spec.serverName}`, ...(instructions !== undefined ? { instructions } : {}) },
144
+ ...(spec.outputSchema ? { outputSchema: nativeMcpOutputSchema(spec.outputSchema) } : {}),
145
+ ...(spec.annotations ? { annotations: {
146
+ ...(typeof spec.annotations.readOnlyHint === "boolean" ? { readOnlyHint: spec.annotations.readOnlyHint } : {}),
147
+ ...(typeof spec.annotations.destructiveHint === "boolean" ? { destructiveHint: spec.annotations.destructiveHint } : {}),
148
+ ...(typeof spec.annotations.idempotentHint === "boolean" ? { idempotentHint: spec.annotations.idempotentHint } : {}),
149
+ ...(typeof spec.annotations.openWorldHint === "boolean" ? { openWorldHint: spec.annotations.openWorldHint } : {}),
150
+ } } : {}),
122
151
  // Inactive direct tools are not callable through codemode or nested execution.
123
152
  defaultActive: false,
124
153
  ...(beforeExecute ? { executionMode: "sequential" } : {}),
@@ -153,7 +182,7 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
153
182
  }
154
183
  }
155
184
  }
156
- // Official 0.87 reactivates allowlisted definitions on registration. Apply our loadout last.
185
+ // Retain the journal: official 1.0 initial SDK resume has a native loadout restoration gap.
157
186
  const ownedNames = new Set([...previousNames, ...[...registered.values()].map(item => item.name)]);
158
187
  for (const feature of registeredFeatures)
159
188
  ownedNames.add(featureNames[feature]);
@@ -1,5 +1,16 @@
1
1
  import type { McpContent, ContentBlock } from "./types.ts";
2
2
  import { type McpPayloadFile } from "./mcp-output-guard.ts";
3
+ import { Type } from "typebox";
4
+ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
5
+ /** Overflow/not-run outcomes are receipts, never a fabricated server-schema result. */
6
+ export declare function nativeMcpOutputSchema(schema: Record<string, unknown>): Type.TUnion<[Type.TUnsafe<unknown>, Type.TObject<{
7
+ mcpAdapter: Type.TObject<{
8
+ omitted: Type.TLiteral<true>;
9
+ resultRef: Type.TOptional<Type.TString>;
10
+ error: Type.TOptional<Type.TString>;
11
+ }>;
12
+ }>]>;
13
+ export declare function nativeMcpResult(result: AgentToolResult<Record<string, unknown>>, structured?: boolean): AgentToolResult<Record<string, unknown>>;
3
14
  /**
4
15
  * Transform MCP content types to Pi content blocks.
5
16
  */
@@ -1,5 +1,28 @@
1
1
  // MCP protocol content to model-visible content.
2
2
  import { formatMcpPayloadFile } from "./mcp-output-guard.js";
3
+ import { Type } from "typebox";
4
+ import { toolErrorOverride } from "./error-signal.js";
5
+ /** Overflow/not-run outcomes are receipts, never a fabricated server-schema result. */
6
+ export function nativeMcpOutputSchema(schema) {
7
+ return Type.Union([Type.Unsafe(schema), Type.Object({
8
+ mcpAdapter: Type.Object({
9
+ omitted: Type.Literal(true),
10
+ resultRef: Type.Optional(Type.String()),
11
+ error: Type.Optional(Type.String()),
12
+ }),
13
+ })]);
14
+ }
15
+ export function nativeMcpResult(result, structured = false) {
16
+ const { details } = result;
17
+ const raw = details.mcpResult;
18
+ return {
19
+ ...result,
20
+ ...toolErrorOverride(details),
21
+ ...(structured ? { structuredContent: !details.outputGuard && (!details.error || details.error === "tool_error") && raw?.structuredContent !== undefined
22
+ ? raw.structuredContent
23
+ : { mcpAdapter: { omitted: true, ...(typeof details.resultRef === "string" ? { resultRef: details.resultRef } : {}), ...(typeof details.error === "string" ? { error: details.error } : {}) } } } : {}),
24
+ };
25
+ }
3
26
  /**
4
27
  * Transform MCP content types to Pi content blocks.
5
28
  */
package/dist/types.d.ts CHANGED
@@ -136,6 +136,11 @@ export declare function parseUiPromptHandoff(prompt: string): UiPromptHandoff |
136
136
  * Collected during the session and available when it ends.
137
137
  */
138
138
  export interface UiSessionMessages {
139
+ /** Private JSONL history, readable with the normal paged result reader. */
140
+ historyRef?: string;
141
+ historyBytes?: number;
142
+ historyCount?: number;
143
+ historyError?: string;
139
144
  prompts: string[];
140
145
  notifications: string[];
141
146
  intents: Array<{
@@ -0,0 +1,11 @@
1
+ import type { UiSessionMessages } from "./types.ts";
2
+ type Category = "prompts" | "intents" | "notifications";
3
+ type Message = string | UiSessionMessages["intents"][number];
4
+ /** Private append-only history; failed writes stay owned in memory, never silently discarded. */
5
+ export declare function createUiMessageHistory(outputDirectory?: string): {
6
+ messages: UiSessionMessages;
7
+ append(category: Category, value: Message): string | undefined;
8
+ };
9
+ export declare function uiMessages(messages: UiSessionMessages, category: Category): AsyncGenerator<Message>;
10
+ export declare function materializeUiMessages(messages: UiSessionMessages): Promise<UiSessionMessages>;
11
+ export {};