// MCP Manager View // Renders the MCP server management interface // CCW Tools available for MCP const CCW_MCP_TOOLS = [ // Core tools (always recommended) { name: 'write_file', desc: 'Write/create files', core: true }, { name: 'edit_file', desc: 'Edit/replace content', core: true }, { name: 'smart_search', desc: 'Hybrid search (regex + semantic)', core: true }, { name: 'core_memory', desc: 'Core memory management', core: true }, // Optional tools { name: 'session_manager', desc: 'Workflow sessions', core: false }, { name: 'generate_module_docs', desc: 'Generate docs', core: false }, { name: 'update_module_claude', desc: 'Update CLAUDE.md', core: false }, { name: 'cli_executor', desc: 'Gemini/Qwen/Codex CLI', core: false }, ]; // Get currently enabled tools from installed config (Claude) function getCcwEnabledTools() { const currentPath = projectPath; // Keep original format (forward slash) const projectData = mcpAllProjects[currentPath] || {}; const ccwConfig = projectData.mcpServers?.['ccw-tools']; if (ccwConfig?.env?.CCW_ENABLED_TOOLS) { const val = ccwConfig.env.CCW_ENABLED_TOOLS; if (val.toLowerCase() === 'all') return CCW_MCP_TOOLS.map(t => t.name); return val.split(',').map(t => t.trim()); } return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name); } // Get currently enabled tools from Codex config function getCcwEnabledToolsCodex() { const ccwConfig = codexMcpServers?.['ccw-tools']; if (ccwConfig?.env?.CCW_ENABLED_TOOLS) { const val = ccwConfig.env.CCW_ENABLED_TOOLS; if (val.toLowerCase() === 'all') return CCW_MCP_TOOLS.map(t => t.name); return val.split(',').map(t => t.trim()); } // Default to core tools if not installed return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name); } // Get current CCW_PROJECT_ROOT from config function getCcwProjectRoot() { // Try project config first, then global config const currentPath = projectPath; const projectData = mcpAllProjects[currentPath] || {}; const projectCcwConfig = projectData.mcpServers?.['ccw-tools']; if (projectCcwConfig?.env?.CCW_PROJECT_ROOT) { return projectCcwConfig.env.CCW_PROJECT_ROOT; } // Fallback to global config const globalCcwConfig = mcpUserServers?.['ccw-tools']; return globalCcwConfig?.env?.CCW_PROJECT_ROOT || ''; } // Get current CCW_ALLOWED_DIRS from config function getCcwAllowedDirs() { // Try project config first, then global config const currentPath = projectPath; const projectData = mcpAllProjects[currentPath] || {}; const projectCcwConfig = projectData.mcpServers?.['ccw-tools']; if (projectCcwConfig?.env?.CCW_ALLOWED_DIRS) { return projectCcwConfig.env.CCW_ALLOWED_DIRS; } // Fallback to global config const globalCcwConfig = mcpUserServers?.['ccw-tools']; return globalCcwConfig?.env?.CCW_ALLOWED_DIRS || ''; } // Get current CCW_PROJECT_ROOT from Codex config function getCcwProjectRootCodex() { const ccwConfig = codexMcpServers?.['ccw-tools']; return ccwConfig?.env?.CCW_PROJECT_ROOT || ''; } // Get current CCW_ALLOWED_DIRS from Codex config function getCcwAllowedDirsCodex() { const ccwConfig = codexMcpServers?.['ccw-tools']; return ccwConfig?.env?.CCW_ALLOWED_DIRS || ''; } async function renderMcpManager() { const container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search for MCP view const statsGrid = document.getElementById('statsGrid'); const searchInput = document.getElementById('searchInput'); if (statsGrid) statsGrid.style.display = 'none'; if (searchInput) searchInput.parentElement.style.display = 'none'; // Load MCP config if not already loaded if (!mcpConfig) { await loadMcpConfig(); } // Load MCP templates await loadMcpTemplates(); const currentPath = projectPath; // Keep original format (forward slash) const projectData = mcpAllProjects[currentPath] || {}; const projectServers = projectData.mcpServers || {}; const disabledServers = projectData.disabledMcpServers || []; const hasMcpJson = projectData.hasMcpJson || false; const mcpJsonPath = projectData.mcpJsonPath || null; // Get all available servers from all projects const allAvailableServers = getAllAvailableMcpServers(); // Separate servers by category: // 1. Project Available = Global + Project-specific (servers available to current project) // 2. Global Management = Global servers that can be managed // 3. Other Projects = Servers from other projects (can install to project or global) const currentProjectServerNames = Object.keys(projectServers); const globalServerNames = Object.keys(mcpUserServers || {}); const enterpriseServerNames = Object.keys(mcpEnterpriseServers || {}); // Project Available MCP: servers available to current project // This includes: Enterprise (highest priority) + Global + Project-specific const projectAvailableEntries = []; // Add enterprise servers first (highest priority) for (const [name, config] of Object.entries(mcpEnterpriseServers || {})) { projectAvailableEntries.push({ name, config, source: 'enterprise', canRemove: false, canToggle: false }); } // Add global servers for (const [name, config] of Object.entries(mcpUserServers || {})) { if (!enterpriseServerNames.includes(name)) { projectAvailableEntries.push({ name, config, source: 'global', canRemove: false, // Can't remove from project view, must go to global management canToggle: true, isEnabled: !disabledServers.includes(name) }); } } // Add project-specific servers for (const [name, config] of Object.entries(projectServers)) { if (!enterpriseServerNames.includes(name) && !globalServerNames.includes(name)) { projectAvailableEntries.push({ name, config, source: 'project', canRemove: true, canToggle: true, isEnabled: !disabledServers.includes(name) }); } } // Global Management: user global servers (for management) const globalManagementEntries = Object.entries(mcpUserServers || {}); // Enterprise servers (for display only, read-only) const enterpriseServerEntries = Object.entries(mcpEnterpriseServers || {}); // Other Projects: servers from other projects (not in current project, not global) const otherProjectServers = Object.entries(allAvailableServers) .filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal); // Check if CCW Tools is already installed const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools"); const enabledTools = getCcwEnabledTools(); const enabledToolsCodex = getCcwEnabledToolsCodex(); // Prepare Codex servers data const codexServerEntries = Object.entries(codexMcpServers || {}); const codexConfigExists = codexMcpConfig?.exists || false; const codexConfigPath = codexMcpConfig?.configPath || '~/.codex/config.toml'; // Collect cross-CLI servers (servers from other CLI not yet in current CLI) const crossCliServers = []; if (currentCliMode === 'claude') { // In Claude mode, show Codex servers that aren't in Claude for (const [name, config] of Object.entries(codexMcpServers || {})) { const existsInClaude = currentProjectServerNames.includes(name) || globalServerNames.includes(name); if (!existsInClaude) { crossCliServers.push({ name, config, fromCli: 'codex' }); } } } else { // In Codex mode, show Claude servers that aren't in Codex const allClaudeServers = { ...mcpUserServers, ...projectServers }; for (const [name, config] of Object.entries(allClaudeServers)) { const existsInCodex = codexMcpServers && codexMcpServers[name]; if (!existsInCodex) { crossCliServers.push({ name, config, fromCli: 'claude' }); } } } container.innerHTML = `
${t('mcp.cliMode')}
${currentCliMode === 'claude' ? ` ~/.claude.json` : ` ${codexConfigPath}` }
${currentCliMode === 'codex' ? `

CCW Tools MCP

Codex ${codexMcpServers && codexMcpServers['ccw-tools'] ? ` ${enabledToolsCodex.length} tools ` : ` ${t('mcp.available')} `}

${t('mcp.ccwToolsDesc')}

${CCW_MCP_TOOLS.map(tool => ` `).join('')}
${t('mcp.pathSettings')}

${t('mcp.codex.globalServers')}

${codexConfigExists ? ` config.toml ` : ` Will create config.toml `}
${codexServerEntries.length} ${t('mcp.serversAvailable')}

${t('mcp.codex.infoTitle')}

${t('mcp.codex.infoDesc')}

${codexServerEntries.length === 0 ? `

${t('mcp.codex.noServers')}

${t('mcp.codex.noServersHint')}

` : `
${codexServerEntries.map(([serverName, serverConfig]) => { return renderCodexServerCard(serverName, serverConfig); }).join('')}
`}
${Object.keys(mcpUserServers || {}).length > 0 ? `

${t('mcp.codex.copyFromClaude')}

${Object.keys(mcpUserServers || {}).length} ${t('mcp.serversAvailable')}
${Object.entries(mcpUserServers || {}).map(([serverName, serverConfig]) => { const alreadyInCodex = codexMcpServers && codexMcpServers[serverName]; return `

${escapeHtml(serverName)}

Claude ${alreadyInCodex ? `${t('mcp.codex.alreadyAdded')}` : ''}
${!alreadyInCodex ? ` ` : ''}
${t('mcp.cmd')} ${escapeHtml(serverConfig.command || 'N/A')}
`; }).join('')}
` : ''}

${t('mcp.availableOther')}

${otherProjectServers.length} ${t('mcp.serversAvailable')}
${otherProjectServers.length === 0 ? `

${t('empty.noAdditionalMcp')}

` : `
${otherProjectServers.map(([serverName, serverInfo]) => { return renderAvailableServerCardForCodex(serverName, serverInfo); }).join('')}
`}
${crossCliServers.length > 0 ? `

${t('mcp.codex.copyFromClaude')}

${crossCliServers.length} ${t('mcp.serversAvailable')}
${crossCliServers.map(server => renderCrossCliServerCard(server, false)).join('')}
` : ''} ` : `

CCW Tools MCP

${isCcwToolsInstalled ? ` ${enabledTools.length} tools ` : ` Available `}
${CCW_MCP_TOOLS.map(tool => ` `).join('')}
${t('mcp.pathSettings')}
${isCcwToolsInstalled ? ` ` : ` `}

${t('mcp.projectAvailable')}

${hasMcpJson ? ` exists ` : ''}
${projectAvailableEntries.length} ${t('mcp.serversAvailable')}
${projectAvailableEntries.length === 0 ? `

${t('empty.noMcpServers')}

${t('empty.addMcpServersHint')}

` : `
${projectAvailableEntries.map(entry => { return renderProjectAvailableServerCard(entry); }).join('')}
`}

${t('mcp.globalAvailable')}

${globalManagementEntries.length} ${t('mcp.globalServersFrom')}
${globalManagementEntries.length === 0 ? `

${t('empty.noGlobalMcpServers')}

${t('empty.globalServersHint')}

` : `
${globalManagementEntries.map(([serverName, serverConfig]) => { return renderGlobalManagementCard(serverName, serverConfig); }).join('')}
`}

${t('mcp.availableOther')}

${otherProjectServers.length} ${t('mcp.serversAvailable')}
${otherProjectServers.length === 0 ? `

${t('empty.noAdditionalMcp')}

` : `
${otherProjectServers.map(([serverName, serverInfo]) => { return renderAvailableServerCard(serverName, serverInfo); }).join('')}
`}
${crossCliServers.length > 0 ? `

${t('mcp.claude.copyFromCodex')}

${crossCliServers.length} ${t('mcp.serversAvailable')}
${crossCliServers.map(server => renderCrossCliServerCard(server, true)).join('')}
` : ''} ${mcpTemplates.length > 0 ? `

${t('mcp.templates')}

${mcpTemplates.length} ${t('mcp.savedTemplates')}
${mcpTemplates.map(template => `

${escapeHtml(template.name)}

${template.description ? ` ${escapeHtml(template.description)} ` : ''}
${t('mcp.cmd')} ${escapeHtml(template.serverConfig.command)}
${template.serverConfig.args && template.serverConfig.args.length > 0 ? `
${t('mcp.args')} ${escapeHtml(template.serverConfig.args.slice(0, 3).join(' '))}${template.serverConfig.args.length > 3 ? '...' : ''}
` : ''}
`).join('')}
` : ''} ${currentCliMode === 'claude' && Object.keys(codexMcpServers || {}).length > 0 ? `

${t('mcp.claude.copyFromCodex')}

${Object.keys(codexMcpServers || {}).length} ${t('mcp.serversAvailable')}
${Object.entries(codexMcpServers || {}).map(([serverName, serverConfig]) => { const alreadyInClaude = mcpUserServers && mcpUserServers[serverName]; const isStdio = !!serverConfig.command; const isHttp = !!serverConfig.url; return `

${escapeHtml(serverName)}

Codex ${isHttp ? 'HTTP' : 'STDIO' } ${alreadyInClaude ? '' + t('mcp.claude.alreadyAdded') + '' : ''}
${!alreadyInClaude ? ` ` : ''}
${isHttp ? t('mcp.url') : t('mcp.cmd')} ${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}
`; }).join('')}
` : ''} ${currentCliMode === 'claude' ? `

${t('mcp.allProjects')}

${Object.keys(mcpAllProjects).length} ${t('mcp.projects')}
${Object.entries(mcpAllProjects).map(([path, config]) => { const servers = config.mcpServers || {}; const projectDisabled = config.disabledMcpServers || []; const serverNames = Object.keys(servers); const isCurrentProject = path === currentPath; const enabledCount = serverNames.filter(s => !projectDisabled.includes(s)).length; const projectHasMcpJson = config.hasMcpJson || false; return ` `; }).join('')}
${t('mcp.project')} ${t('mcp.servers')} ${t('mcp.status')}
${isCurrentProject ? '' : ''}
${escapeHtml(path.split('\\').pop() || path)} ${isCurrentProject ? `${t('mcp.current')}` : ''} ${projectHasMcpJson ? `` : ''}
${escapeHtml(path)}
${serverNames.length === 0 ? `${t('mcp.noMcpServers')}` : serverNames.map(serverName => { const isEnabled = !projectDisabled.includes(serverName); return ` ${escapeHtml(serverName)} `; }).join('') }
${enabledCount}/${serverNames.length}
` : ''} `}
`; // Initialize Lucide icons FIRST (before attaching event listeners) // lucide.createIcons() may replace DOM elements, which would remove event listeners if (typeof lucide !== 'undefined') lucide.createIcons(); // Attach event listeners AFTER icon initialization attachMcpEventListeners(); } // Render card for Project Available MCP (current project can use) function renderProjectAvailableServerCard(entry) { const { name, config, source, canRemove, canToggle, isEnabled } = entry; const command = config.command || 'N/A'; const args = config.args || []; const hasEnv = config.env && Object.keys(config.env).length > 0; // Source badge let sourceBadge = ''; if (source === 'enterprise') { sourceBadge = `${t('mcp.sourceEnterprise')}`; } else if (source === 'global') { sourceBadge = `${t('mcp.sourceGlobal')}`; } else if (source === 'project') { sourceBadge = `${t('mcp.sourceProject')}`; } return `
${canToggle && isEnabled ? '' : ''}

${escapeHtml(name)}

${sourceBadge}
${canToggle ? ` ` : ''}
${t('mcp.cmd')} ${escapeHtml(command)}
${args.length > 0 ? `
${t('mcp.args')} ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}
` : ''} ${hasEnv ? `
${t('mcp.env')} ${Object.keys(config.env).length} ${t('mcp.variables')}
` : ''}
${canRemove ? ` ` : ''}
`; } // Render card for Global Management (manage global servers) function renderGlobalManagementCard(serverName, serverConfig) { const command = serverConfig.command || serverConfig.url || 'N/A'; const args = serverConfig.args || []; const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0; const serverType = serverConfig.type || 'stdio'; return `

${escapeHtml(serverName)}

${serverType === 'stdio' ? t('mcp.cmd') : t('mcp.url')} ${escapeHtml(command)}
${args.length > 0 ? `
${t('mcp.args')} ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}
` : ''} ${hasEnv ? `
${t('mcp.env')} ${Object.keys(serverConfig.env).length} ${t('mcp.variables')}
` : ''}
${t('mcp.availableToAll')}
`; } function renderAvailableServerCard(serverName, serverInfo) { const serverConfig = serverInfo.config; const usedIn = serverInfo.usedIn || []; const command = serverConfig.command || 'N/A'; const args = serverConfig.args || []; // Get the actual name to use when adding (original name if different from display key) const originalName = serverInfo.originalName || serverName; const hasVariant = serverInfo.originalName && serverInfo.originalName !== serverName; // Get source project info const sourceProject = serverInfo.sourceProject; const sourceProjectName = sourceProject ? (sourceProject.split('\\').pop() || sourceProject.split('/').pop()) : null; // Generate args preview const argsPreview = args.length > 0 ? args.slice(0, 3).join(' ') + (args.length > 3 ? '...' : '') : ''; return `

${escapeHtml(originalName)}

${hasVariant ? ` ${escapeHtml(sourceProjectName || 'variant')} ` : ''}
${t('mcp.cmd')} ${escapeHtml(command)}
${argsPreview ? `
${t('mcp.args')} ${escapeHtml(argsPreview)}
` : ''}
${t('mcp.usedInCount').replace('{count}', usedIn.length).replace('{s}', usedIn.length !== 1 ? 's' : '')} ${sourceProjectName ? `• ${t('mcp.from')} ${escapeHtml(sourceProjectName)}` : ''}
`; } // Render available server card for Codex mode (with Claude badge and copy to Codex button) function renderAvailableServerCardForCodex(serverName, serverInfo) { const serverConfig = serverInfo.config; const usedIn = serverInfo.usedIn || []; const command = serverConfig.command || serverConfig.url || 'N/A'; const args = serverConfig.args || []; // Get the actual name to use when adding const originalName = serverInfo.originalName || serverName; const hasVariant = serverInfo.originalName && serverInfo.originalName !== serverName; // Get source project info const sourceProject = serverInfo.sourceProject; const sourceProjectName = sourceProject ? (sourceProject.split('\\').pop() || sourceProject.split('/').pop()) : null; // Generate args preview const argsPreview = args.length > 0 ? args.slice(0, 3).join(' ') + (args.length > 3 ? '...' : '') : ''; // Check if already in Codex const alreadyInCodex = codexMcpServers && codexMcpServers[originalName]; return `

${escapeHtml(originalName)}

Claude ${hasVariant ? ` ${escapeHtml(sourceProjectName || 'variant')} ` : ''} ${alreadyInCodex ? `${t('mcp.codex.alreadyAdded')}` : ''}
${!alreadyInCodex ? ` ` : ''}
${t('mcp.cmd')} ${escapeHtml(command)}
${argsPreview ? `
${t('mcp.args')} ${escapeHtml(argsPreview)}
` : ''}
${t('mcp.usedInCount').replace('{count}', usedIn.length).replace('{s}', usedIn.length !== 1 ? 's' : '')} ${sourceProjectName ? `• ${t('mcp.from')} ${escapeHtml(sourceProjectName)}` : ''}
`; } // ======================================== // Codex MCP Server Card Renderer // ======================================== function renderCodexServerCard(serverName, serverConfig) { const isStdio = !!serverConfig.command; const isHttp = !!serverConfig.url; const isEnabled = serverConfig.enabled !== false; // Default to enabled const command = serverConfig.command || serverConfig.url || 'N/A'; const args = serverConfig.args || []; const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0; // Server type badge const typeBadge = isHttp ? `HTTP` : `STDIO`; return `
${isEnabled ? '' : ''}

${escapeHtml(serverName)}

Codex ${typeBadge}
${isHttp ? t('mcp.url') : t('mcp.cmd')} ${escapeHtml(command)}
${args.length > 0 ? `
${t('mcp.args')} ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}
` : ''} ${hasEnv ? `
${t('mcp.env')} ${Object.keys(serverConfig.env).length} ${t('mcp.variables')}
` : ''} ${serverConfig.enabled_tools ? `
${t('mcp.codex.enabledTools')} ${serverConfig.enabled_tools.length} ${t('mcp.codex.tools')}
` : ''}
`; } // Render card for cross-CLI servers (servers from other CLI not in current CLI) function renderCrossCliServerCard(server, isClaude) { const { name, config, fromCli } = server; const isStdio = !!config.command; const isHttp = !!config.url; const command = config.command || config.url || 'N/A'; const args = config.args || []; // Icon and color based on source CLI const icon = fromCli === 'codex' ? 'circle-dashed' : 'circle'; const sourceBadgeColor = fromCli === 'codex' ? 'green' : 'orange'; const targetCli = isClaude ? 'project' : 'codex'; const buttonText = isClaude ? t('mcp.codex.copyToClaude') : t('mcp.claude.copyToCodex'); const typeBadge = isHttp ? `HTTP` : `STDIO`; // CLI badge with color const cliBadge = fromCli === 'codex' ? `Codex` : `Claude`; return `

${escapeHtml(name)}

${cliBadge} ${typeBadge}
${isHttp ? t('mcp.url') : t('mcp.cmd')} ${escapeHtml(command)}
${args.length > 0 ? `
${t('mcp.args')} ${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}
` : ''}
`; } // Copy server from one CLI to another async function copyCrossCliServer(name, config, fromCli, targetCli) { try { let endpoint, body; if (targetCli === 'codex') { // Copy from Claude to Codex endpoint = '/api/codex-mcp-add'; body = { serverName: name, serverConfig: config }; } else if (targetCli === 'project') { // Copy from Codex to Claude project endpoint = '/api/mcp-copy-server'; body = { projectPath, serverName: name, serverConfig: config, configType: 'mcp' }; } else if (targetCli === 'global') { // Copy to Claude global endpoint = '/api/mcp-add-global-server'; body = { serverName: name, serverConfig: config }; } const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await res.json(); if (data.success) { const targetName = targetCli === 'codex' ? 'Codex' : 'Claude'; showToast(t('mcp.success'), `${t('mcp.serverInstalled')} (${targetName})`, 'success'); await loadMcpConfig(); renderMcpManager(); } else { showToast(t('mcp.error'), data.error, 'error'); } } catch (error) { showToast(t('mcp.error'), error.message, 'error'); } } // ======================================== // Codex MCP Create Modal // ======================================== function openCodexMcpCreateModal() { // Reuse the existing modal with different settings const modal = document.getElementById('mcpCreateModal'); if (modal) { modal.classList.remove('hidden'); // Reset to form mode mcpCreateMode = 'form'; switchMcpCreateTab('form'); // Clear form document.getElementById('mcpServerName').value = ''; document.getElementById('mcpServerCommand').value = ''; document.getElementById('mcpServerArgs').value = ''; document.getElementById('mcpServerEnv').value = ''; // Clear JSON input document.getElementById('mcpServerJson').value = ''; document.getElementById('mcpJsonPreview').classList.add('hidden'); // Set scope to codex const scopeSelect = document.getElementById('mcpServerScope'); if (scopeSelect) { // Add codex option if not exists if (!scopeSelect.querySelector('option[value="codex"]')) { const codexOption = document.createElement('option'); codexOption.value = 'codex'; codexOption.textContent = t('mcp.codex.scopeCodex'); scopeSelect.appendChild(codexOption); } scopeSelect.value = 'codex'; } // Focus on name input document.getElementById('mcpServerName').focus(); // Setup JSON input listener setupMcpJsonListener(); } } function attachMcpEventListeners() { // Debug: Log event listener attachment const viewDetailsCards = document.querySelectorAll('.mcp-server-card[data-action="view-details"]'); const codexCards = document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]'); console.log('[MCP] Attaching event listeners - Claude cards:', viewDetailsCards.length, 'Codex cards:', codexCards.length); // Toggle switches document.querySelectorAll('.mcp-server-card input[data-action="toggle"]').forEach(input => { input.addEventListener('change', async (e) => { const serverName = e.target.dataset.serverName; const enable = e.target.checked; await toggleMcpServer(serverName, enable); }); }); // Add from other projects (with scope selection) document.querySelectorAll('.mcp-server-card button[data-action="add-from-other"]').forEach(btn => { btn.addEventListener('click', async (e) => { try { const serverName = btn.dataset.serverName; const serverConfig = decodeConfigData(btn.dataset.serverConfig); const scope = btn.dataset.scope; // 'project' or 'global' if (scope === 'global') { await addGlobalMcpServer(serverName, serverConfig); } else { await copyMcpServerToProject(serverName, serverConfig); } } catch (err) { console.error('[MCP] Error adding server from other project:', err); } }); }); // Remove buttons (project-level) document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; if (confirm(t('mcp.removeConfirm', { name: serverName }))) { await removeMcpServerFromProject(serverName); } }); }); // Remove buttons (global-level) document.querySelectorAll('.mcp-server-card button[data-action="remove-global"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; if (confirm(t('mcp.removeGlobalConfirm', { name: serverName }))) { await removeGlobalMcpServer(serverName); } }); }); // Install to project buttons document.querySelectorAll('.mcp-server-card button[data-action="install-to-project"]').forEach(btn => { btn.addEventListener('click', async (e) => { try { const serverName = btn.dataset.serverName; const serverConfig = decodeConfigData(btn.dataset.serverConfig); await copyMcpServerToProject(serverName, serverConfig); } catch (err) { console.error('[MCP] Error installing to project:', err); } }); }); // Install to global buttons document.querySelectorAll('.mcp-server-card button[data-action="install-to-global"]').forEach(btn => { btn.addEventListener('click', async (e) => { try { const serverName = btn.dataset.serverName; const serverConfig = decodeConfigData(btn.dataset.serverConfig); await addGlobalMcpServer(serverName, serverConfig); } catch (err) { console.error('[MCP] Error installing to global:', err); } }); }); // Save as template buttons document.querySelectorAll('.mcp-server-card button[data-action="save-as-template"]').forEach(btn => { btn.addEventListener('click', async (e) => { try { const serverName = btn.dataset.serverName; const serverConfig = decodeConfigData(btn.dataset.serverConfig); await saveMcpAsTemplate(serverName, serverConfig); } catch (err) { console.error('[MCP] Error saving as template:', err); } }); }); // Install from template buttons document.querySelectorAll('.mcp-template-card button[data-action="install-template"]').forEach(btn => { btn.addEventListener('click', async (e) => { const templateName = btn.dataset.templateName; const scope = btn.dataset.scope || 'project'; await installFromTemplate(templateName, scope); }); }); // Delete template buttons document.querySelectorAll('.mcp-template-card button[data-action="delete-template"]').forEach(btn => { btn.addEventListener('click', async (e) => { const templateName = btn.dataset.templateName; if (confirm(t('mcp.deleteTemplateConfirm', { name: templateName }))) { await deleteMcpTemplate(templateName); } }); }); // ======================================== // CCW Tools MCP Event Listeners // ======================================== // CCW Tools action buttons (workspace/global install/update) const ccwActions = { 'update-ccw-workspace': () => updateCcwToolsMcp('workspace'), 'update-ccw-global': () => updateCcwToolsMcp('global'), 'install-ccw-workspace': () => installCcwToolsMcp('workspace'), 'install-ccw-global': () => installCcwToolsMcp('global'), 'install-ccw-codex': () => installCcwToolsMcpToCodex() }; // Mode-specific and conditionally rendered actions (don't warn if not found) const conditionalActions = new Set([ 'install-ccw-codex', // Only in Codex mode 'update-ccw-workspace', // Only if ccw-tools installed 'update-ccw-global' // Only if ccw-tools installed ]); Object.entries(ccwActions).forEach(([action, handler]) => { const btns = document.querySelectorAll(`button[data-action="${action}"]`); if (btns.length > 0) { console.log(`[MCP] Attaching listener to ${action} (${btns.length} button(s) found)`); btns.forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); console.log(`[MCP] Button clicked: ${action}`); try { await handler(); } catch (err) { console.error(`[MCP] Error executing handler for ${action}:`, err); if (typeof showRefreshToast === 'function') { showRefreshToast(`Action failed: ${err.message}`, 'error'); } } }); }); } else if (!conditionalActions.has(action)) { // Only warn if button is not conditionally rendered console.warn(`[MCP] No buttons found for action: ${action}`); } }); // ======================================== // Codex MCP Event Listeners // ======================================== // Toggle Codex MCP servers document.querySelectorAll('.mcp-server-card input[data-action="toggle-codex"]').forEach(input => { input.addEventListener('change', async (e) => { const serverName = e.target.dataset.serverName; const enable = e.target.checked; await toggleCodexMcpServer(serverName, enable); }); }); // Remove Codex MCP servers document.querySelectorAll('.mcp-server-card button[data-action="remove-codex"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; if (confirm(t('mcp.codex.removeConfirm', { name: serverName }))) { await removeCodexMcpServer(serverName); } }); }); // Copy Claude servers to Codex document.querySelectorAll('button[data-action="copy-to-codex"]').forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); try { const serverName = btn.dataset.serverName; const serverConfig = decodeConfigData(btn.dataset.serverConfig); console.log('[MCP] Copying to Codex:', serverName); await copyClaudeServerToCodex(serverName, serverConfig); } catch (err) { console.error('[MCP] Error copying to Codex:', err); } }); }); // Copy Codex servers to Claude document.querySelectorAll('button[data-action="copy-codex-to-claude"]').forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const serverName = btn.dataset.serverName; let serverConfig; try { serverConfig = decodeConfigData(btn.dataset.serverConfig); } catch (err) { console.error('[MCP] JSON Parse Error:', err); if (typeof showRefreshToast === 'function') { showRefreshToast('Failed to parse server configuration', 'error'); } return; } console.log('[MCP] Copying Codex to Claude:', serverName); await copyCodexServerToClaude(serverName, serverConfig); }); }); // Copy servers across CLI tools document.querySelectorAll('button[data-action="copy-cross-cli"]').forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const serverName = btn.dataset.serverName; let serverConfig; try { serverConfig = decodeConfigData(btn.dataset.serverConfig); } catch (err) { console.error('[MCP] JSON Parse Error:', err); if (typeof showRefreshToast === 'function') { showRefreshToast('Failed to parse server configuration', 'error'); } return; } const fromCli = btn.dataset.fromCli; const targetCli = btn.dataset.targetCli; console.log('[MCP] Copying cross-CLI:', serverName, 'from', fromCli, 'to', targetCli); await copyCrossCliServer(serverName, serverConfig, fromCli, targetCli); }); }); // View details / Edit - click on Claude server card document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => { card.addEventListener('click', (e) => { // Don't trigger if clicking on buttons or toggle if (e.target.closest('button') || e.target.closest('label') || e.target.closest('input')) { return; } try { const serverName = card.dataset.serverName; const configData = card.dataset.serverConfig; if (!configData) { console.error('[MCP] Missing server config for:', serverName); return; } const serverConfig = decodeConfigData(configData); if (!serverConfig) { console.error('[MCP] Failed to decode server config for:', serverName); return; } const serverSource = card.dataset.serverSource; console.log('[MCP] Card clicked:', serverName, serverSource); showMcpEditModal(serverName, serverConfig, serverSource, 'claude'); } catch (err) { console.error('[MCP] Error handling card click:', err); } }); }); // View details / Edit - click on Codex server card document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]').forEach(card => { card.addEventListener('click', (e) => { // Don't trigger if clicking on buttons or toggle if (e.target.closest('button') || e.target.closest('label') || e.target.closest('input')) { return; } try { const serverName = card.dataset.serverName; const configData = card.dataset.serverConfig; if (!configData) { console.error('[MCP] Missing server config for:', serverName); return; } const serverConfig = decodeConfigData(configData); if (!serverConfig) { console.error('[MCP] Failed to decode server config for:', serverName); return; } console.log('[MCP] Codex card clicked:', serverName); showMcpEditModal(serverName, serverConfig, 'codex', 'codex'); } catch (err) { console.error('[MCP] Error handling Codex card click:', err); } }); }); // Modal close button const closeBtn = document.getElementById('mcpDetailsModalClose'); const modal = document.getElementById('mcpDetailsModal'); if (closeBtn && modal) { closeBtn.addEventListener('click', () => { modal.classList.add('hidden'); }); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.add('hidden'); } }); } } // ======================================== // MCP Edit Modal (replaces Details Modal) // ======================================== // Store current editing context let mcpEditContext = { serverName: null, serverConfig: null, serverSource: null, cliType: 'claude' }; function showMcpDetails(serverName, serverConfig, serverSource, cliType = 'claude') { showMcpEditModal(serverName, serverConfig, serverSource, cliType); } function showMcpEditModal(serverName, serverConfig, serverSource, cliType = 'claude') { const modal = document.getElementById('mcpDetailsModal'); const modalBody = document.getElementById('mcpDetailsModalBody'); if (!modal || !modalBody) return; // Store editing context mcpEditContext = { serverName, serverConfig: JSON.parse(JSON.stringify(serverConfig)), // Deep clone serverSource, cliType }; // Check if editable (enterprise is read-only) const isReadOnly = serverSource === 'enterprise'; const isCodex = cliType === 'codex'; // Build source badge let sourceBadge = ''; if (serverSource === 'enterprise') { sourceBadge = `${t('mcp.sourceEnterprise')}`; } else if (serverSource === 'global') { sourceBadge = `${t('mcp.sourceGlobal')}`; } else if (serverSource === 'project') { sourceBadge = `${t('mcp.sourceProject')}`; } else if (isCodex) { sourceBadge = `Codex`; } // Format args and env for textarea const argsText = (serverConfig.args || []).join('\n'); const envText = Object.entries(serverConfig.env || {}).map(([k, v]) => `${k}=${v}`).join('\n'); // Build edit form HTML modalBody.innerHTML = `
${sourceBadge}
${isCodex ? `
` : ''}
Raw JSON
${escapeHtml(JSON.stringify(serverConfig, null, 2))}
${!isReadOnly ? `
${serverSource === 'project' || isCodex ? ` ` : ''}
` : `
`}
`; // Update modal title const modalTitle = modal.querySelector('h2'); if (modalTitle) { modalTitle.textContent = isReadOnly ? t('mcp.detailsModal.title') : t('mcp.editModal.title'); } // Show modal modal.classList.remove('hidden'); // Re-initialize Lucide icons in modal if (typeof lucide !== 'undefined') lucide.createIcons(); // Add input listeners to update JSON preview if (!isReadOnly) { ['mcpEditCommand', 'mcpEditArgs', 'mcpEditEnv', 'mcpEditEnabledTools'].forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', updateMcpEditJsonPreview); } }); } } function closeMcpEditModal() { const modal = document.getElementById('mcpDetailsModal'); if (modal) { modal.classList.add('hidden'); } mcpEditContext = { serverName: null, serverConfig: null, serverSource: null, cliType: 'claude' }; } function updateMcpEditJsonPreview() { const preview = document.getElementById('mcpEditJsonPreview'); if (!preview) return; const config = buildConfigFromEditForm(); preview.textContent = JSON.stringify(config, null, 2); } function buildConfigFromEditForm() { const command = document.getElementById('mcpEditCommand')?.value.trim() || ''; const argsText = document.getElementById('mcpEditArgs')?.value.trim() || ''; const envText = document.getElementById('mcpEditEnv')?.value.trim() || ''; const enabledToolsEl = document.getElementById('mcpEditEnabledTools'); // Build config const config = {}; // Command or URL if (mcpEditContext.serverConfig?.url) { config.url = command; } else { config.command = command; } // Args if (argsText) { config.args = argsText.split('\n').map(a => a.trim()).filter(a => a); } // Env if (envText) { config.env = {}; envText.split('\n').forEach(line => { const trimmed = line.trim(); if (trimmed && trimmed.includes('=')) { const eqIndex = trimmed.indexOf('='); const key = trimmed.substring(0, eqIndex).trim(); const value = trimmed.substring(eqIndex + 1).trim(); if (key) { config.env[key] = value; } } }); } // Codex-specific: enabled_tools if (enabledToolsEl) { const toolsText = enabledToolsEl.value.trim(); if (toolsText) { config.enabled_tools = toolsText.split('\n').map(t => t.trim()).filter(t => t); } } return config; } async function saveMcpEdit() { const newName = document.getElementById('mcpEditName')?.value.trim(); if (!newName) { showRefreshToast(t('mcp.editModal.nameRequired'), 'error'); return; } const newConfig = buildConfigFromEditForm(); if (!newConfig.command && !newConfig.url) { showRefreshToast(t('mcp.editModal.commandRequired'), 'error'); return; } const { serverName, serverSource, cliType } = mcpEditContext; const nameChanged = newName !== serverName; try { if (cliType === 'codex') { // Codex MCP update // If name changed, remove old and add new if (nameChanged) { await removeCodexMcpServer(serverName); } await addCodexMcpServer(newName, newConfig); } else if (serverSource === 'global') { // Global MCP update if (nameChanged) { await removeGlobalMcpServer(serverName); } await addGlobalMcpServer(newName, newConfig); } else if (serverSource === 'project') { // Project MCP update if (nameChanged) { await removeMcpServerFromProject(serverName); } await copyMcpServerToProject(newName, newConfig, 'mcp'); } closeMcpEditModal(); showRefreshToast(t('mcp.editModal.saved', { name: newName }), 'success'); } catch (err) { console.error('Failed to save MCP edit:', err); showRefreshToast(t('mcp.editModal.saveFailed') + ': ' + err.message, 'error'); } } async function deleteMcpFromEdit() { const { serverName, serverSource, cliType } = mcpEditContext; if (!confirm(t('mcp.editModal.deleteConfirm', { name: serverName }))) { return; } try { if (cliType === 'codex') { await removeCodexMcpServer(serverName); } else if (serverSource === 'global') { await removeGlobalMcpServer(serverName); } else if (serverSource === 'project') { await removeMcpServerFromProject(serverName); } closeMcpEditModal(); showRefreshToast(t('mcp.editModal.deleted', { name: serverName }), 'success'); } catch (err) { console.error('Failed to delete MCP:', err); showRefreshToast(t('mcp.editModal.deleteFailed') + ': ' + err.message, 'error'); } } // ======================================== // MCP Template Management Functions // ======================================== let mcpTemplates = []; /** * Load all MCP templates from API */ async function loadMcpTemplates() { try { const response = await fetch('/api/mcp-templates'); const data = await response.json(); if (data.success) { mcpTemplates = data.templates || []; console.log('[MCP Templates] Loaded', mcpTemplates.length, 'templates'); } else { console.error('[MCP Templates] Failed to load:', data.error); mcpTemplates = []; } return mcpTemplates; } catch (error) { console.error('[MCP Templates] Error loading templates:', error); mcpTemplates = []; return []; } } /** * Save MCP server configuration as a template */ async function saveMcpAsTemplate(serverName, serverConfig) { try { // Prompt for template name and description const templateName = prompt(t('mcp.enterTemplateName'), serverName); if (!templateName) return; const description = prompt(t('mcp.enterTemplateDesc'), `Template for ${serverName}`); const payload = { name: templateName, description: description || '', serverConfig: serverConfig, category: 'user' }; const response = await fetch('/api/mcp-templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); if (data.success) { showRefreshToast(t('mcp.templateSaved', { name: templateName }), 'success'); await loadMcpTemplates(); await renderMcpManager(); // Refresh view } else { showRefreshToast(t('mcp.templateSaveFailed', { error: data.error }), 'error'); } } catch (error) { console.error('[MCP] Save template error:', error); showRefreshToast(t('mcp.templateSaveFailed', { error: error.message }), 'error'); } } /** * Install MCP server from template */ async function installFromTemplate(templateName, scope = 'project') { try { // Find template const template = mcpTemplates.find(t => t.name === templateName); if (!template) { showRefreshToast(t('mcp.templateNotFound', { name: templateName }), 'error'); return; } // Prompt for server name (default to template name) const serverName = prompt(t('mcp.enterServerName'), templateName); if (!serverName) return; // Install based on scope if (scope === 'project') { await copyMcpServerToProject(serverName, template.serverConfig); } else if (scope === 'global') { await addGlobalMcpServer(serverName, template.serverConfig); } showRefreshToast(t('mcp.templateInstalled', { name: serverName }), 'success'); await renderMcpManager(); } catch (error) { console.error('[MCP] Install from template error:', error); showRefreshToast(t('mcp.templateInstallFailed', { error: error.message }), 'error'); } } /** * Delete MCP template */ async function deleteMcpTemplate(templateName) { try { const response = await fetch(`/api/mcp-templates/${encodeURIComponent(templateName)}`, { method: 'DELETE' }); const data = await response.json(); if (data.success) { showRefreshToast(t('mcp.templateDeleted', { name: templateName }), 'success'); await loadMcpTemplates(); await renderMcpManager(); } else { showRefreshToast(t('mcp.templateDeleteFailed', { error: data.error }), 'error'); } } catch (error) { console.error('[MCP] Delete template error:', error); showRefreshToast(t('mcp.templateDeleteFailed', { error: error.message }), 'error'); } } // ========== Global Exports for onclick handlers ========== // Expose functions to global scope to support inline onclick handlers window.openCodexMcpCreateModal = openCodexMcpCreateModal; window.closeMcpEditModal = closeMcpEditModal; window.saveMcpEdit = saveMcpEdit; window.deleteMcpFromEdit = deleteMcpFromEdit; window.saveMcpAsTemplate = saveMcpAsTemplate;