Fix MCP Manager panel - 13 critical issues resolved

Issues fixed:
1. API endpoint mismatch (/api/mcp-add-global-server)
2. Undefined function reference (installMcpToProject → copyMcpServerToProject)
3. Inline onclick handler scope issues (converted to data-action)
4. querySelector only finding first element (use querySelectorAll)
5. Path normalization causing wrong MCP badge count
6. Lucide icons destroying event listeners (reordered execution)
7. Codex button JSON serialization syntax error
8. Codex API routes 404 (add /api/codex-mcp route matching)
9. False warning suppression for conditional buttons
10. HTML syntax error from JSON in onclick attributes
11. CSS z-index issue - ::before pseudo-element blocking clicks
12. Navigation badge path matching (try both slash formats)
13. Remove deprecated codex_lens tool (merged into smart_search)

Key changes:
- server.ts: Add /api/codex-mcp route matching
- 15-mcp-manager.css: Add pointer-events:none to ::before, z-index for buttons
- components/mcp-manager.js: Fix updateMcpBadge path matching, global exports
- views/mcp-manager.js: Convert onclick to data-action, add event listeners,
  update CCW_MCP_TOOLS list, fix JSON escaping in HTML attributes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-17 19:23:39 +08:00
parent c16da759b2
commit 8b927f302c
4 changed files with 170 additions and 25 deletions

View File

@@ -269,8 +269,8 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
if (await handleMemoryRoutes(routeContext)) return; if (await handleMemoryRoutes(routeContext)) return;
} }
// MCP routes (/api/mcp*) // MCP routes (/api/mcp*, /api/codex-mcp*)
if (pathname.startsWith('/api/mcp')) { if (pathname.startsWith('/api/mcp') || pathname.startsWith('/api/codex-mcp')) {
if (await handleMcpRoutes(routeContext)) return; if (await handleMcpRoutes(routeContext)) return;
} }

View File

@@ -218,6 +218,16 @@ button.icon-btn {
background: radial-gradient(circle, rgba(249, 115, 22, 0.1) 0%, transparent 70%); background: radial-gradient(circle, rgba(249, 115, 22, 0.1) 0%, transparent 70%);
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
pointer-events: none; /* Prevent blocking clicks */
z-index: 0;
}
/* Ensure interactive elements are above decorative layers */
.ccw-tools-card button,
.ccw-tools-card input,
.ccw-tools-card label {
position: relative;
z-index: 1;
} }
.ccw-tools-card:hover::before { .ccw-tools-card:hover::before {

View File

@@ -395,14 +395,19 @@ async function removeGlobalMcpServer(serverName) {
function updateMcpBadge() { function updateMcpBadge() {
const badge = document.getElementById('badgeMcpServers'); const badge = document.getElementById('badgeMcpServers');
if (badge) { if (badge) {
const currentPath = projectPath; // Keep original format (forward slash) // Try both path formats to find the matching key
const projectData = mcpAllProjects[currentPath]; const forwardSlashPath = projectPath.replace(/\\/g, '/');
const backSlashPath = projectPath.replace(/\//g, '\\');
// Find matching project data using either path format
const projectData = mcpAllProjects[forwardSlashPath] || mcpAllProjects[backSlashPath] || mcpAllProjects[projectPath];
const servers = projectData?.mcpServers || {}; const servers = projectData?.mcpServers || {};
const disabledServers = projectData?.disabledMcpServers || []; const disabledServers = projectData?.disabledMcpServers || [];
const totalServers = Object.keys(servers).length; const totalServers = Object.keys(servers).length;
const enabledServers = totalServers - disabledServers.length; const enabledServers = totalServers - disabledServers.length;
console.log('[MCP Badge]', { projectPath, forwardSlashPath, backSlashPath, totalServers, enabledServers });
badge.textContent = `${enabledServers}/${totalServers}`; badge.textContent = `${enabledServers}/${totalServers}`;
} }
} }
@@ -957,7 +962,7 @@ async function installCcwToolsMcp(scope = 'workspace') {
if (scope === 'global') { if (scope === 'global') {
// Install to global (~/.claude.json mcpServers) // Install to global (~/.claude.json mcpServers)
const response = await fetch('/api/mcp-add-global', { const response = await fetch('/api/mcp-add-global-server', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -1021,7 +1026,7 @@ async function updateCcwToolsMcp(scope = 'workspace') {
if (scope === 'global') { if (scope === 'global') {
// Update global (~/.claude.json mcpServers) // Update global (~/.claude.json mcpServers)
const response = await fetch('/api/mcp-add-global', { const response = await fetch('/api/mcp-add-global-server', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@@ -1124,3 +1129,11 @@ async function installCcwToolsMcpToCodex() {
showRefreshToast(`Failed to install CCW Tools MCP to Codex: ${err.message}`, 'error'); showRefreshToast(`Failed to install CCW Tools MCP to Codex: ${err.message}`, 'error');
} }
} }
// ========== Global Exports for onclick handlers ==========
// Expose functions to global scope to support inline onclick handlers
window.setCliMode = setCliMode;
window.getCliMode = getCliMode;
window.selectCcwTools = selectCcwTools;
window.selectCcwToolsCodex = selectCcwToolsCodex;
window.openMcpCreateModal = openMcpCreateModal;

View File

@@ -6,8 +6,7 @@ const CCW_MCP_TOOLS = [
// Core tools (always recommended) // Core tools (always recommended)
{ name: 'write_file', desc: 'Write/create files', core: true }, { name: 'write_file', desc: 'Write/create files', core: true },
{ name: 'edit_file', desc: 'Edit/replace content', core: true }, { name: 'edit_file', desc: 'Edit/replace content', core: true },
{ name: 'codex_lens', desc: 'Code index & search', core: true }, { name: 'smart_search', desc: 'Hybrid search (regex + semantic)', core: true },
{ name: 'smart_search', desc: 'Quick regex/NL search', core: true },
// Optional tools // Optional tools
{ name: 'session_manager', desc: 'Workflow sessions', core: false }, { name: 'session_manager', desc: 'Workflow sessions', core: false },
{ name: 'generate_module_docs', desc: 'Generate docs', core: false }, { name: 'generate_module_docs', desc: 'Generate docs', core: false },
@@ -236,7 +235,7 @@ async function renderMcpManager() {
</div> </div>
<div class="shrink-0"> <div class="shrink-0">
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1" <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="installCcwToolsMcpToCodex()"> data-action="install-ccw-codex">
<i data-lucide="download" class="w-4 h-4"></i> <i data-lucide="download" class="w-4 h-4"></i>
${codexMcpServers && codexMcpServers['ccw-tools'] ? t('mcp.update') : t('mcp.install')} ${codexMcpServers && codexMcpServers['ccw-tools'] ? t('mcp.update') : t('mcp.install')}
</button> </button>
@@ -322,7 +321,9 @@ async function renderMcpManager() {
</div> </div>
${!alreadyInCodex ? ` ${!alreadyInCodex ? `
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity" <button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
onclick="copyClaudeServerToCodex('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})" data-action="copy-to-codex"
data-server-name="${escapeHtml(serverName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
title="${t('mcp.codex.copyToCodex')}"> title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex <i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button> </button>
@@ -421,26 +422,26 @@ async function renderMcpManager() {
<div class="shrink-0 flex gap-2"> <div class="shrink-0 flex gap-2">
${isCcwToolsInstalled ? ` ${isCcwToolsInstalled ? `
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1" <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="updateCcwToolsMcp('workspace')" data-action="update-ccw-workspace"
title="${t('mcp.updateInWorkspace')}"> title="${t('mcp.updateInWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i> <i data-lucide="folder" class="w-4 h-4"></i>
${t('mcp.updateInWorkspace')} ${t('mcp.updateInWorkspace')}
</button> </button>
<button class="px-4 py-2 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1" <button class="px-4 py-2 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="updateCcwToolsMcp('global')" data-action="update-ccw-global"
title="${t('mcp.updateInGlobal')}"> title="${t('mcp.updateInGlobal')}">
<i data-lucide="globe" class="w-4 h-4"></i> <i data-lucide="globe" class="w-4 h-4"></i>
${t('mcp.updateInGlobal')} ${t('mcp.updateInGlobal')}
</button> </button>
` : ` ` : `
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1" <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="installCcwToolsMcp('workspace')" data-action="install-ccw-workspace"
title="${t('mcp.installToWorkspace')}"> title="${t('mcp.installToWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i> <i data-lucide="folder" class="w-4 h-4"></i>
${t('mcp.installToWorkspace')} ${t('mcp.installToWorkspace')}
</button> </button>
<button class="px-4 py-2 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1" <button class="px-4 py-2 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
onclick="installCcwToolsMcp('global')" data-action="install-ccw-global"
title="${t('mcp.installToGlobal')}"> title="${t('mcp.installToGlobal')}">
<i data-lucide="globe" class="w-4 h-4"></i> <i data-lucide="globe" class="w-4 h-4"></i>
${t('mcp.installToGlobal')} ${t('mcp.installToGlobal')}
@@ -658,7 +659,9 @@ async function renderMcpManager() {
</div> </div>
${!alreadyInClaude ? ` ${!alreadyInClaude ? `
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity" <button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
onclick="copyCodexServerToClaude('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})" data-action="copy-codex-to-claude"
data-server-name="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
title="${t('mcp.claude.copyToClaude')}"> title="${t('mcp.claude.copyToClaude')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude <i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
</button> </button>
@@ -769,11 +772,12 @@ async function renderMcpManager() {
</div> </div>
`; `;
// Attach event listeners for toggle switches // Initialize Lucide icons FIRST (before attaching event listeners)
attachMcpEventListeners(); // lucide.createIcons() may replace DOM elements, which would remove event listeners
// Initialize Lucide icons
if (typeof lucide !== 'undefined') lucide.createIcons(); if (typeof lucide !== 'undefined') lucide.createIcons();
// Attach event listeners AFTER icon initialization
attachMcpEventListeners();
} }
// Render card for Project Available MCP (current project can use) // Render card for Project Available MCP (current project can use)
@@ -1040,7 +1044,9 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
</div> </div>
${!alreadyInCodex ? ` ${!alreadyInCodex ? `
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity" <button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
onclick="copyClaudeServerToCodex('${escapeHtml(originalName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})" data-action="copy-to-codex"
data-server-name="${escapeHtml(originalName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
title="${t('mcp.codex.copyToCodex')}"> title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex <i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button> </button>
@@ -1066,7 +1072,9 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
<div class="mt-3 pt-3 border-t border-border flex items-center gap-2"> <div class="mt-3 pt-3 border-t border-border flex items-center gap-2">
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1" <button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
onclick="copyClaudeServerToCodex('${escapeHtml(originalName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})" data-action="copy-to-codex"
data-server-name="${escapeHtml(originalName)}"
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
title="${t('mcp.codex.copyToCodex')}"> title="${t('mcp.codex.copyToCodex')}">
<i data-lucide="download" class="w-3 h-3"></i> <i data-lucide="download" class="w-3 h-3"></i>
${t('mcp.codex.install')} ${t('mcp.codex.install')}
@@ -1144,7 +1152,9 @@ function renderCodexServerCard(serverName, serverConfig) {
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between gap-2" onclick="event.stopPropagation()"> <div class="mt-3 pt-3 border-t border-border flex items-center justify-between gap-2" onclick="event.stopPropagation()">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1" <button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
onclick="copyCodexServerToClaude('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "&#39;")})" data-action="copy-codex-to-claude"
data-server-name="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
title="${t('mcp.codex.copyToClaude')}"> title="${t('mcp.codex.copyToClaude')}">
<i data-lucide="copy" class="w-3 h-3"></i> <i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.codex.copyToClaude')} ${t('mcp.codex.copyToClaude')}
@@ -1212,7 +1222,11 @@ function renderCrossCliServerCard(server, isClaude) {
</div> </div>
<div class="mt-3 pt-3 border-t border-border"> <div class="mt-3 pt-3 border-t border-border">
<button class="w-full px-3 py-2 text-sm font-medium bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg transition-colors flex items-center justify-center gap-1.5" <button class="w-full px-3 py-2 text-sm font-medium bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg transition-colors flex items-center justify-center gap-1.5"
onclick="copyCrossCliServer('${escapeHtml(name)}', ${JSON.stringify(config).replace(/'/g, "&#39;")}, '${fromCli}', '${targetCli}')"> data-action="copy-cross-cli"
data-server-name="${escapeHtml(name)}"
data-server-config='${JSON.stringify(config).replace(/'/g, "&#39;")}'
data-from-cli="${fromCli}"
data-target-cli="${targetCli}">
<i data-lucide="copy" class="w-4 h-4"></i> <i data-lucide="copy" class="w-4 h-4"></i>
${buttonText} ${buttonText}
</button> </button>
@@ -1349,7 +1363,7 @@ function attachMcpEventListeners() {
btn.addEventListener('click', async (e) => { btn.addEventListener('click', async (e) => {
const serverName = btn.dataset.serverName; const serverName = btn.dataset.serverName;
const serverConfig = JSON.parse(btn.dataset.serverConfig); const serverConfig = JSON.parse(btn.dataset.serverConfig);
await installMcpToProject(serverName, serverConfig); await copyMcpServerToProject(serverName, serverConfig);
}); });
}); });
@@ -1390,6 +1404,51 @@ function attachMcpEventListeners() {
}); });
}); });
// ========================================
// 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 // Codex MCP Event Listeners
// ======================================== // ========================================
@@ -1413,6 +1472,61 @@ function attachMcpEventListeners() {
}); });
}); });
// Copy Claude servers to Codex
document.querySelectorAll('button[data-action="copy-to-codex"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
const serverName = btn.dataset.serverName;
const serverConfig = JSON.parse(btn.dataset.serverConfig);
console.log('[MCP] Copying to Codex:', serverName);
await copyClaudeServerToCodex(serverName, serverConfig);
});
});
// 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 = JSON.parse(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 = JSON.parse(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 // View details / Edit - click on Claude server card
document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => { document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => {
card.addEventListener('click', (e) => { card.addEventListener('click', (e) => {
@@ -1844,7 +1958,7 @@ async function installFromTemplate(templateName, scope = 'project') {
// Install based on scope // Install based on scope
if (scope === 'project') { if (scope === 'project') {
await installMcpToProject(serverName, template.serverConfig); await copyMcpServerToProject(serverName, template.serverConfig);
} else if (scope === 'global') { } else if (scope === 'global') {
await addGlobalMcpServer(serverName, template.serverConfig); await addGlobalMcpServer(serverName, template.serverConfig);
} }
@@ -1880,3 +1994,11 @@ async function deleteMcpTemplate(templateName) {
showRefreshToast(t('mcp.templateDeleteFailed', { error: error.message }), '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;