// 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: 'codex_lens', desc: 'Code index & search', core: true }, { name: 'smart_search', desc: 'Quick regex/NL search', 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 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); } 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(); // Prepare Codex servers data const codexServerEntries = Object.entries(codexMcpServers || {}); const codexConfigExists = codexMcpConfig?.exists || false; const codexConfigPath = codexMcpConfig?.configPath || '~/.codex/config.toml'; container.innerHTML = `
${t('mcp.cliMode')}
${currentCliMode === 'claude' ? ` ~/.claude.json` : ` ${codexConfigPath}` }
${currentCliMode === 'codex' ? `

${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('')}
`}
` : `

CCW Tools MCP

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

${t('mcp.projectAvailable')}

${hasMcpJson ? ` .mcp.json ` : ` Will use .mcp.json `}
${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('')}
`}
${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, 2).join(' '))}${template.serverConfig.args.length > 2 ? '...' : ''}
` : ''}
`).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}
` : ''} `}
`; // Attach event listeners for toggle switches attachMcpEventListeners(); // Initialize Lucide icons if (typeof lucide !== 'undefined') lucide.createIcons(); } // 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')}
` : ''}
`; } // ======================================== // 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() { // 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) => { const serverName = btn.dataset.serverName; const serverConfig = JSON.parse(btn.dataset.serverConfig); const scope = btn.dataset.scope; // 'project' or 'global' if (scope === 'global') { await addGlobalMcpServer(serverName, serverConfig); } else { await copyMcpServerToProject(serverName, serverConfig); } }); }); // 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) => { const serverName = btn.dataset.serverName; const serverConfig = JSON.parse(btn.dataset.serverConfig); await installMcpToProject(serverName, serverConfig); }); }); // Install to global buttons document.querySelectorAll('.mcp-server-card button[data-action="install-to-global"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; const serverConfig = JSON.parse(btn.dataset.serverConfig); await addGlobalMcpServer(serverName, serverConfig); }); }); // Save as template buttons document.querySelectorAll('.mcp-server-card button[data-action="save-as-template"]').forEach(btn => { btn.addEventListener('click', async (e) => { const serverName = btn.dataset.serverName; const serverConfig = JSON.parse(btn.dataset.serverConfig); await saveMcpAsTemplate(serverName, serverConfig); }); }); // 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); } }); }); // ======================================== // 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); } }); }); // View details / Edit - click on Claude server card document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => { card.addEventListener('click', (e) => { const serverName = card.dataset.serverName; const serverConfig = JSON.parse(card.dataset.serverConfig); const serverSource = card.dataset.serverSource; showMcpEditModal(serverName, serverConfig, serverSource, 'claude'); }); }); // View details / Edit - click on Codex server card document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]').forEach(card => { card.addEventListener('click', (e) => { const serverName = card.dataset.serverName; const serverConfig = JSON.parse(card.dataset.serverConfig); showMcpEditModal(serverName, serverConfig, 'codex', 'codex'); }); }); // 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 installMcpToProject(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'); } }