import { buildProviderAwareCliEnv } from '@main/services/runtime/providerAwareCliEnv'; import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; import { getConfiguredCliFlavor } from '@main/services/team/cliFlavor'; import { execCli } from '@main/utils/childProcess'; import { CLI_NOT_FOUND_MESSAGE } from '@shared/constants/cli'; import { McpConfigStateReader } from './McpConfigStateReader'; import { parseMcpDiagnosticsJsonOutput, parseMcpDiagnosticsOutput } from './mcpDiagnosticsParser'; import { parseInstalledMcpJsonOutput } from './mcpRuntimeJson'; import type { CliFlavor } from '@shared/types'; import type { InstalledMcpEntry, McpServerDiagnostic } from '@shared/types/extensions'; const MCP_LIST_TIMEOUT_MS = 15_000; const MCP_DIAGNOSE_TIMEOUT_MS = 60_000; async function buildManagementCliEnvForBinary(binaryPath: string): Promise { const { env } = await buildProviderAwareCliEnv({ binaryPath, connectionMode: 'augment', }); return env; } export interface ExtensionsRuntimeAdapter { readonly flavor: CliFlavor; buildManagementCliEnv(binaryPath: string): Promise; getInstalledMcp(projectPath?: string): Promise; diagnoseMcp(projectPath?: string): Promise; } export class ClaudeExtensionsAdapter implements ExtensionsRuntimeAdapter { readonly flavor = 'claude' as const; constructor(private readonly stateReader = new McpConfigStateReader()) {} async buildManagementCliEnv(binaryPath: string): Promise { return buildManagementCliEnvForBinary(binaryPath); } async getInstalledMcp(projectPath?: string): Promise { return this.stateReader.readInstalled(projectPath); } async diagnoseMcp(projectPath?: string): Promise { const binaryPath = await ClaudeBinaryResolver.resolve(); if (!binaryPath) { throw new Error(CLI_NOT_FOUND_MESSAGE); } const env = await this.buildManagementCliEnv(binaryPath); const { stdout, stderr } = await execCli(binaryPath, ['mcp', 'list'], { timeout: MCP_DIAGNOSE_TIMEOUT_MS, cwd: projectPath, env, }); return parseMcpDiagnosticsOutput([stdout, stderr].filter(Boolean).join('\n')); } } export class MultimodelExtensionsAdapter implements ExtensionsRuntimeAdapter { readonly flavor = 'agent_teams_orchestrator' as const; constructor(private readonly stateReader = new McpConfigStateReader()) {} async buildManagementCliEnv(binaryPath: string): Promise { return buildManagementCliEnvForBinary(binaryPath); } async getInstalledMcp(projectPath?: string): Promise { const binaryPath = await ClaudeBinaryResolver.resolve(); if (!binaryPath) { throw new Error(CLI_NOT_FOUND_MESSAGE); } const env = await this.buildManagementCliEnv(binaryPath); try { const { stdout } = await execCli(binaryPath, ['mcp', 'list', '--json'], { timeout: MCP_LIST_TIMEOUT_MS, cwd: projectPath, env, }); return parseInstalledMcpJsonOutput(stdout); } catch (error) { if (!isUnsupportedMcpJsonContractError(error)) { throw error; } return this.stateReader.readInstalled(projectPath); } } async diagnoseMcp(projectPath?: string): Promise { const binaryPath = await ClaudeBinaryResolver.resolve(); if (!binaryPath) { throw new Error(CLI_NOT_FOUND_MESSAGE); } const env = await this.buildManagementCliEnv(binaryPath); try { const { stdout } = await execCli(binaryPath, ['mcp', 'diagnose', '--json'], { timeout: MCP_DIAGNOSE_TIMEOUT_MS, cwd: projectPath, env, }); return parseMcpDiagnosticsJsonOutput(stdout); } catch (error) { if (!isUnsupportedMcpJsonContractError(error)) { throw error; } const { stdout, stderr } = await execCli(binaryPath, ['mcp', 'list'], { timeout: MCP_DIAGNOSE_TIMEOUT_MS, cwd: projectPath, env, }); return parseMcpDiagnosticsOutput([stdout, stderr].filter(Boolean).join('\n')); } } } function isUnsupportedMcpJsonContractError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); const normalized = message.toLowerCase(); return ( normalized.includes("unknown command 'diagnose'") || normalized.includes('unknown command "diagnose"') || normalized.includes('unknown option') || normalized.includes('unknown argument') || normalized.includes('unexpected argument') || normalized.includes('unrecognized option') ); } class RuntimeSwitchingExtensionsAdapter implements ExtensionsRuntimeAdapter { constructor( private readonly claudeAdapter: ClaudeExtensionsAdapter, private readonly multimodelAdapter: MultimodelExtensionsAdapter ) {} private getActiveAdapter(): ExtensionsRuntimeAdapter { return getConfiguredCliFlavor() === 'claude' ? this.claudeAdapter : this.multimodelAdapter; } get flavor(): CliFlavor { return this.getActiveAdapter().flavor; } buildManagementCliEnv(binaryPath: string): Promise { return this.getActiveAdapter().buildManagementCliEnv(binaryPath); } getInstalledMcp(projectPath?: string): Promise { return this.getActiveAdapter().getInstalledMcp(projectPath); } diagnoseMcp(projectPath?: string): Promise { return this.getActiveAdapter().diagnoseMcp(projectPath); } } export function createExtensionsRuntimeAdapter(): ExtensionsRuntimeAdapter { return new RuntimeSwitchingExtensionsAdapter( new ClaudeExtensionsAdapter(), new MultimodelExtensionsAdapter() ); }