@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.
- package/CHANGELOG.md +22 -0
- package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
- package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
- package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-ADQYVZ6A.js} +483 -506
- package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
- package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
- package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
- package/dist/bundle/chunks/chunk-NMPIM7MN.js +2 -0
- package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
- package/dist/bundle/chunks/compat-CN7WN6DA.js +2 -0
- package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
- package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
- package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
- package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
- package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
- package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
- package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
- package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
- package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/model-runtime.d.ts +21 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +34 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +21 -8
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/subagent-env.d.ts +20 -0
- package/dist/core/tools/subagent-env.d.ts.map +1 -0
- package/dist/core/tools/subagent-env.js +47 -0
- package/dist/core/tools/subagent-env.js.map +1 -0
- package/dist/core/tools/subagent-scheduler.d.ts +36 -0
- package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
- package/dist/core/tools/subagent-scheduler.js +213 -0
- package/dist/core/tools/subagent-scheduler.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +83 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +215 -76
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/dist/humain/mcp-adapter/vendor/README.md +125 -19
- package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
- package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
- package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
- package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
- package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
- package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
- package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
- package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
- package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
- package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
- package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
- package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
- package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
- package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
- package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
- package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
- package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
- package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
- package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
- package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
- package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
- package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
- package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
- package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
- package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
- package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
- package/dist/humain/mcp-adapter/vendor/package.json +13 -3
- package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
- package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
- package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
- package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
- package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
- package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
- package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
- package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
- package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/abortable.js +22 -0
- package/dist/humain/web-access/vendor/abortable.js.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
- package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
- package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/credential-source.js +1 -0
- package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +20 -2
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
- package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/curator-run.js +38 -0
- package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +58 -11
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +12 -5
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +23 -19
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.js +40 -14
- package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
- package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/page-query.js +6 -2
- package/dist/humain/web-access/vendor/page-query.js.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
- package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
- package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
- package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/serpapi.js +215 -0
- package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
- package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
- package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/storage.js +9 -0
- package/dist/humain/web-access/vendor/storage.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +35 -24
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/utils.js +5 -3
- package/dist/humain/web-access/vendor/utils.js.map +1 -1
- package/dist/modes/json-event.d.ts +8 -0
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -0
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/environment-variables.md +9 -0
- package/docs/json.md +16 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
- package/src/humain/mcp-adapter/upstream.json +6 -4
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/src/humain/web-access/UPSTREAM.md +18 -8
- package/src/humain/web-access/upstream.json +6 -5
- package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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 {
|
|
8
|
-
import {
|
|
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 =
|
|
253
|
+
currentOAuthRuntime = null;
|
|
173
254
|
state = null;
|
|
174
|
-
startInitialization(ctx, owner,
|
|
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
|
-
(
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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?.
|
|
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
|
-
|
|
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,
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
579
|
-
logger.debug(`MCP: metadata update for ${_serverName} (${_reason}) skipped — directTools frozen`);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
903
|
+
guard();
|
|
582
904
|
syncToolSurface(ctx);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
})
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1190
|
+
let commandOwner = currentOwner;
|
|
784
1191
|
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) {
|
|
1192
|
+
let commandCtx = createCommandContext(ctx as unknown as ExtensionContext, commandOwner);
|
|
1193
|
+
if (!state) {
|
|
796
1194
|
try {
|
|
797
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
|
1338
|
+
if (!state) {
|
|
939
1339
|
try {
|
|
940
|
-
|
|
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
|
-
|
|
985
|
-
if (!state
|
|
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(
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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(
|
|
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(
|
|
1145
|
-
: executeAuthStart(
|
|
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(
|
|
1163
|
-
: executeAuthComplete(
|
|
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(
|
|
1809
|
+
return proxyModes.executeCall(proxyState, params.tool, parsedArgs, params.server, getPiTools, signal);
|
|
1167
1810
|
}
|
|
1168
1811
|
if (params.connect) {
|
|
1169
|
-
|
|
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(
|
|
1815
|
+
return proxyModes.executeDescribe(proxyState, params.describe);
|
|
1175
1816
|
}
|
|
1176
1817
|
if (params.instructions) {
|
|
1177
|
-
return executeInstructions(
|
|
1818
|
+
return proxyModes.executeInstructions(proxyState, params.instructions);
|
|
1178
1819
|
}
|
|
1179
1820
|
if (params.search !== undefined) {
|
|
1180
|
-
|
|
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(
|
|
1836
|
+
return proxyModes.executeList(proxyState, params.server);
|
|
1184
1837
|
}
|
|
1185
|
-
return executeStatus(
|
|
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
|
-
|
|
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 } : {}),
|