import type { ExtensionAPI, ExtensionContext, ToolInfo } from '@earendil-works/pi-coding-agent'; import type { McpExtensionState } from './_state.ts'; import type { DirectToolSpec, McpAdapterOptions, McpConfig, PromptMetadata } from './_types.ts'; import type { McpOAuthRuntime } from './_mcp-auth-flow.ts'; import { Type } from 'typebox'; import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup, } from './_commands.ts'; import { cloneMcpConfig, loadMcpConfig, writeProjectServerDisabledOverride } from './_config.ts'; import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools, } from './_direct-tools.ts'; import { flushMetadataCache, initializeMcp, updateStatusBar } from './_init.ts'; import { loadMetadataCache, type MetadataCache } from './_metadata-cache.ts'; import { createPromptCommand, resolveCachedPrompts } from './_prompts.ts'; import { logger } from './_logger.ts'; import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages, } from './_proxy-modes.ts'; import { formatTerminalError, getConfigPathFromArgv, normalizeDirectToolInputSchema, truncateAtWord, } from './_utils.ts'; import { createOAuthRuntime, shutdownOAuth } from './_mcp-auth-flow.ts'; import { createMcpDirectToolCallRenderer, renderMcpProxyToolCall, renderMcpToolResult, } from './_tool-result-renderer.ts'; import { toolErrorOverride } from './_error-signal.ts'; import { createMcpRuntimeOwner, createOwnedUi, isAbortError, type McpRuntimeOwner } from './_runtime-owner.ts'; import { publishMcpStatusShutdown } from './_mcp-status.ts'; export type { McpAdapterOptions } from './_types.ts'; export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, type McpServerRuntimeStatus, type McpServerStatusSnapshot, type McpStatusSnapshot, } from './_types.ts'; const INIT_WAIT_TIMEOUT_MS = 30_000; const INIT_WAIT_TIMED_OUT: unique symbol = Symbol('init-wait-timed-out'); async function awaitWithTimeout(promise: Promise, timeoutMs: number): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ promise, new Promise((resolve) => { timer = setTimeout(() => resolve(INIT_WAIT_TIMED_OUT), timeoutMs); timer.unref?.(); }), ]); } finally { clearTimeout(timer); } } function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) { const sessionConfig = options.config !== undefined ? cloneMcpConfig(options.config) : undefined; const programmaticConfig = sessionConfig !== undefined; let state: McpExtensionState | null = null; let initPromise: Promise | null = null; let currentOwner: McpRuntimeOwner | null = null; let currentOAuthRuntime: McpOAuthRuntime | null = null; let lifecycleGeneration = 0; async function shutdownState(currentState: McpExtensionState | null, reason: string): Promise { if (!currentState) { publishMcpStatusShutdown(pi.events); return; } publishMcpStatusShutdown(currentState.statusEvents); if (currentState.uiServer) { currentState.uiServer.close(reason); currentState.uiServer = null; } let flushError: unknown; try { flushMetadataCache(currentState); } catch (error) { flushError = error; } try { if (currentState.owner) { await currentState.owner.stop(reason); } else { await currentState.lifecycle.gracefulShutdown(); } } catch (error) { if (flushError) { console.error(`MCP: graceful shutdown failed after metadata flush error: ${formatTerminalError(error)}`); } else { throw error; } } if (flushError) { throw flushError; } } const earlyConfigPath = programmaticConfig ? undefined : (options.configPath ?? getConfigPathFromArgv()); const earlyConfig = programmaticConfig ? cloneMcpConfig(sessionConfig) : loadMcpConfig(earlyConfigPath); const earlyCache = loadMetadataCache(); const envRaw = process.env.MCP_DIRECT_TOOLS; const envDirectToolOverride = envRaw ?.split(',') .map((s) => s.trim()) .filter(Boolean); const registeredDirectTools = new Map(); const fallbackDeactivatedTools = new Set(); let proxyToolRegistered = false; let proxyToolDescription: string | null = null; // OMP remaps `typebox` to a host shim that historically lacked Type.Unsafe. // Prefer Unsafe when present (real TypeBox / fixed OMP shim); otherwise pass // the normalized JSON Schema through as a plain object so toolWireSchema and // validateToolArguments still treat it as JSON Schema. const toToolParameters = (schema: Record) => typeof (Type as { Unsafe?: (value: never) => unknown }).Unsafe === 'function' ? (Type as { Unsafe: (value: never) => unknown }).Unsafe(schema as never) : schema; // pi only replaces the extension-runtime action stubs once bindCore() runs. Until // then (and forever, in headless CLI contexts such as `pi install`, where extensions // are loaded only to evaluate project trust) action methods throw // "Extension runtime not initialized". Tool-surface bookkeeping is best-effort, so // degrade to no-ops instead of propagating that error. const isRuntimeNotBoundError = (error: unknown): boolean => error instanceof Error && error.message.includes('Extension runtime not initialized'); const getActiveToolsSafe = (): string[] | undefined => { try { return pi.getActiveTools?.(); } catch (error) { if (isRuntimeNotBoundError(error)) { return undefined; } throw error; } }; const setActiveToolsSafe = (tools: string[]): void => { try { pi.setActiveTools(tools); } catch (error) { if (isRuntimeNotBoundError(error)) { return; } throw error; } }; const isRuntimeBound = (): boolean => { try { pi.getActiveTools?.(); return true; } catch (error) { if (isRuntimeNotBoundError(error)) { return false; } throw error; } }; function directToolFingerprint(spec: DirectToolSpec): string { return JSON.stringify({ serverName: spec.serverName, originalName: spec.originalName, prefixedName: spec.prefixedName, description: spec.description, inputSchema: spec.inputSchema, resourceUri: spec.resourceUri, uiResourceUri: spec.uiResourceUri, uiStreamMode: spec.uiStreamMode, }); } function registerDirectTool(spec: DirectToolSpec): void { (pi.registerTool as (tool: unknown) => unknown)({ name: spec.prefixedName, label: `MCP: ${spec.originalName}`, description: spec.description || '(no description)', promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`, parameters: toToolParameters(normalizeDirectToolInputSchema(spec.inputSchema)), execute: createDirectToolExecutor( () => state, () => initPromise, spec ), renderCall: createMcpDirectToolCallRenderer(spec.prefixedName), renderResult: renderMcpToolResult, }); } function resolveCurrentDirectTools(config: McpConfig, cache: MetadataCache | null): DirectToolSpec[] { if (envRaw === '__none__') { return []; } const prefix = config.settings?.toolPrefix ?? 'server'; return resolveDirectTools(config, cache, prefix, envDirectToolOverride); } function deactivateTools(toolNames: string[]): string[] { if (toolNames.length === 0) { return []; } const unregisterTool = (pi as ExtensionAPI & { unregisterTool?: (name: string) => boolean }).unregisterTool; const unregistered = toolNames.filter((toolName) => unregisterTool?.(toolName) === true); const fallbackNames = toolNames.filter((toolName) => !unregistered.includes(toolName)); const remove = new Set(toolNames); const activeTools = getActiveToolsSafe(); if (!activeTools || activeTools.length === 0) { for (const toolName of fallbackNames) { fallbackDeactivatedTools.add(toolName); } return unregistered; } const nextActiveTools = activeTools.filter((name) => !remove.has(name)); if (nextActiveTools.length !== activeTools.length) { for (const toolName of fallbackNames) { fallbackDeactivatedTools.add(toolName); } setActiveToolsSafe(nextActiveTools); } return unregistered; } function syncDirectTools( config: McpConfig, cache: MetadataCache | null ): { specs: DirectToolSpec[]; added: string[]; updated: string[]; deactivated: string[]; } { const specs = resolveCurrentDirectTools(config, cache); const nextNames = new Set(specs.map((spec) => spec.prefixedName)); const added: string[] = []; const updated: string[] = []; const deactivated: string[] = []; for (const spec of specs) { const fingerprint = directToolFingerprint(spec); const previous = registeredDirectTools.get(spec.prefixedName); if (previous !== fingerprint) { registerDirectTool(spec); registeredDirectTools.set(spec.prefixedName, fingerprint); if (fallbackDeactivatedTools.delete(spec.prefixedName)) { const activeTools = getActiveToolsSafe(); if (activeTools && !activeTools.includes(spec.prefixedName)) { setActiveToolsSafe([...activeTools, spec.prefixedName]); } } (previous ? updated : added).push(spec.prefixedName); } } for (const toolName of [...registeredDirectTools.keys()]) { if (nextNames.has(toolName)) { continue; } registeredDirectTools.delete(toolName); deactivated.push(toolName); } deactivateTools(deactivated); return { specs, added, updated, deactivated }; } function applyDirectToolConfigChanges(changes: Map): void { if (!state) { return; } for (const [serverName, value] of changes) { const definition = state.config.mcpServers[serverName]; if (!definition) { continue; } state.config.mcpServers[serverName] = { ...definition, directTools: value }; } } function syncToolSurface(ctx?: ExtensionContext): void { const config = state?.config ?? earlyConfig; const cache = loadMetadataCache(); const result = syncDirectTools(config, cache); syncProxyTool(config, cache, result.specs); const changed = result.added.length + result.updated.length + result.deactivated.length; if (changed > 0 && ctx?.hasUI) { ctx.ui.notify( `MCP: direct tools refreshed (+${result.added.length}, ~${result.updated.length}, -${result.deactivated.length})`, 'info' ); } } const registeredPromptCommands = new Set(); function registerPromptCommands(specs: Iterable): void { for (const spec of specs) { if (registeredPromptCommands.has(spec.commandName)) { logger.debug( `MCP: prompt "${spec.originalName}" on ${spec.serverName} skipped; /${spec.commandName} is already registered` ); continue; } registeredPromptCommands.add(spec.commandName); pi.registerCommand( spec.commandName, createPromptCommand(pi, () => state, spec) ); } } function syncPromptCommands(): void { registerPromptCommands([...(state?.promptMetadata?.values() ?? [])].flat()); } registerPromptCommands(resolveCachedPrompts(earlyConfig)); const getPiTools = (): ToolInfo[] => pi.getAllTools(); pi.registerFlag('mcp-config', { description: 'Path to MCP config file', type: 'string', }); function startInitialization( ctx: ExtensionContext, owner: McpRuntimeOwner, oauthRuntime: McpOAuthRuntime, generation: number, staleReason: string ): Promise { const promise = initializeMcp(pi, ctx, owner, { ...(programmaticConfig || options.configPath !== undefined ? { configPath: earlyConfigPath, config: sessionConfig } : {}), oauthRuntime, statusEvents: pi.events, }); initPromise = promise; return promise .then(async (nextState) => { if (!owner.isActive() || generation !== lifecycleGeneration || initPromise !== promise) { try { await shutdownState(nextState, staleReason); } catch (error) { console.error(`MCP: failed to clean stale initialization state: ${formatTerminalError(error)}`); } return; } state = nextState; nextState.onToolMetadataUpdated = (_serverName, _reason) => { if (state !== nextState || !owner.isActive()) { return; } syncPromptCommands(); syncToolSurface(ctx); }; syncPromptCommands(); syncToolSurface(ctx); updateStatusBar(nextState); initPromise = null; }) .catch(async (err) => { if (!owner.isActive() || generation !== lifecycleGeneration) { return; } if (initPromise !== promise && initPromise !== null) { return; } if (isRuntimeNotBoundError(err)) { logger.debug( `MCP: load-time initialization skipped (extension runtime not bound): ${formatTerminalError(err)}` ); } else { console.error(`MCP initialization failed: ${formatTerminalError(err)}`); } initPromise = null; if (state) { return; } try { await Promise.all([owner.stop('MCP initialization failed'), shutdownOAuth(oauthRuntime)]); } catch (error) { console.error(`MCP: failed to clean rejected initialization: ${formatTerminalError(error)}`); } }); } function startLoadTimeInitialization(): void { const hasStartupServer = Object.values(earlyConfig.mcpServers).some((definition) => { if (definition.disabled === true) { return false; } return definition.lifecycle === 'eager' || definition.lifecycle === 'keep-alive'; }); if (!hasStartupServer) { return; } setImmediate(() => { if (lifecycleGeneration !== 0 || state || initPromise) { return; } // Headless CLI contexts (pi install/update/config, trust evaluation) never bind // the extension runtime; skip the eager connect there instead of failing later. if (!isRuntimeBound()) { return; } const generation = ++lifecycleGeneration; const owner = createMcpRuntimeOwner(); const oauthRuntime = createOAuthRuntime(owner.signal); currentOwner = owner; currentOAuthRuntime = oauthRuntime; startInitialization( { mode: 'print', hasUI: false, cwd: process.cwd(), model: undefined, modelRegistry: undefined, signal: undefined, } as unknown as ExtensionContext, owner, oauthRuntime, generation, 'stale_load_time_initialization' ); }); } pi.on('session_start', async (_event, ctx) => { const generation = ++lifecycleGeneration; const previousState = state; const previousOwner = currentOwner; const previousOAuthRuntime = currentOAuthRuntime; const owner = createMcpRuntimeOwner(); const oauthRuntime = createOAuthRuntime(owner.signal); currentOwner = owner; currentOAuthRuntime = oauthRuntime; state = null; initPromise = null; // Abort synchronously before awaiting cleanup so old callbacks and startup // work cannot resume into a stale ExtensionContext. const stopPrevious = previousOwner?.stop('MCP extension session restarted') ?? Promise.resolve(); try { await Promise.all([ stopPrevious, shutdownState(previousState, 'session_restart'), previousOAuthRuntime ? shutdownOAuth(previousOAuthRuntime) : Promise.resolve(), ]); } catch (error) { console.error(`MCP: failed to shut down previous session state: ${formatTerminalError(error)}`); } if (generation !== lifecycleGeneration || !owner.isActive()) { return; } const initialization = startInitialization(ctx, owner, oauthRuntime, generation, 'stale_session_start'); if (envRaw !== undefined && envRaw !== '__none__') { const missingEnvDirectTools = getMissingConfiguredDirectToolServers( earlyConfig, loadMetadataCache(), envDirectToolOverride ); if (missingEnvDirectTools.length > 0) { await initialization; } } }); pi.on('session_shutdown', async () => { ++lifecycleGeneration; const currentState = state; const owner = currentOwner; const oauthRuntime = currentOAuthRuntime; currentOwner = null; currentOAuthRuntime = null; state = null; initPromise = null; // Abort before awaiting cleanup so delayed initialization cannot touch stale // Pi context after session shutdown. const stopOwner = owner?.stop('MCP extension session shutdown') ?? Promise.resolve(); try { await Promise.all([ stopOwner, shutdownState(currentState, 'session_shutdown'), oauthRuntime ? shutdownOAuth(oauthRuntime) : Promise.resolve(), ]); } catch (error) { console.error(`MCP: session shutdown cleanup failed: ${formatTerminalError(error)}`); } }); // Re-flag returned MCP tool failures so pi registers them as errors (see toolErrorOverride). pi.on('tool_result', (event) => toolErrorOverride(event.details)); pi.registerCommand('mcp', { description: 'Show MCP server status', getArgumentCompletions: (prefix: string) => { const normalized = prefix.trimStart(); const argumentMatch = normalized.match(/^(\S+)\s+(.*)$/); if (!argumentMatch) { const subcommands = [ { value: 'reconnect', label: 'reconnect - Reconnect servers' }, { value: 'tools', label: 'tools - List all tools' }, { value: 'prompts', label: 'prompts - List all MCP prompts' }, { value: 'setup', label: 'setup - Configure MCP servers' }, { value: 'logout', label: 'logout - Clear server credentials' }, { value: 'disable', label: 'disable - Disable a server' }, { value: 'enable', label: 'enable - Enable a server' }, { value: 'status', label: 'status - Show server status' }, ].filter(({ value }) => value.startsWith(normalized)); return subcommands.length > 0 ? subcommands : null; } const [, subcommand, argumentPrefix] = argumentMatch; if ( (subcommand !== 'reconnect' && subcommand !== 'logout' && subcommand !== 'disable' && subcommand !== 'enable') || !state ) { return null; } const servers = Object.keys(state.config.mcpServers) .filter((serverName) => serverName.startsWith(argumentPrefix.trimStart())) .map((serverName) => ({ value: `${subcommand} ${serverName}`, label: serverName })); return servers.length > 0 ? servers : null; }, handler: async (args, ctx) => { const commandOwner = currentOwner; const commandReload = typeof ctx.reload === 'function' ? ctx.reload.bind(ctx) : async () => {}; const commandHasUI = ctx.hasUI; const commandCtx = { hasUI: commandHasUI, ui: commandHasUI ? (commandOwner ? createOwnedUi(ctx.ui, commandOwner) : ctx.ui) : undefined, cwd: ctx.cwd, mode: ctx.mode, signal: commandOwner?.signal ?? ctx.signal, } as ExtensionContext; if (!state && initPromise) { try { const initialized = await initPromise; commandOwner?.throwIfInactive(); state = initialized; } catch (error) { const message = error instanceof Error ? error.message : String(error); if (commandCtx.hasUI) { commandCtx.ui?.notify(`MCP initialization failed: ${message}`, 'error'); } return; } } if (!state) { if (commandCtx.hasUI) { commandCtx.ui?.notify('MCP not initialized', 'error'); } return; } const parts = args?.trim()?.split(/\s+/) ?? []; const subcommand = parts[0] ?? ''; const targetServer = parts[1]; const rest = parts.slice(1).join(' '); switch (subcommand) { case 'reconnect': commandOwner?.throwIfInactive(); await reconnectServers(state, commandCtx, targetServer); break; case 'tools': await showTools(state, commandCtx); break; case 'prompts': await showPrompts(state, commandCtx); break; case 'setup': { commandOwner?.throwIfInactive(); if (programmaticConfig) { commandCtx.ui?.notify('MCP setup is unavailable when config is supplied by createMcpAdapter().', 'info'); break; } const result = await openMcpSetup(state, pi, commandCtx, earlyConfigPath, 'setup'); if (result?.configChanged) { commandOwner?.throwIfInactive(); await commandReload(); return; } break; } case 'logout': { const serverName = rest; if (!serverName) { if (commandCtx.hasUI) { commandCtx.ui?.notify('Usage: /mcp logout ', 'error'); } return; } commandOwner?.throwIfInactive(); await logoutServer(serverName, state, commandCtx); break; } case 'disable': case 'enable': { const serverName = rest; if (programmaticConfig) { commandCtx.ui?.notify( `/mcp ${subcommand} is unavailable when config is supplied by createMcpAdapter().`, 'info' ); break; } if (!serverName) { commandCtx.ui?.notify(`Usage: /mcp ${subcommand} `, 'error'); break; } if (!state.config.mcpServers[serverName]) { commandCtx.ui?.notify(`Server "${serverName}" not found in effective config`, 'error'); break; } commandOwner?.throwIfInactive(); const result = writeProjectServerDisabledOverride( earlyConfigPath, commandCtx.cwd, serverName, subcommand === 'disable' ); if (result.changed) { commandCtx.ui?.notify( `${subcommand === 'disable' ? 'Disabled' : 'Enabled'} server "${serverName}" in ${result.path} - run /reload to apply`, 'info' ); } else { commandCtx.ui?.notify( `Server "${serverName}" is already ${subcommand === 'disable' ? 'disabled' : 'enabled'}`, 'info' ); } break; } default: if (commandCtx.hasUI) { commandOwner?.throwIfInactive(); if (programmaticConfig) { commandCtx.ui?.notify( 'MCP status is shown from the in-memory SDK config; configuration discovery is unavailable.', 'info' ); await showStatus(state, commandCtx); break; } const result = await openMcpPanel(state, pi, commandCtx, earlyConfigPath, (changes) => { applyDirectToolConfigChanges(changes); syncToolSurface(commandCtx); }); if (result?.configChanged) { commandOwner?.throwIfInactive(); await commandReload(); return; } } else { await showStatus(state, commandCtx); } break; } }, }); pi.registerCommand('mcp-auth', { description: 'Authenticate with an MCP server (OAuth)', handler: async (args, ctx) => { const commandOwner = currentOwner; const commandHasUI = ctx.hasUI; const commandCtx = { hasUI: commandHasUI, ui: commandHasUI ? (commandOwner ? createOwnedUi(ctx.ui, commandOwner) : ctx.ui) : undefined, cwd: ctx.cwd, mode: ctx.mode, signal: commandOwner?.signal ?? ctx.signal, } as ExtensionContext; const serverName = args?.trim(); if (!serverName && !commandCtx.hasUI) { return; } if (!state && initPromise) { try { const initialized = await initPromise; commandOwner?.throwIfInactive(); state = initialized; } catch (error) { const message = error instanceof Error ? error.message : String(error); if (commandCtx.hasUI) { commandCtx.ui?.notify(`MCP initialization failed: ${message}`, 'error'); } return; } } if (!state) { if (commandCtx.hasUI) { commandCtx.ui?.notify('MCP not initialized', 'error'); } return; } if (!serverName) { if (programmaticConfig) { commandCtx.ui?.notify( 'Use /mcp-auth to authenticate a server from the in-memory SDK config.', 'info' ); return; } await openMcpAuthPanel(state, pi, commandCtx, earlyConfigPath); return; } const result = await authenticateServer( serverName, state.config, commandCtx, commandCtx.signal, state.oauthRuntime ); if (result.ok) { commandOwner?.throwIfInactive(); await reconnectServer(state, commandCtx, serverName); } }, }); function registerProxyTool(description: string): void { (pi.registerTool as (tool: unknown) => unknown)({ name: 'mcp', label: 'MCP', description, promptSnippet: 'MCP gateway - connect to MCP servers and call their tools', renderCall: renderMcpProxyToolCall, parameters: Type.Object({ tool: Type.Optional(Type.String({ description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })), args: Type.Optional( Type.Union( [ Type.String({ description: 'Arguments as a JSON string (e.g., \'{"key": "value"}\')' }), Type.Object( {}, { additionalProperties: true, description: 'Arguments as a JSON object (e.g., { "key": "value" })', } ), ], { description: 'Tool arguments as a JSON object, or as a JSON string encoding one' } ) ), connect: Type.Optional( Type.String({ description: 'Server name to connect (lazy connect + metadata refresh)' }) ), describe: Type.Optional(Type.String({ description: 'Tool name to describe (shows parameters)' })), instructions: Type.Optional( Type.String({ description: "Server name to show that server's usage instructions" }) ), search: Type.Optional(Type.String({ description: 'Search tools by name/description' })), regex: Type.Optional(Type.Boolean({ description: 'Treat search as regex (default: substring match)' })), includeSchemas: Type.Optional( Type.Boolean({ description: 'Include parameter schemas in search results (default: true)' }) ), server: Type.Optional( Type.String({ description: 'Filter to specific server (also disambiguates tool calls)' }) ), action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })), }), renderResult: renderMcpToolResult, async execute( _toolCallId: string, params: { tool?: string; args?: string | Record; connect?: string; describe?: string; instructions?: string; search?: string; regex?: boolean; includeSchemas?: boolean; server?: string; action?: string; }, signal?: AbortSignal, _onUpdate?: unknown, _ctx?: unknown ) { const executeOwner = currentOwner; let parsedArgs: Record | undefined; if (params.args !== undefined && params.args !== '') { let args: unknown; if (typeof params.args === 'string') { try { args = JSON.parse(params.args); } catch (error) { if (error instanceof SyntaxError) { throw new Error(`Invalid args JSON: ${error.message}`, { cause: error }); } throw error; } } else { args = params.args; } if (typeof args !== 'object' || args === null || Array.isArray(args)) { const gotType = Array.isArray(args) ? 'array' : args === null ? 'null' : typeof args; throw new Error(`Invalid args: expected a JSON object, got ${gotType}`); } parsedArgs = args as Record; } if (!state && initPromise) { try { const initialized = await awaitWithTimeout(initPromise, INIT_WAIT_TIMEOUT_MS); if (initialized === INIT_WAIT_TIMED_OUT) { return { content: [ { type: 'text' as const, text: 'MCP initialization is still in progress. Try again shortly.' }, ], details: { error: 'init_timeout', timeoutMs: INIT_WAIT_TIMEOUT_MS }, }; } executeOwner?.throwIfInactive(); state = initialized; } catch (error) { if (executeOwner && isAbortError(error, executeOwner.signal)) { throw error; } const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text' as const, text: `MCP initialization failed: ${message}` }], details: { error: 'init_failed', message }, }; } } if (!state) { return { content: [{ type: 'text' as const, text: 'MCP not initialized' }], details: { error: 'not_initialized' }, }; } executeOwner?.throwIfInactive(); if (params.action === 'ui-messages') { return executeUiMessages(state); } if (params.action === 'auth-start') { if (!params.server) { return { content: [ { type: 'text' as const, text: 'auth-start requires `server`. Example: mcp({ action: "auth-start", server: "linear-server" })', }, ], details: { mode: 'auth-start', error: 'missing_server' }, }; } return signal ? executeAuthStart(state, params.server, signal) : executeAuthStart(state, params.server); } if (params.action === 'auth-complete') { if (!params.server) { return { content: [{ type: 'text' as const, text: 'auth-complete requires `server`.' }], details: { mode: 'auth-complete', error: 'missing_server' }, }; } const input = parsedArgs?.redirectUrl ?? parsedArgs?.code ?? parsedArgs?.input; if (typeof input !== 'string' || input.trim().length === 0) { return { content: [ { type: 'text' as const, text: 'auth-complete requires args with `redirectUrl`, `code`, or `input`.' }, ], details: { mode: 'auth-complete', error: 'missing_input' }, }; } return signal ? executeAuthComplete(state, params.server, input, signal) : executeAuthComplete(state, params.server, input); } if (params.tool) { return executeCall(state, params.tool, parsedArgs, params.server, getPiTools, signal); } if (params.connect) { const result = await executeConnect(state, params.connect, signal); syncToolSurface(_ctx as ExtensionContext); return result; } if (params.describe) { return executeDescribe(state, params.describe); } if (params.instructions) { return executeInstructions(state, params.instructions); } if (params.search) { return executeSearch(state, params.search, params.regex, params.server, params.includeSchemas); } if (params.server) { return executeList(state, params.server); } return executeStatus(state); }, }); proxyToolRegistered = true; proxyToolDescription = description; } function syncProxyTool(config: McpConfig, cache: MetadataCache | null, directSpecs: DirectToolSpec[]): void { const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers( config, cache, envRaw === undefined || envRaw === '__none__' ? undefined : envDirectToolOverride ); const shouldRegisterProxyTool = config.settings?.disableProxyTool !== true || directSpecs.length === 0 || missingConfiguredDirectToolServers.length > 0; if (shouldRegisterProxyTool) { const description = buildProxyDescription(config, cache, directSpecs); if (!proxyToolRegistered || proxyToolDescription !== description) { registerProxyTool(description); return; } const activeTools = getActiveToolsSafe(); if (activeTools && !activeTools.includes('mcp')) { setActiveToolsSafe([...activeTools, 'mcp']); } return; } if (proxyToolRegistered) { const unregistered = deactivateTools(['mcp']); if (unregistered.includes('mcp')) { proxyToolRegistered = false; proxyToolDescription = null; } } } const initialDirectTools = syncDirectTools(earlyConfig, earlyCache).specs; syncProxyTool(earlyConfig, earlyCache, initialDirectTools); startLoadTimeInitialization(); } export function createMcpAdapter(options: McpAdapterOptions = {}) { const factoryConfig = options.config !== undefined ? cloneMcpConfig(options.config) : undefined; return function mcpAdapter(pi: ExtensionAPI) { installMcpAdapter(pi, { configPath: options.configPath, config: factoryConfig !== undefined ? cloneMcpConfig(factoryConfig) : undefined, }); }; } export default createMcpAdapter();