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;
}
// MCP routes (/api/mcp*)
if (pathname.startsWith('/api/mcp')) {
// MCP routes (/api/mcp*, /api/codex-mcp*)
if (pathname.startsWith('/api/mcp') || pathname.startsWith('/api/codex-mcp')) {
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%);
opacity: 0;
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 {

View File

@@ -395,14 +395,19 @@ async function removeGlobalMcpServer(serverName) {
function updateMcpBadge() {
const badge = document.getElementById('badgeMcpServers');
if (badge) {
const currentPath = projectPath; // Keep original format (forward slash)
const projectData = mcpAllProjects[currentPath];
// Try both path formats to find the matching key
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 disabledServers = projectData?.disabledMcpServers || [];
const totalServers = Object.keys(servers).length;
const enabledServers = totalServers - disabledServers.length;
console.log('[MCP Badge]', { projectPath, forwardSlashPath, backSlashPath, totalServers, enabledServers });
badge.textContent = `${enabledServers}/${totalServers}`;
}
}
@@ -957,7 +962,7 @@ async function installCcwToolsMcp(scope = 'workspace') {
if (scope === 'global') {
// 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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1021,7 +1026,7 @@ async function updateCcwToolsMcp(scope = 'workspace') {
if (scope === 'global') {
// Update global (~/.claude.json mcpServers)
const response = await fetch('/api/mcp-add-global', {
const response = await fetch('/api/mcp-add-global-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1124,3 +1129,11 @@ async function installCcwToolsMcpToCodex() {
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)
{ 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 },
{ name: 'smart_search', desc: 'Hybrid search (regex + semantic)', core: true },
// Optional tools
{ name: 'session_manager', desc: 'Workflow sessions', core: false },
{ name: 'generate_module_docs', desc: 'Generate docs', core: false },
@@ -236,7 +235,7 @@ async function renderMcpManager() {
</div>
<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"
onclick="installCcwToolsMcpToCodex()">
data-action="install-ccw-codex">
<i data-lucide="download" class="w-4 h-4"></i>
${codexMcpServers && codexMcpServers['ccw-tools'] ? t('mcp.update') : t('mcp.install')}
</button>
@@ -322,7 +321,9 @@ async function renderMcpManager() {
</div>
${!alreadyInCodex ? `
<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')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button>
@@ -421,26 +422,26 @@ async function renderMcpManager() {
<div class="shrink-0 flex gap-2">
${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"
onclick="updateCcwToolsMcp('workspace')"
data-action="update-ccw-workspace"
title="${t('mcp.updateInWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i>
${t('mcp.updateInWorkspace')}
</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"
onclick="updateCcwToolsMcp('global')"
data-action="update-ccw-global"
title="${t('mcp.updateInGlobal')}">
<i data-lucide="globe" class="w-4 h-4"></i>
${t('mcp.updateInGlobal')}
</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"
onclick="installCcwToolsMcp('workspace')"
data-action="install-ccw-workspace"
title="${t('mcp.installToWorkspace')}">
<i data-lucide="folder" class="w-4 h-4"></i>
${t('mcp.installToWorkspace')}
</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"
onclick="installCcwToolsMcp('global')"
data-action="install-ccw-global"
title="${t('mcp.installToGlobal')}">
<i data-lucide="globe" class="w-4 h-4"></i>
${t('mcp.installToGlobal')}
@@ -658,7 +659,9 @@ async function renderMcpManager() {
</div>
${!alreadyInClaude ? `
<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')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
</button>
@@ -769,11 +772,12 @@ async function renderMcpManager() {
</div>
`;
// Attach event listeners for toggle switches
attachMcpEventListeners();
// Initialize Lucide icons
// 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)
@@ -1040,7 +1044,9 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
</div>
${!alreadyInCodex ? `
<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')}">
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
</button>
@@ -1066,7 +1072,9 @@ function renderAvailableServerCardForCodex(serverName, serverInfo) {
<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"
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')}">
<i data-lucide="download" class="w-3 h-3"></i>
${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="flex items-center gap-2">
<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')}">
<i data-lucide="copy" class="w-3 h-3"></i>
${t('mcp.codex.copyToClaude')}
@@ -1212,7 +1222,11 @@ function renderCrossCliServerCard(server, isClaude) {
</div>
<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"
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>
${buttonText}
</button>
@@ -1349,7 +1363,7 @@ function attachMcpEventListeners() {
btn.addEventListener('click', async (e) => {
const serverName = btn.dataset.serverName;
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
// ========================================
@@ -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
document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => {
card.addEventListener('click', (e) => {
@@ -1844,7 +1958,7 @@ async function installFromTemplate(templateName, scope = 'project') {
// Install based on scope
if (scope === 'project') {
await installMcpToProject(serverName, template.serverConfig);
await copyMcpServerToProject(serverName, template.serverConfig);
} else if (scope === 'global') {
await addGlobalMcpServer(serverName, template.serverConfig);
}
@@ -1880,3 +1994,11 @@ async function deleteMcpTemplate(templateName) {
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;