@fitchmultz/pi-mcp-adapter 6.0.0 → 7.1.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 (111) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/OAUTH.md +1 -1
  3. package/README.md +27 -18
  4. package/dist/app-bridge.bundle.js +14 -14
  5. package/dist/config.js +41 -16
  6. package/dist/direct-tools.js +1 -1
  7. package/dist/gateway-arguments.d.ts +1 -1
  8. package/dist/index.js +38 -59
  9. package/dist/lifecycle.d.ts +2 -2
  10. package/dist/lifecycle.js +2 -2
  11. package/dist/mcp-auth-flow.js +2 -2
  12. package/dist/mcp-auth.d.ts +1 -1
  13. package/dist/mcp-auth.js +3 -3
  14. package/dist/mcp-code.d.ts +6 -6
  15. package/dist/mcp-code.js +11 -10
  16. package/dist/mcp-oauth-provider.d.ts +5 -5
  17. package/dist/mcp-oauth-provider.js +5 -5
  18. package/dist/mcp-output-guard.d.ts +1 -1
  19. package/dist/mcp-output-guard.js +1 -1
  20. package/dist/mcp-panel.d.ts +2 -2
  21. package/dist/mcp-panel.js +2 -2
  22. package/dist/mcp-setup-panel.d.ts +5 -4
  23. package/dist/mcp-setup-panel.js +15 -7
  24. package/dist/mcp-trace.d.ts +1 -1
  25. package/dist/mcp-trace.js +1 -1
  26. package/dist/npx-resolver.js +8 -4
  27. package/dist/proxy-modes.d.ts +2 -2
  28. package/dist/proxy-modes.js +20 -19
  29. package/dist/sampling-handler.js +1 -0
  30. package/dist/search-ranking.js +6 -2
  31. package/dist/server-manager.d.ts +2 -2
  32. package/dist/server-manager.js +44 -27
  33. package/dist/session-recovery.d.ts +7 -4
  34. package/dist/session-recovery.js +76 -10
  35. package/dist/tool-loader.d.ts +7 -2
  36. package/dist/tool-loader.js +56 -5
  37. package/dist/tool-metadata.d.ts +43 -43
  38. package/dist/tool-result-renderer.d.ts +3 -3
  39. package/dist/tool-result-renderer.js +3 -3
  40. package/dist/ui-resource-handler.d.ts +2 -2
  41. package/dist/ui-resource-handler.js +2 -2
  42. package/dist/ui-server.js +5 -4
  43. package/dist/ui-stream-types.d.ts +8 -8
  44. package/dist/unix-socket-transport.d.ts +1 -1
  45. package/dist/unix-socket-transport.js +1 -1
  46. package/dist/utils.d.ts +1 -0
  47. package/dist/utils.js +14 -0
  48. package/package.json +34 -98
  49. package/skills/mcp-scripting/SKILL.md +2 -0
  50. package/abort.ts +0 -34
  51. package/agent-dir.ts +0 -24
  52. package/app-bridge.bundle.js +0 -109
  53. package/banner.png +0 -0
  54. package/checkpoint.ts +0 -55
  55. package/commands.ts +0 -622
  56. package/config.ts +0 -1204
  57. package/consent-manager.ts +0 -25
  58. package/direct-tools.ts +0 -75
  59. package/elicitation-handler.ts +0 -349
  60. package/error-signal.ts +0 -21
  61. package/gateway-arguments.ts +0 -73
  62. package/glimpse-ui.ts +0 -80
  63. package/host-html-template.ts +0 -456
  64. package/index.ts +0 -767
  65. package/init.ts +0 -613
  66. package/legacy-migration.ts +0 -47
  67. package/lifecycle.ts +0 -165
  68. package/logger.ts +0 -127
  69. package/mcp-auth-flow.ts +0 -975
  70. package/mcp-auth.ts +0 -793
  71. package/mcp-callback-server.ts +0 -450
  72. package/mcp-code.ts +0 -412
  73. package/mcp-keyring-helper.cjs +0 -89
  74. package/mcp-oauth-provider.ts +0 -774
  75. package/mcp-output-guard.ts +0 -530
  76. package/mcp-panel.ts +0 -907
  77. package/mcp-script-worker.mjs +0 -131
  78. package/mcp-setup-panel.ts +0 -666
  79. package/mcp-status.ts +0 -110
  80. package/mcp-trace.ts +0 -295
  81. package/metadata-cache.ts +0 -271
  82. package/npx-resolver.ts +0 -488
  83. package/onboarding-state.ts +0 -58
  84. package/panel-keys.ts +0 -37
  85. package/prompts.ts +0 -337
  86. package/proxy-modes.ts +0 -1109
  87. package/resource-tools.ts +0 -52
  88. package/runtime-owner.ts +0 -123
  89. package/sampling-handler.ts +0 -276
  90. package/scripts/build-app-bridge.mjs +0 -15
  91. package/scripts/build.mjs +0 -132
  92. package/scripts/prepare.mjs +0 -103
  93. package/search-ranking.ts +0 -132
  94. package/server-manager.ts +0 -1320
  95. package/session-recovery.ts +0 -316
  96. package/state.ts +0 -64
  97. package/tool-approval.ts +0 -78
  98. package/tool-loader.ts +0 -153
  99. package/tool-metadata.ts +0 -240
  100. package/tool-registrar.ts +0 -74
  101. package/tool-result-renderer.ts +0 -294
  102. package/tsconfig.build.json +0 -13
  103. package/tsconfig.json +0 -16
  104. package/types.ts +0 -785
  105. package/ui-resource-handler.ts +0 -249
  106. package/ui-server.ts +0 -808
  107. package/ui-session.ts +0 -568
  108. package/ui-stream-types.ts +0 -89
  109. package/ui-tool-visibility.ts +0 -25
  110. package/unix-socket-transport.ts +0 -85
  111. package/utils.ts +0 -307
package/dist/mcp-panel.js CHANGED
@@ -93,8 +93,6 @@ function estimateTokens(tool) {
93
93
  return Math.ceil((tool.name.length + descLen + schemaLen) / 4) + 10;
94
94
  }
95
95
  class McpPanel {
96
- callbacks;
97
- done;
98
96
  noticeLines;
99
97
  prefix;
100
98
  servers = [];
@@ -116,6 +114,8 @@ class McpPanel {
116
114
  keys;
117
115
  static MAX_VISIBLE = 12;
118
116
  static INACTIVITY_MS = 60_000;
117
+ callbacks;
118
+ done;
119
119
  constructor(config, cache, provenance, callbacks, tui, done, options = {}) {
120
120
  this.callbacks = callbacks;
121
121
  this.done = done;
@@ -31,10 +31,6 @@ export interface SetupPanelOptions {
31
31
  keybindings?: PanelKeybindings;
32
32
  }
33
33
  export declare class McpSetupPanel {
34
- private discovery;
35
- private callbacks;
36
- private options;
37
- private done;
38
34
  private screen;
39
35
  private actionCursor;
40
36
  private importCursor;
@@ -47,6 +43,10 @@ export declare class McpSetupPanel {
47
43
  private keys;
48
44
  private inactivityTimeout;
49
45
  private static readonly INACTIVITY_MS;
46
+ private discovery;
47
+ private callbacks;
48
+ private options;
49
+ private done;
50
50
  constructor(discovery: McpDiscoverySummary, callbacks: SetupPanelCallbacks, options: SetupPanelOptions, tui: {
51
51
  requestRender(): void;
52
52
  }, done: () => void);
@@ -72,6 +72,7 @@ export declare class McpSetupPanel {
72
72
  private previewWidth;
73
73
  private getActionPreview;
74
74
  private formatPreview;
75
+ private previewOrError;
75
76
  private formatWritePreview;
76
77
  private padLine;
77
78
  invalidate(): void;
@@ -38,10 +38,6 @@ function wrapText(text, width) {
38
38
  return lines.length > 0 ? lines : [""];
39
39
  }
40
40
  export class McpSetupPanel {
41
- discovery;
42
- callbacks;
43
- options;
44
- done;
45
41
  screen;
46
42
  actionCursor = 0;
47
43
  importCursor = 0;
@@ -54,6 +50,10 @@ export class McpSetupPanel {
54
50
  keys;
55
51
  inactivityTimeout = null;
56
52
  static INACTIVITY_MS = 60_000;
53
+ discovery;
54
+ callbacks;
55
+ options;
56
+ done;
57
57
  constructor(discovery, callbacks, options, tui, done) {
58
58
  this.discovery = discovery;
59
59
  this.callbacks = callbacks;
@@ -361,7 +361,7 @@ export class McpSetupPanel {
361
361
  lines.push(this.padLine(fg(this.t.muted, `… ${actions.length - end} more below`), innerW));
362
362
  }
363
363
  lines.push(this.padLine("", innerW));
364
- const preview = this.getActionPreview(this.getSelectedAction(), this.previewWidth(innerW));
364
+ const preview = this.previewOrError(width => this.getActionPreview(this.getSelectedAction(), width), innerW);
365
365
  for (const line of preview) {
366
366
  lines.push(this.padLine(line, innerW));
367
367
  }
@@ -384,8 +384,7 @@ export class McpSetupPanel {
384
384
  }
385
385
  lines.push(this.padLine("", innerW));
386
386
  const selected = this.discovery.imports.filter((entry) => this.selectedImports.has(entry.kind)).map((entry) => entry.kind);
387
- const preview = this.callbacks.previewImports(selected);
388
- for (const line of this.formatWritePreview("Compatibility import write preview", preview, [], this.previewWidth(innerW))) {
387
+ for (const line of this.previewOrError(width => this.formatWritePreview("Compatibility import write preview", this.callbacks.previewImports(selected), [], width), innerW)) {
389
388
  lines.push(this.padLine(line, innerW));
390
389
  }
391
390
  return lines;
@@ -522,6 +521,15 @@ export class McpSetupPanel {
522
521
  }
523
522
  return preview;
524
523
  }
524
+ previewOrError(preview, innerW) {
525
+ const width = this.previewWidth(innerW);
526
+ try {
527
+ return preview(width);
528
+ }
529
+ catch (error) {
530
+ return this.formatPreview([`Cannot preview write: ${error instanceof Error ? error.message : String(error)}`], width);
531
+ }
532
+ }
525
533
  formatWritePreview(title, preview, intro = [], width = DESKTOP_PREVIEW_WIDTH) {
526
534
  const lines = [];
527
535
  for (const line of intro) {
@@ -52,7 +52,6 @@ export declare function createMcpTraceEvent(direction: McpTraceDirection, server
52
52
  durationMs?: number;
53
53
  }): McpTraceEvent;
54
54
  export declare class McpTraceWriter {
55
- private readonly options;
56
55
  private readonly maxBytes;
57
56
  private readonly maxEvents;
58
57
  private readonly append;
@@ -65,6 +64,7 @@ export declare class McpTraceWriter {
65
64
  private initializationFailed;
66
65
  private persistenceError;
67
66
  private readonly fileReady;
67
+ private readonly options;
68
68
  constructor(options: McpTraceWriterOptions);
69
69
  get filePath(): string;
70
70
  get isDisabled(): boolean;
package/dist/mcp-trace.js CHANGED
@@ -73,7 +73,6 @@ export function createMcpTraceEvent(direction, server, transport, message, statu
73
73
  return event;
74
74
  }
75
75
  export class McpTraceWriter {
76
- options;
77
76
  maxBytes;
78
77
  maxEvents;
79
78
  append;
@@ -86,6 +85,7 @@ export class McpTraceWriter {
86
85
  initializationFailed = false;
87
86
  persistenceError;
88
87
  fileReady;
88
+ options;
89
89
  constructor(options) {
90
90
  this.options = options;
91
91
  this.maxBytes = boundedPositiveInteger(options.maxBytes, DEFAULT_MCP_TRACE_MAX_BYTES);
@@ -17,13 +17,15 @@ export async function resolveNpxBinary(command, args, signal) {
17
17
  if (!parsed)
18
18
  return null;
19
19
  const packageSpec = parsePackageSpec(parsed.packageSpec);
20
+ if (!packageSpec)
21
+ return null;
20
22
  const cacheKey = JSON.stringify([command, ...args]);
21
23
  const cache = loadCache();
22
24
  const cached = cache?.entries?.[cacheKey];
23
25
  if (cached
24
26
  && Date.now() - cached.resolvedAt < CACHE_TTL_MS
25
27
  && existsSync(cached.resolvedBin)
26
- && (!packageSpec?.exactVersion || cached.packageVersion === packageSpec.exactVersion)) {
28
+ && (!packageSpec.exactVersion || cached.packageVersion === packageSpec.exactVersion)) {
27
29
  return { binPath: cached.resolvedBin, extraArgs: parsed.extraArgs, isJs: cached.isJs };
28
30
  }
29
31
  const resolved = resolveFromNpmCache(parsed.packageSpec, parsed.binName);
@@ -289,11 +291,13 @@ function parsePackageSpec(spec) {
289
291
  if (!packageName)
290
292
  return null;
291
293
  const normalizedVersion = requestedVersion?.replace(/^=/, "").replace(/^v/i, "");
294
+ // Let npm resolve ranges and tags rather than choosing an arbitrary cached version.
295
+ if (requestedVersion !== undefined && (!normalizedVersion || !EXACT_PACKAGE_VERSION_RE.test(normalizedVersion))) {
296
+ return null;
297
+ }
292
298
  return {
293
299
  packageName,
294
- ...(normalizedVersion && EXACT_PACKAGE_VERSION_RE.test(normalizedVersion)
295
- ? { exactVersion: normalizedVersion }
296
- : {}),
300
+ ...(normalizedVersion ? { exactVersion: normalizedVersion } : {}),
297
301
  };
298
302
  }
299
303
  function defaultBinName(packageName) {
@@ -9,7 +9,7 @@ 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>;
11
11
  export declare function executeDescribe(state: McpExtensionState, toolName: string, serverFilter?: string): ProxyToolResult;
12
- export declare function executeSearch(state: McpExtensionState, query: string, server?: string, includeSchemas?: boolean, limit?: number, offset?: number): ProxyToolResult;
12
+ export declare function executeSearch(state: McpExtensionState, query: string, server?: string, includeSchemas?: boolean, limit?: number, offset?: number, caller?: "mcp" | "mcp_search"): ProxyToolResult;
13
13
  export declare function executeList(state: McpExtensionState, server: string, limit?: number, offset?: number): ProxyToolResult;
14
14
  export declare function executeInstructions(state: McpExtensionState, server: string): ProxyToolResult;
15
15
  export declare function executeConnect(state: McpExtensionState, serverName: string, signal?: AbortSignal, limit?: number, offset?: number): Promise<ProxyToolResult>;
@@ -63,10 +63,10 @@ export declare function prepareMcpConnection(state: McpExtensionState, serverNam
63
63
  recoverAuthConnection?: never;
64
64
  autoAuthAttempted?: never;
65
65
  } | {
66
+ error?: never;
66
67
  connection: ServerConnection;
67
68
  recoverAuthConnection: (serverName: string, signal?: AbortSignal, challenge?: import("./session-recovery.ts").AuthChallengeContext) => Promise<ServerConnection | undefined>;
68
69
  autoAuthAttempted: () => boolean;
69
- error?: never;
70
70
  }>;
71
71
  export declare function executeCall(state: McpExtensionState, toolName: string, args?: Record<string, unknown>, serverOverride?: string, getPiTools?: () => ToolInfo[], signal?: AbortSignal, identity?: McpToolCallIdentity, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>, options?: ExecuteCallOptions): Promise<ProxyToolResult>;
72
72
  export {};
@@ -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;
@@ -21,7 +21,7 @@ function disabledResult(mode, serverName) {
21
21
  details: { mode, error: "server_disabled", server: serverName, message },
22
22
  };
23
23
  }
24
- function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
24
+ function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
25
25
  const oauth = state.config.mcpServers[serverName]?.oauth;
26
26
  if (oauth && oauth.crossAppAccess)
27
27
  defaultMessage = `Server "${serverName}" requires enterprise authorization. Check oauth.crossAppAccess IdP configuration and ID-token source, then retry.`;
@@ -33,7 +33,7 @@ function getAuthFailedMessage(state, serverName, message) {
33
33
  if (customGuidance || (oauth && oauth.crossAppAccess)) {
34
34
  return `OAuth authentication failed for "${serverName}": ${message}. ${getAuthRequiredMessage(state, serverName)}`;
35
35
  }
36
- return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
36
+ return `OAuth authentication failed for "${serverName}": ${message}. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
37
37
  }
38
38
  function getRedirectPort(authorizationUrl) {
39
39
  try {
@@ -91,7 +91,7 @@ async function attemptAutoAuth(state, serverName, signal) {
91
91
  if (!state.ui && !isNonInteractiveOAuth(definition.oauth)) {
92
92
  return {
93
93
  status: "failed",
94
- message: getAuthRequiredMessage(state, serverName, `Server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`),
94
+ message: getAuthRequiredMessage(state, serverName, `Server "${serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`),
95
95
  };
96
96
  }
97
97
  try {
@@ -268,7 +268,7 @@ export async function executeAuthStart(state, serverName, signal) {
268
268
  const definition = state.config.mcpServers[serverName];
269
269
  if (!definition) {
270
270
  return {
271
- content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
271
+ content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
272
272
  details: { mode: "auth-start", error: "not_found", server: serverName },
273
273
  };
274
274
  }
@@ -314,7 +314,7 @@ export async function executeAuthComplete(state, serverName, input, signal) {
314
314
  const definition = state.config.mcpServers[serverName];
315
315
  if (!definition) {
316
316
  return {
317
- content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
317
+ content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
318
318
  details: { mode: "auth-complete", error: "not_found", server: serverName },
319
319
  };
320
320
  }
@@ -372,7 +372,7 @@ export function executeDescribe(state, toolName, serverFilter) {
372
372
  return disabledResult("describe", disabled[0]);
373
373
  const suggestions = rankSuggestions(state, toolName, 5, serverFilter);
374
374
  const hint = suggestions.length > 0
375
- ? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }).`
375
+ ? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }). Load if inactive: mcp_search({ enable: ["gateway"] }).`
376
376
  : `Use mcp_search({ query: "..." }) to search.`;
377
377
  return {
378
378
  content: [{ type: "text", text: `Tool "${toolName}" not found. ${hint}` }],
@@ -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;
@@ -461,7 +461,7 @@ export function executeList(state, server, limit = 12, offset = 0) {
461
461
  const definition = state.config.mcpServers[server];
462
462
  if (!definition) {
463
463
  return {
464
- content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
464
+ content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
465
465
  details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
466
466
  };
467
467
  }
@@ -534,7 +534,7 @@ export function executeInstructions(state, server) {
534
534
  const definition = state.config.mcpServers[server];
535
535
  if (!definition) {
536
536
  return {
537
- content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
537
+ content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
538
538
  details: { mode: "instructions", server, error: "not_found" },
539
539
  };
540
540
  }
@@ -565,7 +565,7 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
565
565
  const definition = state.config.mcpServers[serverName];
566
566
  if (!definition) {
567
567
  return {
568
- content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
568
+ content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
569
569
  details: { mode: "connect", error: "not_found", server: serverName },
570
570
  };
571
571
  }
@@ -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 }],
@@ -962,6 +962,7 @@ export async function executeCall(state, toolName, args, serverOverride, getPiTo
962
962
  let hint = suggestions.length
963
963
  ? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: ${JSON.stringify(suggestions[0])} }).`
964
964
  : serverName ? ` Search with mcp({ action: "search", query: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).` : ' Use mcp({ action: "search", query: "..." }) to search.';
965
+ hint += ' If mcp is inactive, load it with mcp_search({ enable: ["gateway"] }).';
965
966
  if (serverName && state.toolMetadata.has(serverName) && state.manager.getConnection(serverName)?.status !== "connected")
966
967
  hint += ` Refresh a stale catalog with mcp({ action: "connect", server: ${JSON.stringify(serverName)} }).`;
967
968
  return {
@@ -973,7 +974,7 @@ export async function executeCall(state, toolName, args, serverOverride, getPiTo
973
974
  return missing();
974
975
  const definition = state.config.mcpServers[serverName];
975
976
  if (!definition)
976
- return { content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }], details: { mode: "call", error: "server_not_found", server: serverName, requestedTool: toolName } };
977
+ return { content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }], details: { mode: "call", error: "server_not_found", server: serverName, requestedTool: toolName } };
977
978
  if (isServerDisabled(definition))
978
979
  return disabledResult("call", serverName);
979
980
  const cached = state.toolMetadata.get(serverName);
@@ -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;