@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.
- package/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
- package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +88 -19
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
- package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
- package/node_modules/@tifan/pi-copy-response/README.md +2 -2
- package/node_modules/@tifan/pi-copy-response/package.json +3 -2
- package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
- package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
- package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
- package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
- package/node_modules/@tifan/pi-recap/package.json +3 -2
- package/node_modules/pi-antigravity/LICENSE +21 -0
- package/node_modules/pi-antigravity/README.md +194 -0
- package/node_modules/pi-antigravity/package.json +67 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
- package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
- package/node_modules/pi-antigravity/src/client/client.ts +561 -0
- package/node_modules/pi-antigravity/src/client/index.ts +1 -0
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/node_modules/pi-antigravity/src/image/image.ts +336 -0
- package/node_modules/pi-antigravity/src/image/index.ts +1 -0
- package/node_modules/pi-antigravity/src/index.ts +280 -0
- package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
- package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
- package/node_modules/pi-antigravity/src/models/index.ts +3 -0
- package/node_modules/pi-antigravity/src/models/models.ts +500 -0
- package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
- package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
- package/node_modules/pi-antigravity/src/types/index.ts +2 -0
- package/node_modules/pi-antigravity/src/types/types.ts +292 -0
- package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
- package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
- package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
- package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
- package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
- package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
- package/node_modules/pi-antigravity/tsconfig.json +21 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +125 -19
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
- package/node_modules/pi-mcp-adapter/commands.ts +13 -15
- package/node_modules/pi-mcp-adapter/config.ts +198 -30
- package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
- package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
- package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
- package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
- package/node_modules/pi-mcp-adapter/index.ts +827 -164
- package/node_modules/pi-mcp-adapter/init.ts +48 -10
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
- package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
- package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
- package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
- package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
- package/node_modules/pi-mcp-adapter/package.json +13 -3
- package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
- package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
- package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
- package/node_modules/pi-mcp-adapter/state.ts +10 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
- package/node_modules/pi-mcp-adapter/types.ts +57 -4
- package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
- package/node_modules/pi-mcp-adapter/utils.ts +64 -6
- package/package.json +15 -15
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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 {
|
|
8
|
-
import {
|
|
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 =
|
|
252
|
+
currentOAuthRuntime = null;
|
|
173
253
|
state = null;
|
|
174
|
-
startInitialization(ctx, owner,
|
|
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
|
-
(
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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?.
|
|
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
|
-
|
|
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,
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
579
|
-
logger.debug(`MCP: metadata update for ${_serverName} (${_reason}) skipped — directTools frozen`);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
902
|
+
guard();
|
|
582
903
|
syncToolSurface(ctx);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
})
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1189
|
+
let commandOwner = currentOwner;
|
|
784
1190
|
const commandReload = typeof ctx.reload === "function" ? ctx.reload.bind(ctx) : async () => {};
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
923
|
-
|
|
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
|
|
1337
|
+
if (!state) {
|
|
939
1338
|
try {
|
|
940
|
-
|
|
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
|
-
|
|
985
|
-
if (!state
|
|
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(
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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(
|
|
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(
|
|
1145
|
-
: executeAuthStart(
|
|
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(
|
|
1163
|
-
: executeAuthComplete(
|
|
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(
|
|
1808
|
+
return proxyModes.executeCall(proxyState, params.tool, parsedArgs, params.server, getPiTools, signal);
|
|
1167
1809
|
}
|
|
1168
1810
|
if (params.connect) {
|
|
1169
|
-
|
|
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(
|
|
1814
|
+
return proxyModes.executeDescribe(proxyState, params.describe);
|
|
1175
1815
|
}
|
|
1176
1816
|
if (params.instructions) {
|
|
1177
|
-
return executeInstructions(
|
|
1817
|
+
return proxyModes.executeInstructions(proxyState, params.instructions);
|
|
1178
1818
|
}
|
|
1179
1819
|
if (params.search !== undefined) {
|
|
1180
|
-
|
|
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(
|
|
1835
|
+
return proxyModes.executeList(proxyState, params.server);
|
|
1184
1836
|
}
|
|
1185
|
-
return executeStatus(
|
|
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
|
-
|
|
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 } : {}),
|