@fitchmultz/pi-mcp-adapter 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +12 -8
  3. package/dist/app-bridge.bundle.js +14 -14
  4. package/dist/config.js +41 -16
  5. package/dist/gateway-arguments.d.ts +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lifecycle.d.ts +2 -2
  8. package/dist/lifecycle.js +2 -2
  9. package/dist/mcp-auth-flow.js +2 -2
  10. package/dist/mcp-auth.d.ts +1 -1
  11. package/dist/mcp-auth.js +3 -3
  12. package/dist/mcp-code.d.ts +6 -6
  13. package/dist/mcp-code.js +11 -10
  14. package/dist/mcp-oauth-provider.d.ts +5 -5
  15. package/dist/mcp-oauth-provider.js +5 -5
  16. package/dist/mcp-output-guard.d.ts +1 -1
  17. package/dist/mcp-panel.d.ts +2 -2
  18. package/dist/mcp-panel.js +2 -2
  19. package/dist/mcp-setup-panel.d.ts +5 -4
  20. package/dist/mcp-setup-panel.js +15 -7
  21. package/dist/mcp-trace.d.ts +1 -1
  22. package/dist/mcp-trace.js +1 -1
  23. package/dist/npx-resolver.js +8 -4
  24. package/dist/proxy-modes.d.ts +2 -2
  25. package/dist/proxy-modes.js +9 -9
  26. package/dist/sampling-handler.js +1 -0
  27. package/dist/search-ranking.js +6 -2
  28. package/dist/server-manager.d.ts +2 -2
  29. package/dist/server-manager.js +44 -27
  30. package/dist/session-recovery.d.ts +7 -4
  31. package/dist/session-recovery.js +76 -10
  32. package/dist/tool-metadata.d.ts +43 -43
  33. package/dist/tool-result-renderer.d.ts +3 -3
  34. package/dist/tool-result-renderer.js +3 -3
  35. package/dist/ui-resource-handler.d.ts +2 -2
  36. package/dist/ui-resource-handler.js +2 -2
  37. package/dist/ui-server.js +4 -3
  38. package/dist/ui-stream-types.d.ts +8 -8
  39. package/dist/unix-socket-transport.d.ts +1 -1
  40. package/dist/unix-socket-transport.js +1 -1
  41. package/dist/utils.d.ts +1 -0
  42. package/dist/utils.js +14 -0
  43. package/package.json +34 -98
  44. package/abort.ts +0 -34
  45. package/agent-dir.ts +0 -24
  46. package/app-bridge.bundle.js +0 -109
  47. package/banner.png +0 -0
  48. package/checkpoint.ts +0 -55
  49. package/commands.ts +0 -622
  50. package/config.ts +0 -1204
  51. package/consent-manager.ts +0 -25
  52. package/direct-tools.ts +0 -75
  53. package/elicitation-handler.ts +0 -349
  54. package/error-signal.ts +0 -21
  55. package/gateway-arguments.ts +0 -73
  56. package/glimpse-ui.ts +0 -80
  57. package/host-html-template.ts +0 -456
  58. package/index.ts +0 -767
  59. package/init.ts +0 -613
  60. package/legacy-migration.ts +0 -47
  61. package/lifecycle.ts +0 -165
  62. package/logger.ts +0 -127
  63. package/mcp-auth-flow.ts +0 -975
  64. package/mcp-auth.ts +0 -793
  65. package/mcp-callback-server.ts +0 -450
  66. package/mcp-code.ts +0 -412
  67. package/mcp-keyring-helper.cjs +0 -89
  68. package/mcp-oauth-provider.ts +0 -774
  69. package/mcp-output-guard.ts +0 -530
  70. package/mcp-panel.ts +0 -907
  71. package/mcp-script-worker.mjs +0 -131
  72. package/mcp-setup-panel.ts +0 -666
  73. package/mcp-status.ts +0 -110
  74. package/mcp-trace.ts +0 -295
  75. package/metadata-cache.ts +0 -271
  76. package/npx-resolver.ts +0 -488
  77. package/onboarding-state.ts +0 -58
  78. package/panel-keys.ts +0 -37
  79. package/prompts.ts +0 -337
  80. package/proxy-modes.ts +0 -1109
  81. package/resource-tools.ts +0 -52
  82. package/runtime-owner.ts +0 -123
  83. package/sampling-handler.ts +0 -276
  84. package/scripts/build-app-bridge.mjs +0 -15
  85. package/scripts/build.mjs +0 -132
  86. package/scripts/prepare.mjs +0 -103
  87. package/search-ranking.ts +0 -132
  88. package/server-manager.ts +0 -1320
  89. package/session-recovery.ts +0 -316
  90. package/state.ts +0 -64
  91. package/tool-approval.ts +0 -78
  92. package/tool-loader.ts +0 -153
  93. package/tool-metadata.ts +0 -240
  94. package/tool-registrar.ts +0 -74
  95. package/tool-result-renderer.ts +0 -294
  96. package/tsconfig.build.json +0 -13
  97. package/tsconfig.json +0 -16
  98. package/types.ts +0 -785
  99. package/ui-resource-handler.ts +0 -249
  100. package/ui-server.ts +0 -808
  101. package/ui-session.ts +0 -568
  102. package/ui-stream-types.ts +0 -89
  103. package/ui-tool-visibility.ts +0 -25
  104. package/unix-socket-transport.ts +0 -85
  105. package/utils.ts +0 -307
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
2
2
  import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix } from "./types.js";
3
- import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
3
+ import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
4
4
  import { abortable, throwIfAborted } from "./abort.js";
5
5
  import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
6
6
  import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToolDescriptor } from "./tool-metadata.js";
@@ -10,7 +10,7 @@ import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGua
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
- import { isToolTransportFailure, SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
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
16
  const INSTRUCTIONS_PREVIEW_LENGTH = 300;
@@ -387,9 +387,9 @@ export function executeDescribe(state, toolName, serverFilter) {
387
387
  details: { mode: "describe", tool: toolMeta, server: serverName },
388
388
  };
389
389
  }
390
- export function executeSearch(state, query, server, includeSchemas, limit = 12, offset = 0) {
390
+ export function executeSearch(state, query, server, includeSchemas, limit = 12, offset = 0, caller = "mcp") {
391
391
  const showSchemas = includeSchemas !== false;
392
- const searchCall = (nextOffset) => `mcp({ action: "search", query: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${showSchemas ? "" : ", includeSchemas: false"}, limit: ${limit}, offset: ${nextOffset} })`;
392
+ const searchCall = (nextOffset) => `${caller}({ ${caller === "mcp" ? 'action: "search", ' : ""}query: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${caller === "mcp" && !showSchemas ? ", includeSchemas: false" : ""}, limit: ${limit}, offset: ${nextOffset} })`;
393
393
  if (server && isServerDisabled(state.config.mcpServers[server]))
394
394
  return disabledResult("search", server);
395
395
  let matches;
@@ -670,7 +670,7 @@ export async function runToolCall(state, serverName, target, args, options) {
670
670
  state.manager.touch(serverName);
671
671
  state.manager.incrementInFlight(serverName);
672
672
  uiSession = !target.resourceUri && target.uiResourceUri
673
- ? await maybeStartUiSession(state, {
673
+ ? await abortable(maybeStartUiSession(state, {
674
674
  serverName,
675
675
  toolName: target.originalName,
676
676
  toolArgs: args ?? {},
@@ -678,7 +678,7 @@ export async function runToolCall(state, serverName, target, args, options) {
678
678
  ...(target.uiStreamMode !== undefined ? { streamMode: target.uiStreamMode } : {}),
679
679
  ...(signal ? { signal } : {}),
680
680
  onNeedsAuth: recoverAuthConnection,
681
- })
681
+ }), callerSignal)
682
682
  : null;
683
683
  // UI preparation can replace the connection; use its current annotations.
684
684
  const liveTools = state.manager.getConnection(serverName)?.tools;
@@ -720,7 +720,7 @@ export async function runToolCall(state, serverName, target, args, options) {
720
720
  };
721
721
  const result = target.resourceUri
722
722
  ? await dispatch(() => withSessionRecovery(recovery, serverName, conn => abortable(conn.client.readResource({ uri: target.resourceUri }, requestOptions), ownedSignal)))
723
- : await dispatch(() => withSessionRecovery({ ...recovery, retryOnTransportFailure: annotations?.readOnlyHint === true || annotations?.idempotentHint === true }, serverName, conn => abortable(conn.client.callTool({ name: target.originalName, arguments: args ?? {}, _meta: uiSession?.requestMeta }, requestOptions), ownedSignal)));
723
+ : await dispatch(() => trackToolCallOutcome(() => withSessionRecovery({ ...recovery, retryOnTransportFailure: annotations?.readOnlyHint === true || annotations?.idempotentHint === true }, serverName, conn => abortable(conn.client.callTool({ name: target.originalName, arguments: args ?? {}, _meta: uiSession?.requestMeta }, requestOptions), ownedSignal)), ownedSignal));
724
724
  if (!target.resourceUri)
725
725
  uiSession?.sendToolResult(result);
726
726
  const record = result;
@@ -762,8 +762,8 @@ export async function runToolCall(state, serverName, target, args, options) {
762
762
  details: { ...detailsBase, ...retained, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
763
763
  };
764
764
  }
765
- if (isToolTransportFailure(error)) {
766
- const message = `The outcome of MCP tool "${target.originalName}" on "${serverName}" is unknown after transport loss. Read back the original operation using its saved arguments and provider identity before continuing. Do not blindly repeat the call or rerun its script.`;
765
+ if (isToolTransportFailure(error) || isInterruptedToolCall(error)) {
766
+ const message = `The outcome of MCP tool "${target.originalName}" on "${serverName}" is unknown after the call was interrupted. Read back the original operation using its saved arguments and provider identity before continuing. Do not blindly repeat the call or rerun its script.`;
767
767
  uiSession?.sendToolCancelled(message);
768
768
  return {
769
769
  content: [{ type: "text", text: message }],
@@ -1,5 +1,6 @@
1
1
  import { truncateAtWord } from "./utils.js";
2
2
  import { throwIfAborted } from "./abort.js";
3
+ import {} from "@modelcontextprotocol/client";
3
4
  export function registerSamplingHandler(client, options, own = run => run()) {
4
5
  // Own the handler's real promise, not the SDK caller's cancellable response facade.
5
6
  client.setRequestHandler("sampling/createMessage", request => own(() => handleSamplingRequest(options, request)));
@@ -1,4 +1,4 @@
1
- import MiniSearch from "minisearch";
1
+ import MiniSearch, {} from "minisearch";
2
2
  import { getServerPrefix, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
3
3
  export const MAX_PAGE_SIZE = 100;
4
4
  export const MAX_TOOL_NAME_LENGTH = 512;
@@ -69,7 +69,11 @@ function searchCatalog(state, query, server, options) {
69
69
  .sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
70
70
  }
71
71
  export function rankToolMatches(state, query, server) {
72
- return searchCatalog(state, query, server);
72
+ const matches = searchCatalog(state, query, server);
73
+ const identity = query.trim();
74
+ const exact = ({ tool }) => tool.name === identity || (Boolean(server) && tool.originalName === identity);
75
+ // Stable partition retains MiniSearch order and scores within both groups.
76
+ return [...matches.filter(exact), ...matches.filter(match => !exact(match))];
73
77
  }
74
78
  export function paginate(items, offset, limit) {
75
79
  const safeOffset = Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0;
@@ -24,7 +24,6 @@ export interface ServerConnection {
24
24
  type UiStreamListener = (serverName: string, notification: ServerStreamResultPatchNotification["params"]) => void;
25
25
  type MetadataListChangedListener = (serverName: string, reason: string) => void;
26
26
  export declare class McpServerManager {
27
- private readonly defaultCwd?;
28
27
  private ownedClients;
29
28
  private connections;
30
29
  private connectPromises;
@@ -56,7 +55,8 @@ export declare class McpServerManager {
56
55
  * Preserve existing property setters (including tracing) rather than replacing their behavior. */
57
56
  private fenceTransport;
58
57
  /** Default cwd for stdio servers without an explicit config `cwd`. */
59
- constructor(defaultCwd?: string | undefined);
58
+ private readonly defaultCwd;
59
+ constructor(defaultCwd?: string);
60
60
  setSamplingConfig(config: ServerSamplingConfig | undefined): void;
61
61
  setMetadataListChangedListener(listener: MetadataListChangedListener | undefined): void;
62
62
  setElicitationConfig(config: ServerElicitationConfig | undefined): void;
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { Client, StreamableHTTPClientTransport, SSEClientTransport, SdkHttpError, auth, extractWWWAuthenticateParams, UnauthorizedError, } from "@modelcontextprotocol/client";
3
3
  import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
4
4
  import { UnixSocketClientTransport } from "./unix-socket-transport.js";
5
- import { trackToolHttpFailures, trackToolTransportFailure } from "./session-recovery.js";
5
+ import { trackToolTransport, trackToolTransportFailure } from "./session-recovery.js";
6
6
  import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, serverStreamResultPatchNotificationSchema, } from "./types.js";
7
7
  import { resolveNpxBinary } from "./npx-resolver.js";
8
8
  import { logger } from "./logger.js";
@@ -11,10 +11,10 @@ import { extractOAuthConfig, supportsOAuth, isOAuthChallenge, recordOAuthChallen
11
11
  import { getAuthBaseDir } from "./mcp-auth.js";
12
12
  import { registerSamplingHandler } from "./sampling-handler.js";
13
13
  import { handleUrlElicitation, registerElicitationHandler, } from "./elicitation-handler.js";
14
- import { normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
14
+ import { ADAPTER_VERSION, normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
15
15
  import { abortable, throwIfAborted } from "./abort.js";
16
16
  import { combineAbortSignals } from "./runtime-owner.js";
17
- import { createMcpTraceWriter, isMcpTraceEnabled, traceTransportKind, wrapTransportWithMcpTrace, } from "./mcp-trace.js";
17
+ import { createMcpTraceWriter, isMcpTraceEnabled, McpTraceWriter, traceTransportKind, wrapTransportWithMcpTrace, } from "./mcp-trace.js";
18
18
  const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
19
19
  const MAX_CAPTURED_STDERR_LINES = 3;
20
20
  const abortCleanupPromises = new WeakMap();
@@ -161,7 +161,6 @@ function appendStderrTail(tail, chunk) {
161
161
  : combined;
162
162
  }
163
163
  export class McpServerManager {
164
- defaultCwd;
165
164
  // Includes native clients whose public connect/close facade ended before their send/auth tail.
166
165
  // Their operation promises remain in the same clientOperations authority until settlement.
167
166
  ownedClients = new Set();
@@ -267,6 +266,7 @@ export class McpServerManager {
267
266
  }
268
267
  }
269
268
  /** Default cwd for stdio servers without an explicit config `cwd`. */
269
+ defaultCwd;
270
270
  constructor(defaultCwd) {
271
271
  this.defaultCwd = defaultCwd;
272
272
  }
@@ -329,9 +329,30 @@ export class McpServerManager {
329
329
  throwIfAborted(ownedSignal);
330
330
  if (this.stopped)
331
331
  throw new Error("MCP server manager is closed");
332
+ const wait = (promise, attempt) => {
333
+ attempt.waiters++;
334
+ return abortable(promise, ownedSignal).finally(() => {
335
+ if (--attempt.waiters === 0 && ownedSignal?.aborted)
336
+ attempt.controller.abort(ownedSignal.reason);
337
+ });
338
+ };
332
339
  // Dedupe concurrent connection attempts.
333
- if (this.connectPromises.has(name)) {
334
- return abortable(this.connectPromises.get(name), ownedSignal);
340
+ const pending = this.connectPromises.get(name);
341
+ if (pending) {
342
+ const attempt = this.connectAttempts.get(name);
343
+ if (attempt.controller.signal.aborted) {
344
+ // Finish the canceled attempt's cleanup before starting a fresh one.
345
+ try {
346
+ await abortable(pending, ownedSignal);
347
+ }
348
+ catch (error) {
349
+ throwIfAborted(ownedSignal);
350
+ if (this.containsCleanupFailure(error))
351
+ throw error;
352
+ }
353
+ return this.connect(name, definition, signal);
354
+ }
355
+ return wait(pending, attempt);
335
356
  }
336
357
  const existing = this.connections.get(name);
337
358
  if (existing?.status === "connected") {
@@ -339,10 +360,10 @@ export class McpServerManager {
339
360
  return existing;
340
361
  }
341
362
  const generation = this.closeGenerations.get(name) ?? 0;
342
- const attemptController = new AbortController();
343
- const attemptSignal = combineAbortSignals(ownedSignal, attemptController.signal);
344
- const promise = this.createConnection(name, definition, attemptSignal, ownedSignal).then(async (connection) => {
345
- if (attemptController.signal.aborted || (this.closeGenerations.get(name) ?? 0) !== generation) {
363
+ const attempt = { controller: new AbortController(), waiters: 0 };
364
+ const attemptSignal = combineAbortSignals(this.runtimeSignal, attempt.controller.signal);
365
+ const promise = this.createConnection(name, definition, attemptSignal, ownedSignal ? attemptSignal : undefined).then(async (connection) => {
366
+ if (connection.status === "closed" || attempt.controller.signal.aborted || (this.closeGenerations.get(name) ?? 0) !== generation) {
346
367
  await this.disposeConnection(connection);
347
368
  throwIfAborted(attemptSignal);
348
369
  throw new Error(`MCP connection for ${name} was closed while connecting`);
@@ -350,18 +371,15 @@ export class McpServerManager {
350
371
  connection.inFlight = this.connections.get(name)?.inFlight ?? 0;
351
372
  this.connections.set(name, connection);
352
373
  return connection;
353
- });
354
- this.connectPromises.set(name, promise);
355
- this.connectAttempts.set(name, attemptController);
356
- try {
357
- return await promise;
358
- }
359
- finally {
374
+ }).finally(() => {
360
375
  if (this.connectPromises.get(name) === promise)
361
376
  this.connectPromises.delete(name);
362
- if (this.connectAttempts.get(name) === attemptController)
377
+ if (this.connectAttempts.get(name) === attempt)
363
378
  this.connectAttempts.delete(name);
364
- }
379
+ });
380
+ this.connectPromises.set(name, promise);
381
+ this.connectAttempts.set(name, attempt);
382
+ return wait(promise, attempt);
365
383
  }
366
384
  /**
367
385
  * Reconnect a server whose connection was proven stale (e.g. by a 404
@@ -493,6 +511,8 @@ export class McpServerManager {
493
511
  else {
494
512
  transport = new UnixSocketClientTransport(resolveConfigPath(definition.socket));
495
513
  }
514
+ if (!definition.url)
515
+ trackToolTransport(transport);
496
516
  if (traceObserver) {
497
517
  const traceTransportKindValue = traceTransportKind(definition, transport);
498
518
  transport = wrapTransportWithMcpTrace(transport, name, traceTransportKindValue, traceObserver);
@@ -536,11 +556,9 @@ export class McpServerManager {
536
556
  status: "connected",
537
557
  oauthProvider,
538
558
  };
539
- // The public client hook preserves native transport cleanup. An old
540
- // client's late close must never change its replacement's status.
559
+ // Track closure during discovery too; this only changes this client's connection.
541
560
  client.onclose = () => {
542
- if (this.connections.get(name) === connection)
543
- connection.status = "closed";
561
+ connection.status = "closed";
544
562
  };
545
563
  const discoveryOptions = this.buildRequestOptions(definition, signal);
546
564
  const catalogs = [
@@ -671,7 +689,7 @@ export class McpServerManager {
671
689
  createClient(serverName, definition) {
672
690
  const capabilities = this.buildClientCapabilities(definition);
673
691
  let client;
674
- client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: "4.3.1" }, {
692
+ client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
675
693
  versionNegotiation: { mode: definition.protocolVersion ?? (definition.url ? "auto" : "legacy") },
676
694
  listMaxPages: 0,
677
695
  ...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
@@ -936,8 +954,7 @@ export class McpServerManager {
936
954
  onAuthChallenge?.(error);
937
955
  };
938
956
  }
939
- if (!legacySse)
940
- trackToolHttpFailures(transport);
957
+ trackToolTransport(transport, true);
941
958
  return { transport, oauthProvider: authProvider !== undefined };
942
959
  }
943
960
  async fetchAllTools(client, requestOptions) {
@@ -1021,7 +1038,7 @@ export class McpServerManager {
1021
1038
  async close(name) {
1022
1039
  this.beforeActivity();
1023
1040
  this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
1024
- this.connectAttempts.get(name)?.abort(new Error(`MCP connection ${name} was closed`));
1041
+ this.connectAttempts.get(name)?.controller.abort(new Error(`MCP connection ${name} was closed`));
1025
1042
  const pendingClose = this.closePromises.get(name);
1026
1043
  if (pendingClose)
1027
1044
  return pendingClose;
@@ -2,15 +2,18 @@ import { type FetchLike, type Transport } from "@modelcontextprotocol/client";
2
2
  import { type McpConfig } from "./types.ts";
3
3
  import type { McpServerManager, ServerConnection } from "./server-manager.ts";
4
4
  export declare function isToolTransportFailure(error: unknown): boolean;
5
- /** Retain fetch/body-error origin without parsing or buffering the SSE response. */
5
+ export declare function isInterruptedToolCall(error: unknown): boolean;
6
+ /** Interrupted outcome reporting is separate from the transport retry policy. */
7
+ export declare function trackToolCallOutcome<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T>;
8
+ /** Retain fetch/body-error origin without parsing or buffering the response. */
6
9
  export declare const trackToolTransportFailure: FetchLike;
7
10
  /** Observe only the originating tool send, not OAuth or catalog requests. */
8
- export declare function trackToolHttpFailures(transport: Transport): void;
11
+ export declare function trackToolTransport(transport: Transport, http?: boolean): void;
9
12
  export declare function isTerminatedSession(err: unknown, hadSessionId: boolean): boolean;
10
13
  export declare class SessionRecoveryAuthRequiredError extends Error {
11
14
  readonly serverName: string;
12
- readonly authMessage?: string | undefined;
13
- constructor(serverName: string, authMessage?: string | undefined);
15
+ readonly authMessage: string | undefined;
16
+ constructor(serverName: string, authMessage?: string);
14
17
  }
15
18
  export interface AuthChallengeContext {
16
19
  connection: ServerConnection;
@@ -21,7 +21,7 @@
21
21
  // - treat generic -32000/ConnectionClosed errors as session expiry
22
22
  // - treat AbortError/cancellation as a session failure
23
23
  import { AsyncLocalStorage } from "node:async_hooks";
24
- import { SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
24
+ import { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
25
25
  import { logger } from "./logger.js";
26
26
  import { abortable, throwIfAborted } from "./abort.js";
27
27
  import { isServerDisabled, isNonInteractiveOAuth } from "./types.js";
@@ -30,18 +30,62 @@ const toolTransportFailures = new WeakSet();
30
30
  export function isToolTransportFailure(error) {
31
31
  return error instanceof Error && toolTransportFailures.has(error);
32
32
  }
33
+ const interruptedToolCalls = new WeakSet();
34
+ export function isInterruptedToolCall(error) {
35
+ return error instanceof Error && interruptedToolCalls.has(error);
36
+ }
37
+ const toolCallOutcome = new AsyncLocalStorage();
38
+ /** Interrupted outcome reporting is separate from the transport retry policy. */
39
+ export async function trackToolCallOutcome(fn, signal) {
40
+ const outcome = { pending: false, cleanup: new Set() };
41
+ return toolCallOutcome.run(outcome, async () => {
42
+ try {
43
+ return await fn();
44
+ }
45
+ catch (error) {
46
+ if (outcome.pending && error instanceof Error
47
+ && (signal?.aborted || (error instanceof SdkError
48
+ && (error.code === SdkErrorCode.RequestTimeout || error.code === SdkErrorCode.ConnectionClosed)))) {
49
+ // Abort reasons can be shared by calls that never reached dispatch.
50
+ const interrupted = new Error(error.message, { cause: error });
51
+ interruptedToolCalls.add(interrupted);
52
+ throw interrupted;
53
+ }
54
+ throw error;
55
+ }
56
+ finally {
57
+ for (const cleanup of outcome.cleanup)
58
+ cleanup();
59
+ }
60
+ });
61
+ }
33
62
  const toolSend = new AsyncLocalStorage();
34
- /** Retain fetch/body-error origin without parsing or buffering the SSE response. */
63
+ /** Retain fetch/body-error origin without parsing or buffering the response. */
35
64
  export const trackToolTransportFailure = async (input, init) => {
36
65
  const pending = toolSend.getStore();
37
66
  if (pending)
38
67
  pending.bodyFailure = undefined;
68
+ let toolPost = false;
69
+ if (pending && init?.method === "POST" && typeof init.body === "string") {
70
+ try {
71
+ const request = JSON.parse(init.body);
72
+ toolPost = request.method === "tools/call" && request.id === pending.id;
73
+ }
74
+ catch { }
75
+ }
76
+ if (toolPost && pending?.outcome)
77
+ pending.outcome.pending = true;
39
78
  try {
40
79
  const response = await fetch(input, init);
41
- if (!pending || !response.ok || !response.body
42
- || response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "text/event-stream")
80
+ if (toolPost && !response.ok && response.status < 500 && pending?.outcome)
81
+ pending.outcome.pending = false;
82
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
83
+ const observeBody = response.ok
84
+ ? mediaType === "text/event-stream" || (toolPost && mediaType === "application/json")
85
+ : toolPost && response.status >= 500;
86
+ if (!pending || !response.body || !observeBody)
43
87
  return response;
44
- pending.streaming = true;
88
+ pending.streaming = response.ok && mediaType === "text/event-stream";
45
89
  // The native end hook has no error argument. Only a failed body read proves loss.
46
90
  const reader = response.body.getReader();
47
91
  return new Response(new ReadableStream({
@@ -59,8 +103,11 @@ export const trackToolTransportFailure = async (input, init) => {
59
103
  // Native intentional closure suppresses onRequestStreamEnd.
60
104
  if (init?.signal?.aborted)
61
105
  pending.finishSend();
62
- else if (error instanceof Error)
106
+ else if (error instanceof Error) {
63
107
  pending.bodyFailure = error;
108
+ if (!pending.streaming)
109
+ interruptedToolCalls.add(error);
110
+ }
64
111
  controller.error(error);
65
112
  reader.releaseLock();
66
113
  }
@@ -77,12 +124,16 @@ export const trackToolTransportFailure = async (input, init) => {
77
124
  }
78
125
  };
79
126
  /** Observe only the originating tool send, not OAuth or catalog requests. */
80
- export function trackToolHttpFailures(transport) {
127
+ export function trackToolTransport(transport, http = false) {
128
+ const requests = new Map();
81
129
  const onmessage = transport.onmessage;
82
130
  transport.onmessage = (message, extra) => {
83
- const pending = toolSend.getStore();
131
+ const pending = ("id" in message && message.id !== undefined ? requests.get(message.id) : undefined) ?? toolSend.getStore();
84
132
  if (pending && "id" in message && message.id === pending.id && ("result" in message || "error" in message)) {
85
133
  pending.responseReceived = true;
134
+ if (pending.outcome)
135
+ pending.outcome.pending = false;
136
+ requests.delete(pending.id);
86
137
  pending.finishSend();
87
138
  }
88
139
  onmessage?.(message, extra);
@@ -98,15 +149,24 @@ export function trackToolHttpFailures(transport) {
98
149
  const send = transport.send.bind(transport);
99
150
  transport.send = async (message, options) => {
100
151
  try {
101
- if (!("method" in message && message.method === "tools/call" && "id" in message && options?.requestSignal)) {
152
+ if (!("method" in message && message.method === "tools/call" && "id" in message)) {
102
153
  return await toolSend.run(undefined, () => send(message, options));
103
154
  }
104
155
  const pending = {
105
- id: message.id, streaming: false, responseReceived: false,
156
+ id: message.id, outcome: toolCallOutcome.getStore(), streaming: false, responseReceived: false,
106
157
  bodyFailure: undefined, otherFailure: undefined,
107
158
  finishSend: () => { },
108
159
  };
160
+ if (pending.outcome) {
161
+ requests.set(message.id, pending);
162
+ pending.outcome.cleanup.add(() => requests.delete(message.id));
163
+ if (!http)
164
+ pending.outcome.pending = true;
165
+ }
109
166
  try {
167
+ if (!options?.requestSignal) {
168
+ return await toolSend.run(pending, () => send(message, options));
169
+ }
110
170
  await abortable(new Promise((resolve, reject) => {
111
171
  pending.finishSend = resolve;
112
172
  void toolSend.run(pending, () => send(message, {
@@ -126,6 +186,12 @@ export function trackToolHttpFailures(transport) {
126
186
  resolve(); }, reject);
127
187
  }), options.requestSignal);
128
188
  }
189
+ catch (error) {
190
+ // Local send failures are distinct from a sent request waiting for its reply.
191
+ if (!http && pending.outcome)
192
+ pending.outcome.pending = false;
193
+ throw error;
194
+ }
129
195
  finally {
130
196
  pending.finishSend = () => { };
131
197
  }
@@ -6,10 +6,20 @@ export declare function buildToolMetadata(tools: McpTool[], resources: McpResour
6
6
  };
7
7
  export declare function totalToolCount(state: McpExtensionState): number;
8
8
  export declare function toToolDescriptor(server: string, tool: ToolMetadata): {
9
- server: string;
10
- path: string;
11
- name: string;
12
- description: string;
9
+ outputSchema?: {
10
+ [x: string]: unknown;
11
+ $schema?: string | undefined;
12
+ } | undefined;
13
+ annotations?: {
14
+ title?: string | undefined;
15
+ readOnlyHint?: boolean | undefined;
16
+ destructiveHint?: boolean | undefined;
17
+ idempotentHint?: boolean | undefined;
18
+ openWorldHint?: boolean | undefined;
19
+ } | undefined;
20
+ execution?: {
21
+ taskSupport?: "forbidden" | "optional" | "required" | undefined;
22
+ } | undefined;
13
23
  _meta?: {
14
24
  [x: string]: unknown;
15
25
  } | undefined;
@@ -17,55 +27,45 @@ export declare function toToolDescriptor(server: string, tool: ToolMetadata): {
17
27
  src: string;
18
28
  mimeType?: string | undefined;
19
29
  sizes?: string[] | undefined;
20
- theme?: "light" | "dark" | undefined;
30
+ theme?: "dark" | "light" | undefined;
21
31
  }[] | undefined;
22
32
  title?: string | undefined;
23
33
  inputSchema?: {
24
34
  [x: string]: unknown;
25
35
  type: "object";
26
36
  properties?: {
27
- [x: string]: string | number | boolean | {
28
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
29
- } | (string | number | boolean | {
30
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
31
- } | (string | number | boolean | {
32
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
33
- } | (string | number | boolean | {
34
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
35
- } | (string | number | boolean | {
36
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
37
- } | (string | number | boolean | {
38
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
39
- } | (string | number | boolean | {
40
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null;
41
- } | (string | number | boolean | {
42
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null;
43
- } | (string | number | boolean | {
44
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null;
45
- } | (string | number | boolean | {
46
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null;
47
- } | (string | number | boolean | {
48
- [x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null;
49
- } | (string | number | boolean | {
37
+ [x: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | {
50
38
  [x: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
51
- } | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
39
+ } | null)[] | {
40
+ [x: string]: string | number | boolean | any | any | null;
41
+ } | null)[] | {
42
+ [x: string]: string | number | boolean | any | any | null;
43
+ } | null)[] | {
44
+ [x: string]: string | number | boolean | any | any | null;
45
+ } | null)[] | {
46
+ [x: string]: string | number | boolean | any | any | null;
47
+ } | null)[] | {
48
+ [x: string]: string | number | boolean | any | any | null;
49
+ } | null)[] | {
50
+ [x: string]: string | number | boolean | any | any | null;
51
+ } | null)[] | {
52
+ [x: string]: string | number | boolean | any | any | null;
53
+ } | null)[] | {
54
+ [x: string]: string | number | boolean | any | any | null;
55
+ } | null)[] | {
56
+ [x: string]: string | number | boolean | any | any | null;
57
+ } | null)[] | {
58
+ [x: string]: string | number | boolean | any | any | null;
59
+ } | null)[] | {
60
+ [x: string]: string | number | boolean | any | any | null;
61
+ } | null;
52
62
  } | undefined;
53
63
  required?: string[] | undefined;
54
64
  };
55
- outputSchema?: {
56
- [x: string]: unknown;
57
- $schema?: string | undefined;
58
- } | undefined;
59
- annotations?: {
60
- title?: string | undefined;
61
- readOnlyHint?: boolean | undefined;
62
- destructiveHint?: boolean | undefined;
63
- idempotentHint?: boolean | undefined;
64
- openWorldHint?: boolean | undefined;
65
- } | undefined;
66
- execution?: {
67
- taskSupport?: "optional" | "required" | "forbidden" | undefined;
68
- } | undefined;
65
+ description: string;
66
+ server: string;
67
+ path: string;
68
+ name: string;
69
69
  };
70
70
  export declare function catalogCoverage(state: McpExtensionState, server?: string): {
71
71
  complete: boolean;
@@ -25,15 +25,15 @@ export interface McpToolResultDisplay {
25
25
  truncated: boolean;
26
26
  }
27
27
  declare class CollapsibleText implements Component {
28
+ private readonly fullText;
29
+ private readonly footerText;
30
+ private collapsedText;
28
31
  private readonly text;
29
32
  private readonly expanded;
30
33
  private readonly maxCollapsedLines;
31
34
  private readonly ellipsis;
32
35
  private readonly expandHint;
33
36
  private readonly preTruncated;
34
- private readonly fullText;
35
- private readonly footerText;
36
- private collapsedText;
37
37
  constructor(text: string, expanded: boolean, maxCollapsedLines: number, ellipsis: string, expandHint: string, preTruncated?: boolean);
38
38
  render(width: number): string[];
39
39
  invalidate(): void;
@@ -9,15 +9,15 @@ const DEFAULT_MAX_COLLAPSED_LINES = 3;
9
9
  const DEFAULT_MAX_COLLAPSED_CHARS = 8000;
10
10
  const COLLAPSED_RENDER_CHAR_SLACK = 8;
11
11
  class CollapsibleText {
12
+ fullText;
13
+ footerText;
14
+ collapsedText = null;
12
15
  text;
13
16
  expanded;
14
17
  maxCollapsedLines;
15
18
  ellipsis;
16
19
  expandHint;
17
20
  preTruncated;
18
- fullText;
19
- footerText;
20
- collapsedText = null;
21
21
  constructor(text, expanded, maxCollapsedLines, ellipsis, expandHint, preTruncated = false) {
22
22
  this.text = text;
23
23
  this.expanded = expanded;
@@ -7,10 +7,10 @@ interface ReadUiResourceOptions {
7
7
  onNeedsAuth?: SessionRecoveryDeps["onNeedsAuth"];
8
8
  }
9
9
  export declare class UiResourceHandler {
10
+ private log;
10
11
  private manager;
11
12
  private config;
12
- private log;
13
- constructor(manager: McpServerManager, config?: McpConfig | undefined);
13
+ constructor(manager: McpServerManager, config?: McpConfig);
14
14
  readUiResource(serverName: string, uri: string, options?: ReadUiResourceOptions): Promise<UiResourceContent>;
15
15
  private getListResourceMeta;
16
16
  }
@@ -4,10 +4,10 @@ import { logger } from "./logger.js";
4
4
  import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
5
5
  import { isServerDisabled } from "./types.js";
6
6
  export class UiResourceHandler {
7
+ log = logger.child({ component: "UiResourceHandler" });
7
8
  manager;
8
9
  config;
9
- log = logger.child({ component: "UiResourceHandler" });
10
- constructor(manager, config = undefined) {
10
+ constructor(manager, config) {
11
11
  this.manager = manager;
12
12
  this.config = config;
13
13
  }