@herbertgao/pi-extensions 2026.9.3 → 2026.9.5

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 (143) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_NOTICES.md +25 -0
  3. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  4. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  6. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  11. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  15. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  16. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  17. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +88 -19
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
  26. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  35. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  37. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  38. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  39. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  40. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  41. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  42. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  43. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  44. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  45. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  46. package/node_modules/@tifan/pi-recap/package.json +3 -2
  47. package/node_modules/pi-antigravity/LICENSE +21 -0
  48. package/node_modules/pi-antigravity/README.md +194 -0
  49. package/node_modules/pi-antigravity/package.json +67 -0
  50. package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
  51. package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
  52. package/node_modules/pi-antigravity/src/client/client.ts +561 -0
  53. package/node_modules/pi-antigravity/src/client/index.ts +1 -0
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  55. package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
  56. package/node_modules/pi-antigravity/src/image/image.ts +336 -0
  57. package/node_modules/pi-antigravity/src/image/index.ts +1 -0
  58. package/node_modules/pi-antigravity/src/index.ts +280 -0
  59. package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
  60. package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
  61. package/node_modules/pi-antigravity/src/models/index.ts +3 -0
  62. package/node_modules/pi-antigravity/src/models/models.ts +500 -0
  63. package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
  64. package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
  65. package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
  66. package/node_modules/pi-antigravity/src/types/index.ts +2 -0
  67. package/node_modules/pi-antigravity/src/types/types.ts +292 -0
  68. package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
  69. package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
  70. package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
  71. package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
  72. package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
  73. package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
  74. package/node_modules/pi-antigravity/tsconfig.json +21 -0
  75. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  76. package/node_modules/pi-mcp-adapter/README.md +125 -19
  77. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  78. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  79. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  80. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  81. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  82. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  83. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  84. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  85. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  86. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  87. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  88. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  89. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  90. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  91. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  92. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  93. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  94. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  95. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  97. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  98. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  99. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  100. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  101. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  102. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  103. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  104. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  105. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  106. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  107. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  108. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  109. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  110. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  111. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  112. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  113. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  114. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  115. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  116. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  117. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  118. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  119. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  120. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  121. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  122. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  123. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  124. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  125. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  126. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  127. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  128. package/node_modules/pi-mcp-adapter/package.json +13 -3
  129. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  130. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  131. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  132. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  133. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  134. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  135. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  136. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  137. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  138. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  139. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  140. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  141. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  142. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  143. package/package.json +15 -15
@@ -1,27 +1,24 @@
1
- import type { AgentToolUpdateCallback, ExtensionAPI, ExtensionContext, ToolInfo } from "@earendil-works/pi-coding-agent";
1
+ import { withFileMutationQueue, type AgentToolUpdateCallback, type ExtensionAPI, type ExtensionContext, type ToolInfo } from "@earendil-works/pi-coding-agent";
2
+ import { resolve } from "node:path";
2
3
  import type { McpExtensionState } from "./state.ts";
3
- import type { DirectToolSpec, McpAdapterOptions, McpConfig, PromptMetadata, ServerEntry } from "./types.ts";
4
+ import { isServerDisabled, type DirectToolSpec, type McpAdapterOptions, type McpConfig, type PromptMetadata, type ServerEntry } from "./types.ts";
4
5
  import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
5
6
  import { Type } from "typebox";
6
7
  import type { TSchema } from "typebox";
7
- import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, manageBearerToken, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.ts";
8
- import { cloneMcpConfig, loadMcpConfig, writeProjectServerDisabledOverride } from "./config.ts";
9
- import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, prepareDirectToolArguments, resolveDirectTools } from "./direct-tools.ts";
10
- import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.ts";
8
+ import { cloneMcpConfig, discoverConfiguredClaudePluginSkills, getPiGlobalConfigPath, getProjectConfigPath, loadMcpConfig, resolveConfiguredClaudePluginMcp, writeProjectServerDisabledOverride, writeSharedServerEntry } from "./config.ts";
9
+ import { buildProxyDescription, getMissingConfiguredDirectToolServers, prepareDirectToolArguments, resolveDirectTools } from "./direct-tool-surface.ts";
11
10
  import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
12
- import { loadMetadataCache, parseDirectToolSelectors, type MetadataCache } from "./metadata-cache.ts";
11
+ import { computeServerHash, isServerCacheValid, loadMetadataCache, parseDirectToolSelectors, type MetadataCache } from "./metadata-cache.ts";
13
12
  import { createPromptCommand, resolveCachedPrompts } from "./prompts.ts";
14
13
  import { logger } from "./logger.ts";
15
- import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
16
14
  import { formatTerminalError, getConfigPathFromArgv, normalizeDirectToolInputSchema, truncateAtWord } from "./utils.ts";
17
- import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.ts";
18
15
  import { createMcpDirectToolCallRenderer, createMcpProxyToolCallRenderer, createMcpScriptToolCallRenderer, createMcpToolResultRenderer, resolveMcpToolRenderOptions } from "./tool-result-renderer.ts";
19
16
  import { toolErrorOverride } from "./error-signal.ts";
20
17
  import { createMcpRuntimeOwner, createOwnedUi, isAbortError, type McpRuntimeOwner } from "./runtime-owner.ts";
21
18
  import { publishMcpStatusShutdown } from "./mcp-status.ts";
22
- import { runMcpScript } from "./mcp-code.ts";
23
- import { cleanupMaterializedBinaryResources } from "./tool-registrar.ts";
24
19
  import { syncNamespaceProxyTools } from "./namespace-tools.ts";
20
+ import { restoreSessionApprovalState } from "./session-approvals.ts";
21
+ import { createRetryableLoader } from "./lazy-loader.ts";
25
22
 
26
23
  export type { McpAdapterOptions } from "./types.ts";
27
24
  export type { ServerEntry } from "./types.ts";
@@ -45,6 +42,14 @@ export {
45
42
  type McpToolApprovalRequest,
46
43
  } from "./types.ts";
47
44
 
45
+ const loadCoreRuntime = createRetryableLoader(() => import("./init.ts"));
46
+ const loadOAuthRuntime = createRetryableLoader(() => import("./mcp-auth-flow.ts"));
47
+ const loadProxyModes = createRetryableLoader(() => import("./proxy-modes.ts"));
48
+ const loadDirectExecution = createRetryableLoader(() => import("./direct-tools.ts"));
49
+ const loadCommands = createRetryableLoader(() => import("./commands.ts"));
50
+ const loadCodeMode = createRetryableLoader(() => import("./mcp-code.ts"));
51
+ const loadInstallParsing = createRetryableLoader(() => import("./mcp-install.ts"));
52
+
48
53
  const INIT_WAIT_TIMEOUT_MS = 30_000;
49
54
  const INIT_FAILURE_MESSAGE_MAX_CHARS = 1_000;
50
55
  const INIT_WAIT_TIMED_OUT: unique symbol = Symbol("init-wait-timed-out");
@@ -91,6 +96,24 @@ export interface McpRuntimeSnapshotRequest {
91
96
  const runtimeRegistrars = new WeakMap<ExtensionAPI, (name: string, definition: ServerEntry) => McpServerRegistration>();
92
97
  const runtimeSnapshotters = new WeakMap<ExtensionAPI, (name: string) => McpRuntimeServerSnapshot>();
93
98
 
99
+ function resolveProgrammaticClaudePluginPath(path: string, cwd: string): string {
100
+ if (path === "~") return resolve(process.env.HOME ?? "", ".");
101
+ if (path.startsWith("~/")) return resolve(process.env.HOME ?? "", path.slice(2));
102
+ return resolve(cwd, path);
103
+ }
104
+
105
+ function normalizeProgrammaticConfig(config: McpConfig): McpConfig {
106
+ if (!config.claudePlugins) return config;
107
+ const cwd = process.cwd();
108
+ return {
109
+ ...config,
110
+ claudePlugins: config.claudePlugins.map(plugin => ({
111
+ ...plugin,
112
+ path: resolveProgrammaticClaudePluginPath(plugin.path, cwd),
113
+ })),
114
+ };
115
+ }
116
+
94
117
  async function awaitWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | typeof INIT_WAIT_TIMED_OUT> {
95
118
  let timer: ReturnType<typeof setTimeout> | undefined;
96
119
  try {
@@ -135,10 +158,20 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
135
158
  const programmaticConfig = sessionConfig !== undefined;
136
159
  let state: McpExtensionState | null = null;
137
160
  let initPromise: Promise<McpExtensionState> | null = null;
161
+ let initStartedPromise: Promise<void> | null = null;
138
162
  let currentOwner: McpRuntimeOwner | null = null;
139
163
  let currentOAuthRuntime: McpOAuthRuntime | null = null;
140
164
  let lifecycleGeneration = 0;
141
165
  let retainedInitFailure: string | null = null;
166
+ let finalizationGuard: (() => void) | null = null;
167
+ let finalizationRegistrations: Set<string> | null = null;
168
+
169
+ function callReentrant<T>(callback: () => T): T {
170
+ finalizationGuard?.();
171
+ const result = callback();
172
+ finalizationGuard?.();
173
+ return result;
174
+ }
142
175
 
143
176
  function retainInitFailure(error: unknown): string {
144
177
  const message = truncateAtWord(formatTerminalError(error), INIT_FAILURE_MESSAGE_MAX_CHARS);
@@ -164,23 +197,70 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
164
197
  return true;
165
198
  }
166
199
 
200
+ interface RuntimeGuard {
201
+ generation: number;
202
+ owner: McpRuntimeOwner | null;
203
+ state: McpExtensionState | null;
204
+ }
205
+
206
+ function captureRuntimeGuard(expectedState: McpExtensionState | null = state): RuntimeGuard {
207
+ return { generation: lifecycleGeneration, owner: currentOwner, state: expectedState };
208
+ }
209
+
210
+ function assertRuntimeGuard(guard: RuntimeGuard): void {
211
+ guard.owner?.throwIfInactive();
212
+ if (
213
+ lifecycleGeneration !== guard.generation
214
+ || currentOwner !== guard.owner
215
+ || state !== guard.state
216
+ ) {
217
+ throw guard.owner?.signal.reason ?? new Error("MCP operation belongs to a stale session");
218
+ }
219
+ }
220
+
221
+ function isRuntimeGuardStale(guard: RuntimeGuard): boolean {
222
+ return guard.owner?.isActive() === false
223
+ || lifecycleGeneration !== guard.generation
224
+ || currentOwner !== guard.owner
225
+ || state !== guard.state;
226
+ }
227
+
228
+ async function loadForRuntime<T>(loader: () => Promise<T>, guard: RuntimeGuard): Promise<T> {
229
+ const loaded = await loader();
230
+ assertRuntimeGuard(guard);
231
+ return loaded;
232
+ }
233
+
234
+ function createCommandContext(ctx: ExtensionContext, owner: McpRuntimeOwner | null): ExtensionContext {
235
+ const commandCtx = Object.create(ctx) as ExtensionContext;
236
+ const hasUI = ctx.hasUI;
237
+ Object.defineProperties(commandCtx, {
238
+ hasUI: { value: hasUI, enumerable: true },
239
+ ui: {
240
+ value: hasUI ? owner ? createOwnedUi(ctx.ui, owner) : ctx.ui : undefined,
241
+ enumerable: true,
242
+ },
243
+ signal: { value: owner?.signal ?? ctx.signal, enumerable: true },
244
+ });
245
+ return commandCtx;
246
+ }
247
+
167
248
  function startGatewayRetryInitialization(ctx: ExtensionContext): void {
168
249
  const generation = ++lifecycleGeneration;
169
250
  const owner = createMcpRuntimeOwner();
170
- const oauthRuntime = createOAuthRuntime(owner.signal);
171
251
  currentOwner = owner;
172
- currentOAuthRuntime = oauthRuntime;
252
+ currentOAuthRuntime = null;
173
253
  state = null;
174
- startInitialization(ctx, owner, oauthRuntime, generation, "stale_gateway_retry_initialization");
254
+ startInitialization(ctx, owner, generation, "stale_gateway_retry_initialization");
175
255
  }
176
256
 
177
- async function shutdownState(currentState: McpExtensionState | null, reason: string): Promise<void> {
257
+ async function shutdownState(currentState: McpExtensionState | null, reason: string, publishStatus = true): Promise<void> {
178
258
  if (!currentState) {
179
- publishMcpStatusShutdown(pi.events);
259
+ if (publishStatus) publishMcpStatusShutdown(pi.events);
180
260
  return;
181
261
  }
182
262
 
183
- publishMcpStatusShutdown(currentState.statusEvents);
263
+ if (publishStatus) publishMcpStatusShutdown(currentState.statusEvents);
184
264
 
185
265
  if (currentState.uiServer) {
186
266
  currentState.uiServer.close(reason);
@@ -189,6 +269,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
189
269
 
190
270
  let flushError: unknown;
191
271
  try {
272
+ const { flushMetadataCache } = await loadCoreRuntime();
192
273
  flushMetadataCache(currentState);
193
274
  } catch (error) {
194
275
  flushError = error;
@@ -213,19 +294,54 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
213
294
  }
214
295
  }
215
296
 
297
+ function restoreCurrentSessionApprovals(targetState: McpExtensionState): void {
298
+ const sessionManager = targetState.sessionManager;
299
+ if (!sessionManager) return;
300
+
301
+ let branch: readonly unknown[] = [];
302
+ try {
303
+ branch = sessionManager.getBranch();
304
+ } catch (error) {
305
+ const detail = error instanceof Error ? error.message : String(error);
306
+ logger.debug(`MCP: could not read the active session branch for approval restore: ${detail}`);
307
+ }
308
+ restoreSessionApprovalState(targetState, branch);
309
+ }
310
+
216
311
  const earlyConfigPath = programmaticConfig
217
312
  ? undefined
218
313
  : options.configPath ?? getConfigPathFromArgv();
219
314
  const earlyConfig = programmaticConfig
220
- ? cloneMcpConfig(sessionConfig)
315
+ ? resolveConfiguredClaudePluginMcp(cloneMcpConfig(sessionConfig), process.cwd())
221
316
  : loadMcpConfig(earlyConfigPath);
222
317
  const earlyCache = loadMetadataCache();
223
318
  const envRaw = process.env.MCP_DIRECT_TOOLS;
224
319
  const envDirectToolOverride = parseEnvDirectToolOverride(envRaw);
225
320
  const namespaceEnvOverride = resolveNamespaceEnvOverride(envRaw, envDirectToolOverride);
321
+ const enabledEarlyServers = Object.entries(earlyConfig.mcpServers)
322
+ .filter(([, definition]) => !isServerDisabled(definition));
323
+ const hasStartupServer = enabledEarlyServers.some(([, definition]) =>
324
+ definition.lifecycle === "eager" || definition.lifecycle === "keep-alive");
325
+ const hasUsableCachedMetadata = earlyCache !== null && enabledEarlyServers.every(([serverName, definition]) => {
326
+ const entry = earlyCache.servers[serverName];
327
+ return entry !== undefined && isServerCacheValid(entry, definition);
328
+ });
329
+ const hasColdEnvironmentDirectTools = envRaw !== undefined && envRaw !== "__none__"
330
+ && getMissingConfiguredDirectToolServers(earlyConfig, earlyCache, envDirectToolOverride).length > 0;
331
+ const deferSessionRuntime = !hasStartupServer && hasUsableCachedMetadata && !hasColdEnvironmentDirectTools;
226
332
  const registeredDirectTools = new Map<string, string>();
333
+ const registeredDirectToolServers = new Map<string, string>();
334
+ const registeredDirectToolVersions = new Map<string, number>();
335
+ // Overlapping connect results consume each server's discovery names once.
336
+ // Removal clears the record so stale/fallback reactivation is reportable.
337
+ const reportedDirectToolNamesByServer = new Map<string, Set<string>>();
227
338
  const registeredNamespaceProxyTools = new Set<string>();
228
339
  const fallbackDeactivatedTools = new Set<string>();
340
+ // directTools: "search" — registered inactive, activated by mcp({ search }).
341
+ const lazyDirectTools = new Set<string>();
342
+ // The lazy tools a search has activated; every other lazy tool is held out
343
+ // of the active set. Per process: nothing here survives a restart.
344
+ const searchActivatedTools = new Set<string>();
229
345
  const toolRenderOptions = resolveMcpToolRenderOptions(earlyConfig.settings);
230
346
  const toolRenderShell = toolRenderOptions.resultRendering === "compact" ? "self" : "default";
231
347
  const renderMcpToolResult = createMcpToolResultRenderer(toolRenderOptions);
@@ -265,11 +381,23 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
265
381
  resourceUri: spec.resourceUri,
266
382
  uiResourceUri: spec.uiResourceUri,
267
383
  uiStreamMode: spec.uiStreamMode,
384
+ // A mode-only change (search ↔ eager) must re-register, or the tool
385
+ // keeps the activation behavior of the mode it was registered under.
386
+ lazy: spec.lazy === true,
268
387
  });
269
388
  }
270
389
 
390
+ function forgetReportedDirectToolName(serverName: string | undefined, toolName: string): void {
391
+ if (!serverName) return;
392
+ const reportedNames = reportedDirectToolNamesByServer.get(serverName);
393
+ if (!reportedNames) return;
394
+ reportedNames.delete(toolName);
395
+ if (reportedNames.size === 0) reportedDirectToolNamesByServer.delete(serverName);
396
+ }
397
+
271
398
  function registerDirectTool(spec: DirectToolSpec, config: McpConfig): void {
272
- (pi.registerTool as (tool: unknown) => unknown)({
399
+ finalizationRegistrations?.add(spec.prefixedName);
400
+ callReentrant(() => (pi.registerTool as (tool: unknown) => unknown)({
273
401
  name: spec.prefixedName,
274
402
  label: `MCP: ${spec.originalName}`,
275
403
  description: spec.description || "(no description)",
@@ -278,11 +406,69 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
278
406
  ...(config.settings?.strictDirectToolArguments === true
279
407
  ? { prepareArguments: (args: unknown) => prepareDirectToolArguments(spec.inputSchema, args) }
280
408
  : {}),
281
- execute: createDirectToolExecutor(() => state, () => initPromise, spec),
409
+ async execute(toolCallId: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) {
410
+ let executor: ReturnType<(typeof import("./direct-tools.ts"))["createDirectToolExecutor"]>;
411
+ let guard: RuntimeGuard | undefined;
412
+ try {
413
+ const targetState = await ensureSessionRuntime(ctx);
414
+ if (!targetState) throw new Error("MCP not initialized");
415
+ guard = captureRuntimeGuard(targetState);
416
+ const executionGuard = guard;
417
+ const { createDirectToolExecutor } = await loadForRuntime(loadDirectExecution, executionGuard);
418
+ executor = createDirectToolExecutor(() => executionGuard.state, () => initPromise, spec);
419
+ } catch (error) {
420
+ if (guard && (isRuntimeGuardStale(guard) || (guard.owner && isOwnerAbortError(error, guard.owner)))) throw error;
421
+ const message = error instanceof Error ? error.message : String(error);
422
+ return {
423
+ content: [{ type: "text" as const, text: `MCP initialization failed for ${spec.serverName}: ${message}` }],
424
+ details: { error: "init_failed", server: spec.serverName, message },
425
+ };
426
+ }
427
+ if (!guard) throw new Error("MCP runtime guard unavailable");
428
+ assertRuntimeGuard(guard);
429
+ return executor(toolCallId, params, signal, onUpdate, ctx);
430
+ },
282
431
  renderShell: toolRenderShell,
283
432
  renderCall: createMcpDirectToolCallRenderer(spec.prefixedName, toolRenderOptions),
284
433
  renderResult: renderMcpToolResult,
285
- });
434
+ }));
435
+ }
436
+
437
+ // Pi registers a tool active. A lazy tool must not stay that way: hold every
438
+ // lazy tool that search has not activated out of the active set. Safe to call
439
+ // repeatedly; a no-op until Pi's action methods are available.
440
+ function holdLazyToolsInactive(): void {
441
+ if (lazyDirectTools.size === 0) return;
442
+ const activeTools = getActiveToolsIfReady();
443
+ if (!activeTools) return;
444
+ const next = activeTools.filter((name) => !lazyDirectTools.has(name) || searchActivatedTools.has(name));
445
+ if (next.length !== activeTools.length) pi.setActiveTools(next);
446
+ }
447
+
448
+ /**
449
+ * Activate the lazy direct tools a search matched, additively. This is the
450
+ * one place a search-mode tool becomes active; nothing is ever deactivated
451
+ * here. Returns the names that actually changed state so the result can
452
+ * report only real additions.
453
+ */
454
+ function activateSearchMatches(matches: ReadonlyArray<{ server: string; tool: string }>): string[] {
455
+ const activeTools = getActiveToolsIfReady();
456
+ if (!activeTools) return [];
457
+ const activeSet = new Set(activeTools);
458
+ // executeSearch reports ToolMetadata.name, which is the prefixed name a
459
+ // direct tool is registered under — the same key the lazy set holds.
460
+ const added: string[] = [];
461
+ for (const match of matches) {
462
+ const name = match.tool;
463
+ if (!lazyDirectTools.has(name) || registeredDirectToolServers.get(name) !== match.server) continue;
464
+ if (activeSet.has(name) || added.includes(name)) continue;
465
+ added.push(name);
466
+ }
467
+ if (added.length > 0) {
468
+ pi.setActiveTools([...activeTools, ...added]);
469
+ for (const name of added) searchActivatedTools.add(name);
470
+ }
471
+ return added;
286
472
  }
287
473
 
288
474
  function activeFailureServers(): Set<string> {
@@ -299,7 +485,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
299
485
 
300
486
  function getActiveToolsIfReady(): string[] | undefined {
301
487
  try {
302
- return pi.getActiveTools?.();
488
+ return callReentrant(() => pi.getActiveTools?.());
303
489
  } catch (error) {
304
490
  if (error instanceof Error
305
491
  && error.message.includes("Action methods cannot be called during extension loading")) return undefined;
@@ -310,7 +496,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
310
496
  function deactivateTools(toolNames: string[]): string[] {
311
497
  if (toolNames.length === 0) return [];
312
498
  const unregisterTool = (pi as ExtensionAPI & { unregisterTool?: (name: string) => boolean }).unregisterTool;
313
- const unregistered = toolNames.filter((toolName) => unregisterTool?.(toolName) === true);
499
+ const unregistered = toolNames.filter((toolName) => callReentrant(() => unregisterTool?.(toolName)) === true);
314
500
  const fallbackNames = toolNames.filter((toolName) => !unregistered.includes(toolName));
315
501
  const remove = new Set(toolNames);
316
502
  const activeTools = getActiveToolsIfReady();
@@ -321,7 +507,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
321
507
  const nextActiveTools = activeTools.filter((name) => !remove.has(name));
322
508
  if (nextActiveTools.length !== activeTools.length) {
323
509
  for (const toolName of fallbackNames) fallbackDeactivatedTools.add(toolName);
324
- pi.setActiveTools(nextActiveTools);
510
+ callReentrant(() => pi.setActiveTools(nextActiveTools));
325
511
  }
326
512
  return unregistered;
327
513
  }
@@ -345,29 +531,53 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
345
531
  const fingerprint = directToolFingerprint(spec);
346
532
  const previous = registeredDirectTools.get(spec.prefixedName);
347
533
  if (previous !== fingerprint) {
534
+ const previousServer = registeredDirectToolServers.get(spec.prefixedName);
348
535
  registerDirectTool(spec, config);
536
+ finalizationGuard?.();
349
537
  registeredDirectTools.set(spec.prefixedName, fingerprint);
350
- if (fallbackDeactivatedTools.delete(spec.prefixedName)) {
538
+ registeredDirectToolServers.set(spec.prefixedName, spec.serverName);
539
+ registeredDirectToolVersions.set(spec.prefixedName, (registeredDirectToolVersions.get(spec.prefixedName) ?? 0) + 1);
540
+ if (previousServer !== spec.serverName) {
541
+ forgetReportedDirectToolName(previousServer, spec.prefixedName);
542
+ }
543
+ if (fallbackDeactivatedTools.delete(spec.prefixedName) && !spec.lazy) {
351
544
  const activeTools = getActiveToolsIfReady();
352
545
  if (activeTools && !activeTools.includes(spec.prefixedName)) {
353
- pi.setActiveTools([...activeTools, spec.prefixedName]);
546
+ callReentrant(() => pi.setActiveTools([...activeTools, spec.prefixedName]));
354
547
  }
355
548
  }
356
549
  (previous ? updated : added).push(spec.prefixedName);
357
550
  }
551
+ if (spec.lazy) {
552
+ // Search mode, whether first registered or flipped from eager (e.g. in
553
+ // the panel): held inactive below unless a search already activated it.
554
+ lazyDirectTools.add(spec.prefixedName);
555
+ } else if (lazyDirectTools.delete(spec.prefixedName)) {
556
+ // Search → eager: an eager direct tool is active by definition, so a
557
+ // tool that search never activated must be activated now. Pi does not
558
+ // re-activate a tool it already knows on re-registration.
559
+ searchActivatedTools.delete(spec.prefixedName);
560
+ const activeTools = getActiveToolsIfReady();
561
+ if (activeTools && !activeTools.includes(spec.prefixedName)) callReentrant(() => pi.setActiveTools([...activeTools, spec.prefixedName]));
562
+ }
358
563
  }
359
564
 
360
565
  for (const toolName of [...registeredDirectTools.keys()]) {
361
566
  if (nextNames.has(toolName)) continue;
567
+ const serverName = registeredDirectToolServers.get(toolName);
362
568
  registeredDirectTools.delete(toolName);
569
+ registeredDirectToolServers.delete(toolName);
570
+ forgetReportedDirectToolName(serverName, toolName);
571
+ if (lazyDirectTools.delete(toolName)) searchActivatedTools.delete(toolName);
363
572
  deactivated.push(toolName);
364
573
  }
365
574
 
366
575
  deactivateTools(deactivated);
576
+ holdLazyToolsInactive();
367
577
  return { specs, reservedDirectNames, activeDirectNames: nextNames, added, updated, deactivated };
368
578
  }
369
579
 
370
- function applyDirectToolConfigChanges(changes: Map<string, true | string[] | false>): void {
580
+ function applyDirectToolConfigChanges(changes: Map<string, true | string[] | false | "search">): void {
371
581
  if (!state) return;
372
582
  for (const [serverName, value] of changes) {
373
583
  const definition = state.config.mcpServers[serverName];
@@ -376,18 +586,44 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
376
586
  }
377
587
  }
378
588
 
589
+ function loadToolSurfaceCache(config: McpConfig): MetadataCache | null {
590
+ const cache = loadMetadataCache();
591
+ const currentState = state;
592
+ if (!currentState || !cache) return cache;
593
+ const servers = { ...cache.servers };
594
+ const connections = callReentrant(() => [...currentState.manager.getAllConnections()]);
595
+ for (const [serverName, connection] of connections) {
596
+ const definition = config.mcpServers[serverName];
597
+ const entry = servers[serverName];
598
+ if (connection.status !== "connected" || !entry || !definition || isServerDisabled(definition)) continue;
599
+ const configHash = computeServerHash(definition);
600
+ if (computeServerHash(connection.definition) !== configHash || entry.configHash !== configHash) continue;
601
+ const { ttlMs: _liveTtl, ...liveEntry } = entry;
602
+ servers[serverName] = liveEntry;
603
+ }
604
+ return { ...cache, servers };
605
+ }
606
+
379
607
  function syncToolSurface(ctx?: ExtensionContext): void {
380
608
  const config = state?.config ?? earlyConfig;
381
- const cache = loadMetadataCache();
609
+ const cache = loadToolSurfaceCache(config);
382
610
  const result = syncDirectTools(config, cache);
611
+ if (state) {
612
+ const directToolCounts = state.directToolCounts ?? new Map<string, number>();
613
+ directToolCounts.clear();
614
+ for (const spec of result.specs) {
615
+ directToolCounts.set(spec.serverName, (directToolCounts.get(spec.serverName) ?? 0) + 1);
616
+ }
617
+ state.directToolCounts = directToolCounts;
618
+ }
383
619
  syncProxyTool(config, cache, result.specs);
384
620
  syncNamespaceTools(config, cache, result.reservedDirectNames, result.activeDirectNames);
385
621
  const changed = result.added.length + result.updated.length + result.deactivated.length;
386
622
  if (changed > 0 && ctx?.hasUI) {
387
- ctx.ui.notify(
623
+ callReentrant(() => ctx.ui.notify(
388
624
  `MCP: direct tools refreshed (+${result.added.length}, ~${result.updated.length}, -${result.deactivated.length})`,
389
625
  "info",
390
- );
626
+ ));
391
627
  }
392
628
  }
393
629
 
@@ -408,11 +644,19 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
408
644
  pi,
409
645
  getState: () => state,
410
646
  getInitPromise: () => initPromise,
647
+ ensureRuntime: (ctx) => ensureSessionRuntime(ctx as ExtensionContext),
648
+ executeCall: async (...args) => {
649
+ const guard = captureRuntimeGuard(args[0]);
650
+ return (await loadForRuntime(loadProxyModes, guard)).executeCall(...args);
651
+ },
411
652
  getPiTools: () => pi.getAllTools(),
412
653
  renderOptions: toolRenderOptions,
413
654
  renderShell: toolRenderShell,
414
655
  renderResult: renderMcpToolResult,
656
+ guardReentrant: () => finalizationGuard?.(),
657
+ onToolRegistered: (name) => finalizationRegistrations?.add(name),
415
658
  });
659
+ finalizationGuard?.();
416
660
  for (const name of result.added) registeredNamespaceProxyTools.add(name);
417
661
  for (const name of result.deactivated) registeredNamespaceProxyTools.delete(name);
418
662
  }
@@ -425,13 +669,22 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
425
669
  logger.debug(`MCP: prompt "${spec.originalName}" on ${spec.serverName} skipped; /${spec.commandName} is already registered`);
426
670
  continue;
427
671
  }
672
+ callReentrant(() => pi.registerCommand(spec.commandName, createPromptCommand(pi, () => state, spec, {
673
+ ensureState: (ctx) => ensureSessionRuntime(ctx as unknown as ExtensionContext),
674
+ lazyConnect: async (...args) => {
675
+ const guard = captureRuntimeGuard(args[0]);
676
+ return (await loadForRuntime(loadCoreRuntime, guard)).lazyConnect(...args);
677
+ },
678
+ isStateCurrent: (targetState) => state === targetState && currentOwner?.isActive() === true,
679
+ })));
428
680
  registeredPromptCommands.add(spec.commandName);
429
- pi.registerCommand(spec.commandName, createPromptCommand(pi, () => state, spec));
430
681
  }
431
682
  }
432
683
 
433
684
  function syncPromptCommands(): void {
434
- registerPromptCommands([...(state?.promptMetadata?.values() ?? [])].flat());
685
+ registerPromptCommands([...(state?.promptMetadata?.entries() ?? [])]
686
+ .filter(([name]) => !state?.provisionalInstalls?.has(name))
687
+ .flatMap(([, prompts]) => prompts));
435
688
  }
436
689
 
437
690
  registerPromptCommands(resolveCachedPrompts(earlyConfig));
@@ -457,7 +710,14 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
457
710
  registeredState.config.mcpServers[name] = entry;
458
711
  attachRuntimeServerLifecycle(registeredState, name, entry);
459
712
  syncToolSurface();
460
- updateStatusBar(registeredState);
713
+ const guard = captureRuntimeGuard(registeredState);
714
+ void loadForRuntime(loadCoreRuntime, guard)
715
+ .then(({ updateStatusBar }) => updateStatusBar(registeredState))
716
+ .catch((error) => {
717
+ if (!isAbortError(error, guard.owner?.signal)) {
718
+ logger.debug(`MCP: could not update status after runtime registration: ${formatTerminalError(error)}`);
719
+ }
720
+ });
461
721
  }
462
722
  let disposed = false;
463
723
  return {
@@ -469,9 +729,11 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
469
729
  if (!currentState || currentState.config.mcpServers[name] !== entry) return;
470
730
  delete currentState.config.mcpServers[name];
471
731
  currentState.lifecycle.unregisterServer(name);
732
+ const guard = captureRuntimeGuard(currentState);
472
733
  await currentState.manager.close(name);
734
+ assertRuntimeGuard(guard);
473
735
  syncToolSurface();
474
- updateStatusBar(currentState);
736
+ (await loadForRuntime(loadCoreRuntime, guard)).updateStatusBar(currentState);
475
737
  },
476
738
  };
477
739
  };
@@ -539,75 +801,178 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
539
801
  type: "string",
540
802
  });
541
803
 
542
- function startInitialization(ctx: ExtensionContext, owner: McpRuntimeOwner, oauthRuntime: McpOAuthRuntime, generation: number, staleReason: string): Promise<void> {
543
- owner.addCleanup(() => cleanupMaterializedBinaryResources(owner.signal));
544
- const promise = initializeMcp(pi, ctx, owner, {
545
- ...(programmaticConfig || options.configPath !== undefined
546
- ? {
547
- ...(earlyConfigPath !== undefined ? { configPath: earlyConfigPath } : {}),
548
- ...(sessionConfig !== undefined ? { config: sessionConfig } : {}),
549
- }
550
- : {}),
551
- oauthRuntime,
804
+ function startInitialization(ctx: ExtensionContext, owner: McpRuntimeOwner, generation: number, staleReason: string): Promise<void> {
805
+ let oauthRuntime: McpOAuthRuntime | null = null;
806
+ let markStarted!: () => void;
807
+ initStartedPromise = new Promise<void>((resolve) => {
808
+ markStarted = resolve;
552
809
  });
553
- initPromise = promise;
554
-
555
- return promise.then(async (nextState) => {
810
+ let coreRuntime: Awaited<ReturnType<typeof loadCoreRuntime>> | null = null;
811
+ const initializationPromise = (async () => {
812
+ const guard = captureRuntimeGuard(null);
813
+ const [core, oauth] = await Promise.all([
814
+ loadCoreRuntime(),
815
+ loadOAuthRuntime(),
816
+ ]);
817
+ assertRuntimeGuard(guard);
818
+ if (generation !== guard.generation || guard.owner !== owner) {
819
+ throw owner.signal.reason ?? new Error("Stale MCP initialization");
820
+ }
821
+ coreRuntime = core;
822
+ assertRuntimeGuard(guard);
823
+ oauthRuntime = oauth.createOAuthRuntime(owner.signal);
824
+ try {
825
+ assertRuntimeGuard(guard);
826
+ } catch (error) {
827
+ await oauth.shutdownOAuth(oauthRuntime);
828
+ throw error;
829
+ }
830
+ currentOAuthRuntime = oauthRuntime;
831
+ assertRuntimeGuard(guard);
832
+ owner.addCleanup(async () => {
833
+ const { cleanupMaterializedBinaryResources } = await import("./tool-registrar.ts");
834
+ cleanupMaterializedBinaryResources(owner.signal);
835
+ });
836
+ assertRuntimeGuard(guard);
837
+ const initialization = core.initializeMcp(pi, ctx, owner, {
838
+ ...(programmaticConfig || options.configPath !== undefined
839
+ ? {
840
+ ...(earlyConfigPath !== undefined ? { configPath: earlyConfigPath } : {}),
841
+ ...(sessionConfig !== undefined ? { config: sessionConfig } : {}),
842
+ }
843
+ : {}),
844
+ oauthRuntime,
845
+ });
846
+ assertRuntimeGuard(guard);
847
+ markStarted();
848
+ return initialization;
849
+ })().catch((error) => {
850
+ markStarted();
851
+ throw error;
852
+ }).then((value) => value);
853
+ let promise: Promise<McpExtensionState>;
854
+ promise = initializationPromise.then(async (nextState) => {
556
855
  if (!owner.isActive() || generation !== lifecycleGeneration || initPromise !== promise) {
557
856
  try {
558
- await shutdownState(nextState, staleReason);
857
+ await shutdownState(nextState, staleReason, false);
559
858
  } catch (error) {
560
859
  console.error(`MCP: failed to clean stale initialization state: ${formatTerminalError(error)}`);
561
860
  }
562
- return;
861
+ throw owner.signal.reason ?? new Error("Stale MCP initialization completed");
563
862
  }
564
863
 
864
+ const guard = () => {
865
+ owner.throwIfInactive();
866
+ if (generation !== lifecycleGeneration || currentOwner !== owner || state !== nextState || initPromise !== promise) {
867
+ throw owner.signal.reason ?? new Error("Stale MCP initialization finalized");
868
+ }
869
+ };
870
+ const registeredDuringFinalization = new Set<string>();
871
+ let statusPublicationAttempted = false;
565
872
  state = nextState;
566
- clearRetainedInitFailure();
567
- for (const [name, { entry }] of runtimeServers) {
568
- if (Object.hasOwn(nextState.config.mcpServers, name)) {
569
- console.error(`MCP: runtime-registered server "${name}" now collides with a configured server; keeping the configured server`);
570
- continue;
873
+ finalizationGuard = guard;
874
+ finalizationRegistrations = registeredDuringFinalization;
875
+ try {
876
+ guard();
877
+ // Re-read after asynchronous startup so navigation during initialization
878
+ // cannot restore a stale branch.
879
+ callReentrant(() => restoreCurrentSessionApprovals(nextState));
880
+ for (const [name, { entry }] of runtimeServers) {
881
+ guard();
882
+ if (Object.hasOwn(nextState.config.mcpServers, name)) {
883
+ console.error(`MCP: runtime-registered server "${name}" now collides with a configured server; keeping the configured server`);
884
+ continue;
885
+ }
886
+ nextState.config.mcpServers[name] = entry;
887
+ guard();
888
+ callReentrant(() => attachRuntimeServerLifecycle(nextState, name, entry));
571
889
  }
572
- nextState.config.mcpServers[name] = entry;
573
- attachRuntimeServerLifecycle(nextState, name, entry);
574
- }
575
- nextState.onToolMetadataUpdated = (_serverName, _reason) => {
576
- if (state !== nextState || !owner.isActive()) return;
890
+ guard();
891
+ nextState.onToolMetadataUpdated = (_serverName, _reason) => {
892
+ if (state !== nextState || !owner.isActive()) return;
893
+ syncPromptCommands();
894
+ if (directToolsFrozen) {
895
+ logger.debug(`MCP: metadata update for ${_serverName} (${_reason}) skipped — directTools frozen`);
896
+ return;
897
+ }
898
+ syncToolSurface(ctx);
899
+ };
900
+ guard();
577
901
  syncPromptCommands();
578
- if (directToolsFrozen) {
579
- logger.debug(`MCP: metadata update for ${_serverName} (${_reason}) skipped — directTools frozen`);
580
- return;
581
- }
902
+ guard();
582
903
  syncToolSurface(ctx);
583
- };
584
- syncPromptCommands();
585
- syncToolSurface(ctx);
586
- // A connected snapshot is readiness-like external state. Publish it only
587
- // after Pi's model-facing direct-tool surface reflects live metadata.
588
- nextState.statusEvents = pi.events;
589
- updateStatusBar(nextState);
590
- initPromise = null;
591
- if (earlyConfig.settings?.freezeDirectTools === true) {
592
- directToolsFrozen = true;
593
- logger.info("MCP: direct tools frozen after initial sync — metadata can refresh without rebuilding the active tool surface");
904
+ guard();
905
+ // A connected snapshot is readiness-like external state. Publish it only
906
+ // after Pi's model-facing direct-tool surface reflects live metadata.
907
+ nextState.statusEvents = pi.events;
908
+ guard();
909
+ if (!coreRuntime) throw new Error("MCP core runtime was not loaded");
910
+ const loadedCoreRuntime = coreRuntime;
911
+ statusPublicationAttempted = true;
912
+ callReentrant(() => loadedCoreRuntime.updateStatusBar(nextState));
913
+ guard();
914
+ clearRetainedInitFailure();
915
+ guard();
916
+ if (earlyConfig.settings?.freezeDirectTools === true) {
917
+ directToolsFrozen = true;
918
+ logger.info("MCP: direct tools frozen after initial sync — metadata can refresh without rebuilding the active tool surface");
919
+ }
920
+ guard();
921
+ initPromise = null;
922
+ initStartedPromise = null;
923
+ return nextState;
924
+ } catch (error) {
925
+ if (state === nextState) state = null;
926
+ finalizationGuard = null;
927
+ finalizationRegistrations = null;
928
+ if (registeredDuringFinalization.size > 0) {
929
+ deactivateTools([...registeredDuringFinalization]);
930
+ for (const name of registeredDuringFinalization) {
931
+ registeredDirectTools.delete(name);
932
+ registeredDirectToolServers.delete(name);
933
+ registeredDirectToolVersions.delete(name);
934
+ registeredNamespaceProxyTools.delete(name);
935
+ }
936
+ if (registeredDuringFinalization.has("mcp")) {
937
+ proxyToolRegistered = false;
938
+ proxyToolDescription = null;
939
+ }
940
+ }
941
+ if (statusPublicationAttempted && generation === lifecycleGeneration && currentOwner === owner) {
942
+ publishMcpStatusShutdown(nextState.statusEvents);
943
+ }
944
+ // A synchronous lifecycle replacement captures published state and owns
945
+ // its cleanup. Only clean here when no replacement took ownership.
946
+ if (generation === lifecycleGeneration && currentOwner === owner) {
947
+ try {
948
+ await shutdownState(nextState, staleReason, false);
949
+ } catch (cleanupError) {
950
+ console.error(`MCP: failed to clean interrupted initialization state: ${formatTerminalError(cleanupError)}`);
951
+ }
952
+ }
953
+ throw error;
954
+ } finally {
955
+ if (finalizationGuard === guard) finalizationGuard = null;
956
+ if (finalizationRegistrations === registeredDuringFinalization) finalizationRegistrations = null;
594
957
  }
595
- }).catch(async err => {
958
+ });
959
+ initPromise = promise;
960
+
961
+ return promise.then(() => undefined).catch(async err => {
596
962
  if (!owner.isActive() || generation !== lifecycleGeneration) {
597
963
  return;
598
964
  }
599
- if (initPromise !== promise && initPromise !== null) {
600
- return;
601
- }
965
+ if (initPromise !== promise) return;
602
966
  const message = retainInitFailure(err);
603
967
  console.error(`MCP initialization failed: ${message}`);
604
968
  initPromise = null;
969
+ initStartedPromise = null;
605
970
  if (state) return;
606
971
 
607
972
  try {
608
973
  await Promise.all([
609
974
  owner.stop("MCP initialization failed"),
610
- shutdownOAuth(oauthRuntime),
975
+ oauthRuntime ? loadOAuthRuntime().then(({ shutdownOAuth }) => shutdownOAuth(oauthRuntime!)) : Promise.resolve(),
611
976
  ]);
612
977
  } catch (error) {
613
978
  console.error(`MCP: failed to clean rejected initialization: ${formatTerminalError(error)}`);
@@ -615,19 +980,26 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
615
980
  });
616
981
  }
617
982
 
983
+ function ensureSessionRuntime(ctx: ExtensionContext): Promise<McpExtensionState | null> {
984
+ if (state) return Promise.resolve(state);
985
+ if (initPromise) return initPromise;
986
+ const owner = currentOwner?.isActive() ? currentOwner : createMcpRuntimeOwner();
987
+ if (owner !== currentOwner) currentOwner = owner;
988
+ const generation = currentOwner === owner && lifecycleGeneration > 0
989
+ ? lifecycleGeneration
990
+ : ++lifecycleGeneration;
991
+ startInitialization(ctx, owner, generation, "stale_first_operation_initialization");
992
+ return initPromise!;
993
+ }
994
+
618
995
  function startLoadTimeInitialization(): void {
619
- const hasStartupServer = Object.values(earlyConfig.mcpServers).some((definition) => {
620
- if (definition.disabled === true) return false;
621
- return definition.lifecycle === "eager" || definition.lifecycle === "keep-alive";
622
- });
623
996
  if (!hasStartupServer) return;
624
997
  setImmediate(() => {
625
998
  if (lifecycleGeneration !== 0 || state || initPromise) return;
626
999
  const generation = ++lifecycleGeneration;
627
1000
  const owner = createMcpRuntimeOwner();
628
- const oauthRuntime = createOAuthRuntime(owner.signal);
629
1001
  currentOwner = owner;
630
- currentOAuthRuntime = oauthRuntime;
1002
+ currentOAuthRuntime = null;
631
1003
  startInitialization({
632
1004
  mode: "print",
633
1005
  hasUI: false,
@@ -635,21 +1007,29 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
635
1007
  model: undefined,
636
1008
  modelRegistry: undefined,
637
1009
  signal: undefined,
638
- } as unknown as ExtensionContext, owner, oauthRuntime, generation, "stale_load_time_initialization");
1010
+ } as unknown as ExtensionContext, owner, generation, "stale_load_time_initialization");
639
1011
  });
640
1012
  }
641
1013
 
1014
+ pi.on("resources_discover", (event) => {
1015
+ const resourceConfig = programmaticConfig
1016
+ ? cloneMcpConfig(sessionConfig)
1017
+ : loadMcpConfig(earlyConfigPath, event.cwd);
1018
+ const skillPaths = discoverConfiguredClaudePluginSkills(resourceConfig, event.cwd);
1019
+ return skillPaths.length > 0 ? { skillPaths } : undefined;
1020
+ });
1021
+
642
1022
  pi.on("session_start", async (_event, ctx) => {
643
1023
  const generation = ++lifecycleGeneration;
644
1024
  const previousState = state;
645
1025
  const previousOwner = currentOwner;
646
1026
  const previousOAuthRuntime = currentOAuthRuntime;
647
1027
  const owner = createMcpRuntimeOwner();
648
- const oauthRuntime = createOAuthRuntime(owner.signal);
649
1028
  currentOwner = owner;
650
- currentOAuthRuntime = oauthRuntime;
1029
+ currentOAuthRuntime = null;
651
1030
  state = null;
652
1031
  initPromise = null;
1032
+ initStartedPromise = null;
653
1033
  clearRetainedInitFailure();
654
1034
 
655
1035
  // Abort synchronously before awaiting cleanup so old callbacks and startup
@@ -659,15 +1039,22 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
659
1039
  await Promise.all([
660
1040
  stopPrevious,
661
1041
  shutdownState(previousState, "session_restart"),
662
- previousOAuthRuntime ? shutdownOAuth(previousOAuthRuntime) : Promise.resolve(),
1042
+ previousOAuthRuntime ? loadOAuthRuntime().then(({ shutdownOAuth }) => shutdownOAuth(previousOAuthRuntime)) : Promise.resolve(),
663
1043
  ]);
664
1044
  } catch (error) {
665
1045
  console.error(`MCP: failed to shut down previous session state: ${formatTerminalError(error)}`);
666
1046
  }
667
1047
 
668
1048
  if (generation !== lifecycleGeneration || !owner.isActive()) return;
1049
+ if (state) return;
1050
+
1051
+ if (!initPromise) {
1052
+ if (deferSessionRuntime) return;
1053
+ startInitialization(ctx, owner, generation, "stale_session_start");
1054
+ }
669
1055
 
670
- const initialization = startInitialization(ctx, owner, oauthRuntime, generation, "stale_session_start");
1056
+ const initialization = initPromise as Promise<McpExtensionState> | null;
1057
+ const initializationStarted = initStartedPromise as Promise<void> | null;
671
1058
  if (envRaw !== undefined && envRaw !== "__none__") {
672
1059
  const missingEnvDirectTools = getMissingConfiguredDirectToolServers(
673
1060
  earlyConfig,
@@ -675,9 +1062,27 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
675
1062
  envDirectToolOverride,
676
1063
  );
677
1064
  if (missingEnvDirectTools.length > 0) {
678
- await initialization;
1065
+ await initialization?.then(() => undefined, () => undefined);
1066
+ return;
679
1067
  }
680
1068
  }
1069
+ await initializationStarted;
1070
+ });
1071
+
1072
+ pi.on("session_tree", (_event, ctx) => {
1073
+ const currentState = state;
1074
+ const owner = currentOwner;
1075
+ if (!currentState || !owner?.isActive() || !currentState.sessionManager) return;
1076
+
1077
+ let sessionManager: ExtensionContext["sessionManager"] | undefined;
1078
+ try {
1079
+ sessionManager = ctx.sessionManager;
1080
+ } catch {
1081
+ return;
1082
+ }
1083
+ if (sessionManager !== currentState.sessionManager) return;
1084
+
1085
+ restoreCurrentSessionApprovals(currentState);
681
1086
  });
682
1087
 
683
1088
  pi.on("input", async () => {
@@ -712,6 +1117,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
712
1117
  currentOAuthRuntime = null;
713
1118
  state = null;
714
1119
  initPromise = null;
1120
+ initStartedPromise = null;
715
1121
  clearRetainedInitFailure();
716
1122
 
717
1123
  // Abort before awaiting cleanup so delayed initialization cannot touch stale
@@ -721,7 +1127,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
721
1127
  await Promise.all([
722
1128
  stopOwner,
723
1129
  shutdownState(currentState, "session_shutdown"),
724
- oauthRuntime ? shutdownOAuth(oauthRuntime) : Promise.resolve(),
1130
+ oauthRuntime ? loadOAuthRuntime().then(({ shutdownOAuth }) => shutdownOAuth(oauthRuntime)) : Promise.resolve(),
725
1131
  ]);
726
1132
  } catch (error) {
727
1133
  console.error(`MCP: session shutdown cleanup failed: ${formatTerminalError(error)}`);
@@ -755,9 +1161,9 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
755
1161
  if (
756
1162
  (subcommand !== "reconnect" && subcommand !== "logout" && subcommand !== "disable" && subcommand !== "enable" && subcommand !== "token")
757
1163
  || argumentPrefix === undefined
758
- || !state
759
1164
  ) return null;
760
1165
 
1166
+ const completionConfig = state?.config ?? earlyConfig;
761
1167
  if (subcommand === "token") {
762
1168
  const tokenMatch = argumentPrefix.trimStart().match(/^(set|remove|status)\s+(.*)$/);
763
1169
  if (!tokenMatch) {
@@ -768,35 +1174,27 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
768
1174
  }
769
1175
  const action = tokenMatch[1] ?? "";
770
1176
  const serverPrefix = tokenMatch[2] ?? "";
771
- const servers = Object.keys(state.config.mcpServers)
1177
+ const servers = Object.keys(completionConfig.mcpServers)
772
1178
  .filter(serverName => serverName.startsWith(serverPrefix.trimStart()))
773
1179
  .map(serverName => ({ value: `token ${action} ${serverName}`, label: serverName }));
774
1180
  return servers.length > 0 ? servers : null;
775
1181
  }
776
1182
 
777
- const servers = Object.keys(state.config.mcpServers)
1183
+ const servers = Object.keys(completionConfig.mcpServers)
778
1184
  .filter((serverName) => serverName.startsWith(argumentPrefix.trimStart()))
779
1185
  .map((serverName) => ({ value: `${subcommand} ${serverName}`, label: serverName }));
780
1186
  return servers.length > 0 ? servers : null;
781
1187
  },
782
1188
  handler: async (args, ctx) => {
783
- const commandOwner = currentOwner;
1189
+ let commandOwner = currentOwner;
784
1190
  const commandReload = typeof ctx.reload === "function" ? ctx.reload.bind(ctx) : async () => {};
785
- const commandHasUI = ctx.hasUI;
786
- const commandCtx = {
787
- hasUI: commandHasUI,
788
- ui: commandHasUI
789
- ? commandOwner ? createOwnedUi(ctx.ui, commandOwner) : ctx.ui
790
- : undefined,
791
- cwd: ctx.cwd,
792
- mode: ctx.mode,
793
- signal: commandOwner?.signal ?? ctx.signal,
794
- } as unknown as ExtensionContext;
795
- if (!state && initPromise) {
1191
+ let commandCtx = createCommandContext(ctx as unknown as ExtensionContext, commandOwner);
1192
+ if (!state) {
796
1193
  try {
797
- const initialized = await initPromise;
1194
+ state = await ensureSessionRuntime(ctx as unknown as ExtensionContext);
1195
+ commandOwner = currentOwner;
1196
+ commandCtx = createCommandContext(ctx as unknown as ExtensionContext, commandOwner);
798
1197
  commandOwner?.throwIfInactive();
799
- state = initialized;
800
1198
  } catch (error) {
801
1199
  const message = error instanceof Error ? error.message : String(error);
802
1200
  if (commandCtx.hasUI) commandCtx.ui?.notify(`MCP initialization failed: ${message}`, "error");
@@ -808,6 +1206,16 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
808
1206
  return;
809
1207
  }
810
1208
 
1209
+ const commandGuard = captureRuntimeGuard(state);
1210
+ let commands: Awaited<ReturnType<typeof loadCommands>>;
1211
+ try {
1212
+ commands = await loadForRuntime(loadCommands, commandGuard);
1213
+ } catch (error) {
1214
+ if (commandGuard.owner && isOwnerAbortError(error, commandGuard.owner)) return;
1215
+ const message = error instanceof Error ? error.message : String(error);
1216
+ if (commandCtx.hasUI) commandCtx.ui?.notify(`MCP initialization failed: ${message}`, "error");
1217
+ return;
1218
+ }
811
1219
  const parts = args?.trim()?.split(/\s+/) ?? [];
812
1220
  const subcommand = parts[0] ?? "";
813
1221
  const targetServer = parts[1];
@@ -816,13 +1224,13 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
816
1224
  switch (subcommand) {
817
1225
  case "reconnect":
818
1226
  commandOwner?.throwIfInactive();
819
- await reconnectServers(state, commandCtx, targetServer);
1227
+ await commands.reconnectServers(state, commandCtx, targetServer);
820
1228
  break;
821
1229
  case "tools":
822
- await showTools(state, commandCtx);
1230
+ await commands.showTools(state, commandCtx);
823
1231
  break;
824
1232
  case "prompts":
825
- await showPrompts(state, commandCtx);
1233
+ await commands.showPrompts(state, commandCtx);
826
1234
  break;
827
1235
  case "setup": {
828
1236
  commandOwner?.throwIfInactive();
@@ -830,7 +1238,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
830
1238
  commandCtx.ui?.notify("MCP setup is unavailable when config is supplied by createMcpAdapter().", "info");
831
1239
  break;
832
1240
  }
833
- const result = await openMcpSetup(state, pi, commandCtx, earlyConfigPath, "setup");
1241
+ const result = await commands.openMcpSetup(state, pi, commandCtx, earlyConfigPath, "setup");
834
1242
  if (result?.configChanged) {
835
1243
  commandOwner?.throwIfInactive();
836
1244
  await commandReload();
@@ -845,7 +1253,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
845
1253
  return;
846
1254
  }
847
1255
  commandOwner?.throwIfInactive();
848
- await logoutServer(serverName, state, commandCtx);
1256
+ await commands.logoutServer(serverName, state, commandCtx);
849
1257
  break;
850
1258
  }
851
1259
  case "token": {
@@ -860,7 +1268,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
860
1268
  return;
861
1269
  }
862
1270
  commandOwner?.throwIfInactive();
863
- await manageBearerToken(action, serverName, state, commandCtx);
1271
+ await commands.manageBearerToken(action, serverName, state, commandCtx);
864
1272
  break;
865
1273
  }
866
1274
  case "disable":
@@ -894,10 +1302,10 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
894
1302
  commandOwner?.throwIfInactive();
895
1303
  if (programmaticConfig) {
896
1304
  commandCtx.ui?.notify("MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.", "info");
897
- await showStatus(state, commandCtx);
1305
+ await commands.showStatus(state, commandCtx);
898
1306
  break;
899
1307
  }
900
- const result = await openMcpPanel(state, pi, commandCtx, earlyConfigPath, (changes) => {
1308
+ const result = await commands.openMcpPanel(state, pi, commandCtx, earlyConfigPath, (changes) => {
901
1309
  applyDirectToolConfigChanges(changes);
902
1310
  syncToolSurface(commandCtx);
903
1311
  });
@@ -907,7 +1315,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
907
1315
  return;
908
1316
  }
909
1317
  } else {
910
- await showStatus(state, commandCtx);
1318
+ await commands.showStatus(state, commandCtx);
911
1319
  }
912
1320
  break;
913
1321
  }
@@ -919,27 +1327,19 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
919
1327
  pi.registerCommand("mcp-auth", {
920
1328
  description: "Authenticate with an MCP server (OAuth)",
921
1329
  handler: async (args, ctx) => {
922
- const commandOwner = currentOwner;
923
- const commandHasUI = ctx.hasUI;
924
- const commandCtx = {
925
- hasUI: commandHasUI,
926
- ui: commandHasUI
927
- ? commandOwner ? createOwnedUi(ctx.ui, commandOwner) : ctx.ui
928
- : undefined,
929
- cwd: ctx.cwd,
930
- mode: ctx.mode,
931
- signal: commandOwner?.signal ?? ctx.signal,
932
- } as unknown as ExtensionContext;
1330
+ let commandOwner = currentOwner;
1331
+ let commandCtx = createCommandContext(ctx as unknown as ExtensionContext, commandOwner);
933
1332
  const serverName = args?.trim();
934
1333
  if (!serverName && !commandCtx.hasUI) {
935
1334
  return;
936
1335
  }
937
1336
 
938
- if (!state && initPromise) {
1337
+ if (!state) {
939
1338
  try {
940
- const initialized = await initPromise;
1339
+ state = await ensureSessionRuntime(ctx as unknown as ExtensionContext);
1340
+ commandOwner = currentOwner;
1341
+ commandCtx = createCommandContext(ctx as unknown as ExtensionContext, commandOwner);
941
1342
  commandOwner?.throwIfInactive();
942
- state = initialized;
943
1343
  } catch (error) {
944
1344
  const message = error instanceof Error ? error.message : String(error);
945
1345
  if (commandCtx.hasUI) commandCtx.ui?.notify(`MCP initialization failed: ${message}`, "error");
@@ -951,19 +1351,29 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
951
1351
  return;
952
1352
  }
953
1353
 
1354
+ const commandGuard = captureRuntimeGuard(state);
1355
+ let commands: Awaited<ReturnType<typeof loadCommands>>;
1356
+ try {
1357
+ commands = await loadForRuntime(loadCommands, commandGuard);
1358
+ } catch (error) {
1359
+ if (commandGuard.owner && isOwnerAbortError(error, commandGuard.owner)) return;
1360
+ const message = error instanceof Error ? error.message : String(error);
1361
+ if (commandCtx.hasUI) commandCtx.ui?.notify(`MCP initialization failed: ${message}`, "error");
1362
+ return;
1363
+ }
954
1364
  if (!serverName) {
955
1365
  if (programmaticConfig) {
956
1366
  commandCtx.ui?.notify("Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.", "info");
957
1367
  return;
958
1368
  }
959
- await openMcpAuthPanel(state, pi, commandCtx, earlyConfigPath);
1369
+ await commands.openMcpAuthPanel(state, pi, commandCtx, earlyConfigPath);
960
1370
  return;
961
1371
  }
962
1372
 
963
- const result = await authenticateServer(serverName, state.config, commandCtx, commandCtx.signal, state.oauthRuntime);
1373
+ const result = await commands.authenticateServer(serverName, state.config, commandCtx, commandCtx.signal, state.oauthRuntime);
964
1374
  if (result.ok) {
965
1375
  commandOwner?.throwIfInactive();
966
- await reconnectServer(state, commandCtx, serverName);
1376
+ await commands.reconnectServer(state, commandCtx, serverName);
967
1377
  }
968
1378
  },
969
1379
  });
@@ -980,17 +1390,18 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
980
1390
  }),
981
1391
  renderCall: createMcpScriptToolCallRenderer(toolRenderOptions),
982
1392
  renderResult: renderMcpToolResult,
983
- async execute(_toolCallId: string, params: { code: string; timeoutMs?: number }, signal: AbortSignal | undefined) {
984
- const executeOwner = currentOwner;
985
- if (!state && initPromise) {
1393
+ async execute(_toolCallId: string, params: { code: string; timeoutMs?: number }, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
1394
+ let executeOwner = currentOwner;
1395
+ if (!state) {
986
1396
  try {
987
- const initialized = await awaitWithTimeout(initPromise, INIT_WAIT_TIMEOUT_MS);
1397
+ const initialized = await awaitWithTimeout(ensureSessionRuntime(ctx), INIT_WAIT_TIMEOUT_MS);
988
1398
  if (initialized === INIT_WAIT_TIMED_OUT) {
989
1399
  return {
990
1400
  content: [{ type: "text" as const, text: "MCP initialization is still in progress. Try again shortly." }],
991
1401
  details: { mode: "script", error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
992
1402
  };
993
1403
  }
1404
+ executeOwner = currentOwner;
994
1405
  executeOwner?.throwIfInactive();
995
1406
  state = initialized;
996
1407
  } catch (error) {
@@ -1011,17 +1422,235 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1011
1422
  };
1012
1423
  }
1013
1424
  executeOwner?.throwIfInactive();
1014
- return runMcpScript(state, params.code, params.timeoutMs, getPiTools, signal);
1425
+ const scriptState = state;
1426
+ const scriptGuard = captureRuntimeGuard(scriptState);
1427
+ const codeMode = await loadForRuntime(loadCodeMode, scriptGuard);
1428
+ return codeMode.runMcpScript(scriptState, params.code, params.timeoutMs, getPiTools, signal);
1015
1429
  },
1016
1430
  });
1017
1431
  }
1018
1432
 
1433
+ async function connectAndReport(targetState: McpExtensionState, serverName: string, signal?: AbortSignal, ctx?: ExtensionContext) {
1434
+ const guard = captureRuntimeGuard(targetState);
1435
+ const directToolsBefore = new Map([...registeredDirectTools.keys()].map((name) => [name, registeredDirectToolVersions.get(name) ?? 0]));
1436
+ const proxyModes = await loadForRuntime(loadProxyModes, guard);
1437
+ const result = await proxyModes.executeConnect(targetState, serverName, signal);
1438
+ assertRuntimeGuard(guard);
1439
+ if (!directToolsFrozen) syncToolSurface(ctx);
1440
+ const reportedNames = reportedDirectToolNamesByServer.get(serverName) ?? new Set<string>();
1441
+ // Attribute only this server's new definitions; search-mode tools load on search, not connect.
1442
+ const addedToolNames = [...registeredDirectTools.keys()].filter(
1443
+ (name) => (!directToolsBefore.has(name) || (registeredDirectToolVersions.get(name) ?? 0) !== directToolsBefore.get(name))
1444
+ && registeredDirectToolServers.get(name) === serverName
1445
+ && !reportedNames.has(name)
1446
+ && !lazyDirectTools.has(name),
1447
+ );
1448
+ if (addedToolNames.length === 0) return result;
1449
+ for (const name of addedToolNames) reportedNames.add(name);
1450
+ reportedDirectToolNamesByServer.set(serverName, reportedNames);
1451
+ return { ...result, addedToolNames };
1452
+ }
1453
+
1454
+ async function executeInstall(
1455
+ targetState: McpExtensionState,
1456
+ rawUrl: string | undefined,
1457
+ requestedName: string | undefined,
1458
+ target: string | undefined,
1459
+ cwd: string,
1460
+ signal?: AbortSignal,
1461
+ ) {
1462
+ const installOwner = currentOwner;
1463
+ signal?.throwIfAborted();
1464
+ installOwner?.throwIfInactive();
1465
+ if (programmaticConfig) {
1466
+ return {
1467
+ content: [{ type: "text" as const, text: "MCP install is unavailable when the adapter uses programmatic configuration." }],
1468
+ details: { mode: "install", error: "programmatic_config" },
1469
+ };
1470
+ }
1471
+ if (target !== undefined && target !== "global" && target !== "project") {
1472
+ return {
1473
+ content: [{ type: "text" as const, text: "MCP install target must be 'global' or 'project'." }],
1474
+ details: { mode: "install", error: "invalid_target" },
1475
+ };
1476
+ }
1477
+
1478
+ const destination = target === "project" ? getProjectConfigPath(cwd) : getPiGlobalConfigPath(earlyConfigPath);
1479
+ if (target === "project" && process.env.PI_MCP_CONFIG_MODE?.trim().toLowerCase() === "exclusive"
1480
+ && resolve(destination) !== resolve(getPiGlobalConfigPath(earlyConfigPath))) {
1481
+ return {
1482
+ content: [{ type: "text" as const, text: "Project installation is unavailable in exclusive config mode; use the global target." }],
1483
+ details: { mode: "install", error: "inactive_target" },
1484
+ };
1485
+ }
1486
+
1487
+ const installGuard = captureRuntimeGuard(targetState);
1488
+ const installParsing = await loadForRuntime(loadInstallParsing, installGuard);
1489
+ let normalized: ReturnType<typeof installParsing.normalizeMcpInstallRequest>;
1490
+ try {
1491
+ normalized = installParsing.normalizeMcpInstallRequest({
1492
+ url: rawUrl ?? "",
1493
+ ...(requestedName !== undefined ? { serverName: requestedName } : {}),
1494
+ });
1495
+ } catch (error) {
1496
+ const message = error instanceof Error ? error.message : String(error);
1497
+ return {
1498
+ content: [{ type: "text" as const, text: `Failed to install MCP server: ${message}` }],
1499
+ details: { mode: "install", error: "invalid_request", message },
1500
+ };
1501
+ }
1502
+
1503
+ const matchingUrl = Object.entries(targetState.config.mcpServers).find(([, definition]) =>
1504
+ definition.url !== undefined && installParsing.canonicalMcpServerUrl(definition.url) === normalized.url,
1505
+ );
1506
+ const serverName = requestedName?.trim() || matchingUrl?.[0] || normalized.serverName;
1507
+ const existing = targetState.config.mcpServers[serverName];
1508
+ if (existing && installParsing.canonicalMcpServerUrl(existing.url ?? "") !== normalized.url) {
1509
+ return {
1510
+ content: [{ type: "text" as const, text: `MCP server name "${serverName}" is already configured with a different endpoint.` }],
1511
+ details: { mode: "install", error: "name_conflict", server: serverName, url: normalized.url },
1512
+ };
1513
+ }
1514
+
1515
+ if (targetState.provisionalInstalls?.has(serverName)) {
1516
+ return {
1517
+ content: [{ type: "text" as const, text: `MCP server "${serverName}" is still being installed. Retry after that installation finishes.` }],
1518
+ details: { mode: "install", error: "install_in_progress", server: serverName },
1519
+ };
1520
+ }
1521
+ if (runtimeServers.has(serverName)) {
1522
+ return {
1523
+ content: [{ type: "text" as const, text: `Runtime MCP server "${serverName}" cannot be promoted by URL install. Save its full definition manually.` }],
1524
+ details: { mode: "install", error: "runtime_promotion_unsupported", server: serverName },
1525
+ };
1526
+ }
1527
+
1528
+ const persistedEntry: ServerEntry = { url: normalized.url };
1529
+ const runtimeEntry: ServerEntry = existing ?? { ...persistedEntry, directTools: false };
1530
+ const provisional = existing === undefined;
1531
+ const restoreMetadata = [targetState.toolMetadata, targetState.promptMetadata, targetState.serverInstructions,
1532
+ targetState.resourceCounts, targetState.directToolCounts].map((map) => {
1533
+ const previous = map.get(serverName);
1534
+ return () => {
1535
+ if (previous === undefined) map.delete(serverName);
1536
+ else (map as Map<string, unknown>).set(serverName, previous);
1537
+ };
1538
+ });
1539
+ const hadLivePrompts = targetState.promptMetadataLive.has(serverName);
1540
+ const rollback = async (): Promise<void> => {
1541
+ if (!provisional || targetState.config.mcpServers[serverName] !== runtimeEntry) return;
1542
+ delete targetState.config.mcpServers[serverName];
1543
+ targetState.lifecycle.unregisterServer(serverName);
1544
+ try {
1545
+ await targetState.manager.close(serverName);
1546
+ } finally {
1547
+ targetState.provisionalInstalls?.delete(serverName);
1548
+ for (const restore of restoreMetadata) restore();
1549
+ if (!hadLivePrompts) targetState.promptMetadataLive.delete(serverName);
1550
+ const core = await loadForRuntime(loadCoreRuntime, installGuard);
1551
+ core.clearFailure(targetState, serverName, "install-rollback");
1552
+ syncToolSurface();
1553
+ core.updateStatusBar(targetState);
1554
+ }
1555
+ };
1556
+
1557
+ let connectResult: Awaited<ReturnType<typeof connectAndReport>>;
1558
+ try {
1559
+ if (provisional) {
1560
+ (targetState.provisionalInstalls ??= new Set()).add(serverName);
1561
+ targetState.config.mcpServers[serverName] = runtimeEntry;
1562
+ attachRuntimeServerLifecycle(targetState, serverName, runtimeEntry);
1563
+ syncToolSurface();
1564
+ (await loadForRuntime(loadCoreRuntime, installGuard)).updateStatusBar(targetState);
1565
+ }
1566
+ connectResult = await connectAndReport(targetState, serverName, signal);
1567
+ signal?.throwIfAborted();
1568
+ installOwner?.throwIfInactive();
1569
+ } catch (error) {
1570
+ await rollback();
1571
+ throw error;
1572
+ }
1573
+ const connectError = connectResult.details?.error;
1574
+ const connectText = connectResult.content.find((content) => content.type === "text")?.text;
1575
+ if (connectError && connectError !== "auth_required") {
1576
+ await rollback();
1577
+ return {
1578
+ ...connectResult,
1579
+ content: [{ type: "text" as const, text: `MCP installation validation failed for "${serverName}". ${connectText ?? "Connection failed."}` }],
1580
+ details: { mode: "install", error: "validation_failed", server: serverName, url: normalized.url },
1581
+ };
1582
+ }
1583
+
1584
+ if (provisional) {
1585
+ try {
1586
+ await withFileMutationQueue(destination, async () => {
1587
+ signal?.throwIfAborted();
1588
+ installOwner?.throwIfInactive();
1589
+ writeSharedServerEntry(destination, serverName, persistedEntry);
1590
+ });
1591
+ } catch (error) {
1592
+ await rollback();
1593
+ const message = error instanceof Error ? error.message : String(error);
1594
+ return {
1595
+ content: [{ type: "text" as const, text: `MCP server validation succeeded, but configuration could not be saved: ${message}` }],
1596
+ details: { mode: "install", error: "persistence_failed", server: serverName, url: normalized.url, message },
1597
+ };
1598
+ }
1599
+ targetState.provisionalInstalls?.delete(serverName);
1600
+ try {
1601
+ (await loadForRuntime(loadCoreRuntime, installGuard)).updateMetadataCache(targetState, serverName);
1602
+ assertRuntimeGuard(installGuard);
1603
+ } catch (error) {
1604
+ if (isRuntimeGuardStale(installGuard) || isAbortError(error, installGuard.owner?.signal)) throw error;
1605
+ logger.warn(`MCP: installed "${serverName}" but could not cache metadata: ${error}`);
1606
+ }
1607
+ syncPromptCommands();
1608
+ }
1609
+
1610
+ if (connectError === "auth_required") {
1611
+ const proxyModes = await loadForRuntime(loadProxyModes, installGuard);
1612
+ const authResult = await proxyModes.executeAuthStart(targetState, serverName, signal);
1613
+ const authError = authResult.details?.error;
1614
+ const authText = authResult.content.find((content) => content.type === "text")?.text;
1615
+ return {
1616
+ ...connectResult,
1617
+ content: [{
1618
+ type: "text" as const,
1619
+ text: `${provisional ? "Installed" : "Found"} MCP server "${serverName}" at ${normalized.url}.\n\n${authText ?? "OAuth authorization is required."}`,
1620
+ }],
1621
+ details: {
1622
+ mode: "install",
1623
+ status: authError ? "auth_start_failed" : "awaiting_auth",
1624
+ server: serverName,
1625
+ url: normalized.url,
1626
+ ...(provisional ? { path: destination } : {}),
1627
+ ...(authError ? { error: authError } : {}),
1628
+ },
1629
+ };
1630
+ }
1631
+
1632
+ return {
1633
+ ...connectResult,
1634
+ content: [{
1635
+ type: "text" as const,
1636
+ text: `${provisional ? "Installed and connected" : "Already installed; connected"} MCP server "${serverName}" at ${normalized.url}.\n\n${connectText ?? ""}`.trim(),
1637
+ }],
1638
+ details: {
1639
+ mode: "install",
1640
+ status: "connected",
1641
+ server: serverName,
1642
+ url: normalized.url,
1643
+ ...(provisional ? { path: destination } : {}),
1644
+ },
1645
+ };
1646
+ }
1647
+
1019
1648
  function registerProxyTool(description: string): void {
1020
- (pi.registerTool as (tool: unknown) => unknown)({
1649
+ callReentrant(() => (pi.registerTool as (tool: unknown) => unknown)({
1021
1650
  name: "mcp",
1022
1651
  label: "MCP",
1023
1652
  description,
1024
- promptSnippet: "MCP gateway — status, search, describe, auth, and single MCP tool calls",
1653
+ promptSnippet: "MCP gateway — install by URL, status, search, describe, auth, and single MCP tool calls",
1025
1654
  renderShell: toolRenderShell,
1026
1655
  renderCall: createMcpProxyToolCallRenderer(toolRenderOptions),
1027
1656
  parameters: Type.Object({
@@ -1041,8 +1670,10 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1041
1670
  includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
1042
1671
  limit: optionalNumber({ minimum: 1, description: "Maximum search results to return (default: 12)" }),
1043
1672
  offset: optionalNumber({ minimum: 0, description: "Search result offset (default: 0)" }),
1044
- server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
1045
- action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })),
1673
+ server: Type.Optional(Type.String({ description: "Server name (filters/disambiguates calls and optionally names an install)" })),
1674
+ action: Type.Optional(Type.String({ description: "Action: 'install', 'ui-messages', 'auth-start', or 'auth-complete'" })),
1675
+ url: Type.Optional(Type.String({ description: "MCP endpoint URL for action: 'install'" })),
1676
+ target: Type.Optional(Type.String({ description: "Install target: 'global' (default) or 'project'" })),
1046
1677
  }),
1047
1678
  renderResult: renderMcpToolResult,
1048
1679
  async execute(_toolCallId: string, params: {
@@ -1058,6 +1689,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1058
1689
  offset?: number;
1059
1690
  server?: string;
1060
1691
  action?: string;
1692
+ url?: string;
1693
+ target?: string;
1061
1694
  }, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, _ctx: ExtensionContext) {
1062
1695
  let executeOwner = currentOwner;
1063
1696
  const parseArgs = (value: string | Record<string, unknown> | undefined): Record<string, unknown> | undefined => {
@@ -1090,13 +1723,16 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1090
1723
  || value.instructions !== undefined
1091
1724
  || value.search !== undefined
1092
1725
  || value.server !== undefined
1093
- || value.action !== undefined;
1726
+ || value.action !== undefined
1727
+ || value.url !== undefined
1728
+ || value.target !== undefined;
1094
1729
  if (!hasGatewayMode(params) && params.args !== undefined) {
1095
1730
  throw new Error("Gateway params were nested inside `args`; pass them top-level (for example, mcp({ search: \"...\" }) or mcp({ tool: \"...\", args: {} })).");
1096
1731
  }
1097
1732
 
1098
- if (!state && !initPromise && retainedInitFailure) {
1099
- startGatewayRetryInitialization(_ctx);
1733
+ if (!state && !initPromise) {
1734
+ if (retainedInitFailure) startGatewayRetryInitialization(_ctx);
1735
+ else void ensureSessionRuntime(_ctx);
1100
1736
  executeOwner = currentOwner;
1101
1737
  }
1102
1738
 
@@ -1129,9 +1765,15 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1129
1765
  };
1130
1766
  }
1131
1767
  executeOwner?.throwIfInactive();
1768
+ const proxyState = state;
1769
+ const proxyGuard = captureRuntimeGuard(proxyState);
1770
+ const proxyModes = await loadForRuntime(loadProxyModes, proxyGuard);
1132
1771
 
1772
+ if (params.action === "install") {
1773
+ return executeInstall(proxyState, params.url, params.server, params.target, _ctx.cwd, signal);
1774
+ }
1133
1775
  if (params.action === "ui-messages") {
1134
- return executeUiMessages(state);
1776
+ return proxyModes.executeUiMessages(proxyState);
1135
1777
  }
1136
1778
  if (params.action === "auth-start") {
1137
1779
  if (!params.server) {
@@ -1141,8 +1783,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1141
1783
  };
1142
1784
  }
1143
1785
  return signal
1144
- ? executeAuthStart(state, params.server, signal)
1145
- : executeAuthStart(state, params.server);
1786
+ ? proxyModes.executeAuthStart(proxyState, params.server, signal)
1787
+ : proxyModes.executeAuthStart(proxyState, params.server);
1146
1788
  }
1147
1789
  if (params.action === "auth-complete") {
1148
1790
  if (!params.server) {
@@ -1159,32 +1801,42 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1159
1801
  };
1160
1802
  }
1161
1803
  return signal
1162
- ? executeAuthComplete(state, params.server, input, signal)
1163
- : executeAuthComplete(state, params.server, input);
1804
+ ? proxyModes.executeAuthComplete(proxyState, params.server, input, signal)
1805
+ : proxyModes.executeAuthComplete(proxyState, params.server, input);
1164
1806
  }
1165
1807
  if (params.tool) {
1166
- return executeCall(state, params.tool, parsedArgs, params.server, getPiTools, signal);
1808
+ return proxyModes.executeCall(proxyState, params.tool, parsedArgs, params.server, getPiTools, signal);
1167
1809
  }
1168
1810
  if (params.connect) {
1169
- const result = await executeConnect(state, params.connect, signal);
1170
- if (!directToolsFrozen) syncToolSurface(_ctx as ExtensionContext);
1171
- return result;
1811
+ return connectAndReport(proxyState, params.connect, signal, _ctx as ExtensionContext);
1172
1812
  }
1173
1813
  if (params.describe) {
1174
- return executeDescribe(state, params.describe);
1814
+ return proxyModes.executeDescribe(proxyState, params.describe);
1175
1815
  }
1176
1816
  if (params.instructions) {
1177
- return executeInstructions(state, params.instructions);
1817
+ return proxyModes.executeInstructions(proxyState, params.instructions);
1178
1818
  }
1179
1819
  if (params.search !== undefined) {
1180
- return executeSearch(state, params.search, params.regex, params.server, params.includeSchemas, params.limit, params.offset);
1820
+ const result = proxyModes.executeSearch(proxyState, params.search, params.regex, params.server, params.includeSchemas, params.limit, params.offset);
1821
+ if (lazyDirectTools.size === 0) return result;
1822
+ holdLazyToolsInactive();
1823
+ const matches = (result.details as { matches?: Array<{ server: string; tool: string }> } | undefined)?.matches ?? [];
1824
+ const added = activateSearchMatches(matches);
1825
+ if (added.length === 0) return result;
1826
+ const text = result.content.map((block) => ("text" in block ? block.text : "")).join("\n");
1827
+ return {
1828
+ ...result,
1829
+ content: [{ type: "text" as const, text: `Activated as direct tools: ${added.join(", ")}.\n\n${text}` }],
1830
+ details: { ...(result.details ?? {}), activated: added },
1831
+ addedToolNames: added,
1832
+ };
1181
1833
  }
1182
1834
  if (params.server) {
1183
- return executeList(state, params.server);
1835
+ return proxyModes.executeList(proxyState, params.server);
1184
1836
  }
1185
- return executeStatus(state);
1837
+ return proxyModes.executeStatus(proxyState);
1186
1838
  },
1187
- });
1839
+ }));
1188
1840
  proxyToolRegistered = true;
1189
1841
  proxyToolDescription = description;
1190
1842
  }
@@ -1195,20 +1847,27 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1195
1847
  cache,
1196
1848
  envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride,
1197
1849
  );
1850
+ // Search-mode tools are registered INACTIVE and `mcp({ search })` is their only
1851
+ // activation entry point, so dropping the gateway strands them: every tool held, nothing
1852
+ // able to activate one. Keep it whenever any spec depends on it.
1853
+ const hasSearchModeSpecs = directSpecs.some((spec) => spec.lazy === true);
1198
1854
  const shouldRegisterProxyTool =
1199
1855
  config.settings?.disableProxyTool !== true
1200
1856
  || directSpecs.length === 0
1857
+ || hasSearchModeSpecs
1201
1858
  || missingConfiguredDirectToolServers.length > 0;
1202
1859
 
1203
1860
  if (shouldRegisterProxyTool) {
1204
1861
  const description = buildProxyDescription(config);
1205
1862
  if (!proxyToolRegistered || proxyToolDescription !== description) {
1863
+ finalizationRegistrations?.add("mcp");
1206
1864
  registerProxyTool(description);
1865
+ finalizationGuard?.();
1207
1866
  return;
1208
1867
  }
1209
1868
  const activeTools = getActiveToolsIfReady();
1210
1869
  if (activeTools && !activeTools.includes("mcp")) {
1211
- pi.setActiveTools([...activeTools, "mcp"]);
1870
+ callReentrant(() => pi.setActiveTools([...activeTools, "mcp"]));
1212
1871
  }
1213
1872
  return;
1214
1873
  }
@@ -1233,7 +1892,11 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1233
1892
  }
1234
1893
 
1235
1894
  export function createMcpAdapter(options: McpAdapterOptions = {}) {
1236
- const factoryConfig = options.config !== undefined ? cloneMcpConfig(options.config) : undefined;
1895
+ // Snapshot programmatic plugin roots at the API boundary so early and
1896
+ // session-scoped loading cannot resolve the same relative path differently.
1897
+ const factoryConfig = options.config !== undefined
1898
+ ? normalizeProgrammaticConfig(cloneMcpConfig(options.config))
1899
+ : undefined;
1237
1900
  return function mcpAdapter(pi: ExtensionAPI) {
1238
1901
  installMcpAdapter(pi, {
1239
1902
  ...(options.configPath !== undefined ? { configPath: options.configPath } : {}),