// 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('')}
`}
` : `
${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')}
| ${t('mcp.project')} |
${t('mcp.servers')} |
${t('mcp.status')} |
${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 `
${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}
|
`;
}).join('')}
` : ''}
`}
${t('mcp.detailsModal.title')}
`;
// 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 `
${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 = `
${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');
}
}