import { MCPClientConfig, ClientId, Platform, RegistryOptions, ConfigForClient, safeValidateClientConfig, McpRemoteOptions, } from './types.js'; import { BaseConfigBuilder, GenericConfigBuilder, GooseConfigBuilder, VSCodeConfigBuilder, CursorConfigBuilder, ClaudeCodeConfigBuilder, CodexConfigBuilder, GeminiConfigBuilder, OpenCodeConfigBuilder, } from './builders/index.js'; import chatgptConfig from '../configs/chatgpt.json'; import claudeCodeConfig from '../configs/claude-code.json'; import claudeDesktopConfig from '../configs/claude-desktop.json'; import claudeTeamsEnterpriseConfig from '../configs/claude-teams-enterprise.json'; import codexConfig from '../configs/codex.json'; import cursorConfig from '../configs/cursor.json'; import cursorAgentConfig from '../configs/cursor-agent.json'; import gooseConfig from '../configs/goose.json'; import vscodeConfig from '../configs/vscode.json'; import windsurfConfig from '../configs/windsurf.json'; import junieConfig from '../configs/junie.json'; import jetbrainsConfig from '../configs/jetbrains.json'; import geminiConfig from '../configs/gemini.json'; import opencodeConfig from '../configs/opencode.json'; import antigravityConfig from '../configs/antigravity.json'; const allConfigs = [ antigravityConfig, chatgptConfig, claudeCodeConfig, claudeDesktopConfig, claudeTeamsEnterpriseConfig, codexConfig, cursorConfig, cursorAgentConfig, gooseConfig, vscodeConfig, windsurfConfig, junieConfig, jetbrainsConfig, geminiConfig, opencodeConfig, ]; export class MCPConfigRegistry { private configs: Map = new Map(); private builderFactories: Map BaseConfigBuilder> = new Map(); private options: RegistryOptions; /** * Create a new MCP configuration registry. * @param options - Optional configuration for server packages, env vars, etc. */ constructor(options: RegistryOptions = {}) { this.options = options; this.loadConfigs(); this.registerBuilders(); } private registerBuilders(): void { // Register specific builders for clients that need special handling this.builderFactories.set( 'goose' as ClientId, GooseConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); this.builderFactories.set( 'vscode' as ClientId, VSCodeConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); this.builderFactories.set( 'cursor' as ClientId, CursorConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); this.builderFactories.set( 'claude-code' as ClientId, ClaudeCodeConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); this.builderFactories.set( 'codex' as ClientId, CodexConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); this.builderFactories.set( 'gemini' as ClientId, GeminiConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); this.builderFactories.set( 'opencode' as ClientId, OpenCodeConfigBuilder as new (config: MCPClientConfig) => BaseConfigBuilder ); // Other clients will use GenericConfigBuilder by default } private loadConfigs(): void { for (const data of allConfigs) { try { const result = safeValidateClientConfig(data); if (!result.success) { const errorMessage = result.error.issues .map((err) => ` - ${err.path.join('.')}: ${err.message}`) .join('\n'); throw new Error(`Validation failed:\n${errorMessage}`); } const config = result.data; this.validateBusinessRules(config); this.configs.set(config.id, config); } catch (error) { const configId = (data as Record).id || 'unknown'; if (error instanceof Error) { console.error(`Failed to load config ${configId}:\n${error.message}`); } else { console.error(`Failed to load config ${configId}:`, error); } } } } private validateBusinessRules(config: MCPClientConfig): void { if (!config.userConfigurable) { return; } if ( !config.configStructure.httpPropertyMapping && !config.configStructure.stdioPropertyMapping ) { throw new Error(`Client must support at least one configuration type (http or stdio)`); } // Business rule: HTTP support requires httpPropertyMapping if (config.transports.includes('http') && !config.configStructure.httpPropertyMapping) { throw new Error(`Client with HTTP support must have httpPropertyMapping defined`); } } getConfig(clientId: ClientId): MCPClientConfig | undefined { return this.configs.get(clientId); } getAllConfigs(): MCPClientConfig[] { return Array.from(this.configs.values()); } getNativeHttpClients(): MCPClientConfig[] { return this.getAllConfigs().filter((config) => config.transports.includes('http')); } getBridgeRequiredClients(): MCPClientConfig[] { // Clients that don't support HTTP natively need mcp-remote bridge return this.getAllConfigs().filter((config) => !config.transports.includes('http')); } getStdioOnlyClients(): MCPClientConfig[] { return this.getAllConfigs().filter( (config) => config.transports.length === 1 && config.transports[0] === 'stdio' ); } getClientsWithOneClick(): MCPClientConfig[] { return this.getAllConfigs().filter((config) => config.protocolHandler !== undefined); } getSupportedClients(): MCPClientConfig[] { return this.getAllConfigs().filter((config) => config.userConfigurable); } getClientsByPlatform(platform: Platform): MCPClientConfig[] { return this.getAllConfigs().filter((config) => config.supportedPlatforms.includes(platform)); } getUnsupportedClients(): MCPClientConfig[] { return this.getAllConfigs().filter((config) => !config.userConfigurable); } /** * Determines if a client needs mcp-remote to connect to HTTP servers. * * When called without options, checks if the client supports HTTP natively. * When called with an auth option, checks if the client supports that auth method natively. * * @param clientId - The client to check * @param options - Optional auth method to check * @returns true if the client needs mcp-remote for HTTP connections (or for the specified auth method) * * @example * ```typescript * // Basic transport check (existing behavior) * registry.clientNeedsMcpRemote('cursor') // false (has native HTTP) * registry.clientNeedsMcpRemote('claude-desktop') // true (stdio only) * * // Auth-aware check * registry.clientNeedsMcpRemote('cursor', { auth: 'oauth:dcr' }) // false (supports oauth:dcr) * registry.clientNeedsMcpRemote('jetbrains', { auth: 'oauth:dcr' }) // true (no oauth:dcr support) * ``` */ clientNeedsMcpRemote(clientId: ClientId, options?: McpRemoteOptions): boolean { const config = this.getConfig(clientId); if (!config) { throw new Error(`Unknown client: ${clientId}`); } // If client doesn't support HTTP natively, always needs mcp-remote if (!config.transports.includes('http')) { return true; } // If no auth specified, just check transport support (existing behavior) if (!options?.auth) { return false; // Has native HTTP } // Check if client supports the specified auth method return !config.supportedAuth.includes(options.auth); } /** * Determines if a client can connect to HTTP servers natively. * @param clientId - The client to check * @returns true if the client supports HTTP natively */ clientSupportsHttpNatively(clientId: ClientId): boolean { const config = this.getConfig(clientId); if (!config) { throw new Error(`Unknown client: ${clientId}`); } return config.transports.includes('http'); } /** * Determines if a client can connect to stdio servers. * @param clientId - The client to check * @returns true if the client supports stdio */ clientSupportsStdio(clientId: ClientId): boolean { const config = this.getConfig(clientId); if (!config) { throw new Error(`Unknown client: ${clientId}`); } return config.transports.includes('stdio'); } /** * Create a configuration builder for a specific client. * * The returned builder is typed based on the client ID: * - 'vscode' returns a builder producing VSCodeMCPConfig * - 'goose' returns a builder producing GooseMCPConfig * - 'codex' returns a builder producing CodexMCPConfig * - All others return a builder producing StandardMCPConfig * * @typeParam C - The client ID type (inferred from clientId parameter) * @param clientId - The client ID to create a builder for * @returns A typed builder instance * * @example * ```typescript * const vsCodeBuilder = registry.createBuilder('vscode'); * const config = vsCodeBuilder.buildConfiguration(options); * config.servers; // ✓ Typed as MCPServersRecord * ``` */ createBuilder(clientId: C): BaseConfigBuilder> { const config = this.getConfig(clientId); if (!config) { throw new Error(`Unknown client: ${clientId}`); } if (!config.userConfigurable) { throw new Error( `Cannot create builder for ${config.displayName}: ${config.localConfigNotes || 'No local configuration support.'}` ); } // Check if we have a specific builder for this client const BuilderClass = this.builderFactories.get(clientId); const builder = BuilderClass ? new BuilderClass(config) : new GenericConfigBuilder(config); // Inject registry options into the builder builder.setRegistryOptions(this.options); // Internal cast: runtime builder selection is correct, we're just informing TypeScript return builder as BaseConfigBuilder>; } }