@humain/terminal 0.1.1 → 0.2.2

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