mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
fix(mcp): support same-name MCP servers with different configs
- Add getMcpConfigHash() to generate unique config fingerprints - Modify getAllAvailableMcpServers() to differentiate same-name servers with different configurations using name@project-folder suffix - Update renderAvailableServerCard() to show source project info and use originalName for correct installation - Fix event handlers to use btn.dataset for event bubbling safety - Add CCW Tools MCP installation with npx for cross-platform compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -159,30 +159,83 @@ function updateMcpBadge() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== Helpers ==========
|
// ========== Helpers ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique key for MCP server config comparison
|
||||||
|
* Used to distinguish servers with same name but different configurations
|
||||||
|
*/
|
||||||
|
function getMcpConfigHash(config) {
|
||||||
|
const cmd = config.command || '';
|
||||||
|
const args = (config.args || []).join('|');
|
||||||
|
const envKeys = Object.keys(config.env || {}).sort().join(',');
|
||||||
|
return `${cmd}::${args}::${envKeys}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available MCP servers from all sources
|
||||||
|
* Supports servers with same name but different configurations from different projects
|
||||||
|
*/
|
||||||
function getAllAvailableMcpServers() {
|
function getAllAvailableMcpServers() {
|
||||||
const allServers = {};
|
const allServers = {};
|
||||||
|
const configHashes = {}; // Track unique configs per server name
|
||||||
|
|
||||||
// Collect global servers first
|
// Collect global servers first
|
||||||
for (const [name, serverConfig] of Object.entries(mcpGlobalServers)) {
|
for (const [name, serverConfig] of Object.entries(mcpGlobalServers)) {
|
||||||
|
const hash = getMcpConfigHash(serverConfig);
|
||||||
allServers[name] = {
|
allServers[name] = {
|
||||||
config: serverConfig,
|
config: serverConfig,
|
||||||
usedIn: [],
|
usedIn: [],
|
||||||
isGlobal: true
|
isGlobal: true,
|
||||||
|
configHash: hash
|
||||||
};
|
};
|
||||||
|
configHashes[name] = { [hash]: name };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect servers from all projects
|
// Collect servers from all projects - handle same name with different configs
|
||||||
for (const [path, config] of Object.entries(mcpAllProjects)) {
|
for (const [path, config] of Object.entries(mcpAllProjects)) {
|
||||||
const servers = config.mcpServers || {};
|
const servers = config.mcpServers || {};
|
||||||
for (const [name, serverConfig] of Object.entries(servers)) {
|
for (const [name, serverConfig] of Object.entries(servers)) {
|
||||||
if (!allServers[name]) {
|
const hash = getMcpConfigHash(serverConfig);
|
||||||
allServers[name] = {
|
|
||||||
config: serverConfig,
|
if (!configHashes[name]) {
|
||||||
usedIn: [],
|
// First occurrence of this server name
|
||||||
isGlobal: false
|
configHashes[name] = {};
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (!configHashes[name][hash]) {
|
||||||
|
// New unique configuration for this server name
|
||||||
|
// Use suffixed key if name already exists with different config
|
||||||
|
let serverKey = name;
|
||||||
|
if (allServers[name] && allServers[name].configHash !== hash) {
|
||||||
|
// Generate unique key: name@project-folder
|
||||||
|
const projectFolder = path.split('\\').pop() || path.split('/').pop() || 'unknown';
|
||||||
|
serverKey = `${name}@${projectFolder}`;
|
||||||
|
// Avoid collisions
|
||||||
|
let suffix = 1;
|
||||||
|
while (allServers[serverKey]) {
|
||||||
|
serverKey = `${name}@${projectFolder}-${suffix++}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configHashes[name][hash] = serverKey;
|
||||||
|
|
||||||
|
if (!allServers[serverKey]) {
|
||||||
|
allServers[serverKey] = {
|
||||||
|
config: serverConfig,
|
||||||
|
usedIn: [],
|
||||||
|
isGlobal: false,
|
||||||
|
configHash: hash,
|
||||||
|
originalName: name, // Store original name for installation
|
||||||
|
sourceProject: path // Store source project for reference
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which projects use this config
|
||||||
|
const serverKey = configHashes[name][hash];
|
||||||
|
if (allServers[serverKey]) {
|
||||||
|
allServers[serverKey].usedIn.push(path);
|
||||||
}
|
}
|
||||||
allServers[name].usedIn.push(path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,3 +579,42 @@ async function createMcpServerWithConfig(name, serverConfig) {
|
|||||||
showRefreshToast(`Failed to create MCP server: ${err.message}`, 'error');
|
showRefreshToast(`Failed to create MCP server: ${err.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ========== CCW Tools MCP Installation ==========
|
||||||
|
async function installCcwToolsMcp() {
|
||||||
|
// Define CCW Tools MCP server configuration
|
||||||
|
// Use npx for better cross-platform compatibility (handles PATH issues)
|
||||||
|
const ccwToolsConfig = {
|
||||||
|
command: "npx",
|
||||||
|
args: ["-y", "ccw-mcp"]
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading toast
|
||||||
|
showRefreshToast('Installing CCW Tools MCP...', 'info');
|
||||||
|
|
||||||
|
// Use the existing copyMcpServerToProject function
|
||||||
|
const response = await fetch('/api/mcp-copy-server', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
projectPath: projectPath,
|
||||||
|
serverName: 'ccw-tools',
|
||||||
|
serverConfig: ccwToolsConfig
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to install CCW Tools MCP');
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
await loadMcpConfig();
|
||||||
|
renderMcpManager();
|
||||||
|
showRefreshToast('CCW Tools MCP installed successfully', 'success');
|
||||||
|
} else {
|
||||||
|
showRefreshToast(result.error || 'Failed to install CCW Tools MCP', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to install CCW Tools MCP:', err);
|
||||||
|
showRefreshToast(`Failed to install CCW Tools MCP: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,9 +34,72 @@ async function renderMcpManager() {
|
|||||||
.filter(([name]) => !currentProjectServerNames.includes(name) && !(mcpEnterpriseServers || {})[name]);
|
.filter(([name]) => !currentProjectServerNames.includes(name) && !(mcpEnterpriseServers || {})[name]);
|
||||||
const otherProjectServers = Object.entries(allAvailableServers)
|
const otherProjectServers = Object.entries(allAvailableServers)
|
||||||
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
||||||
|
// Check if CCW Tools is already installed
|
||||||
|
const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools");
|
||||||
|
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="mcp-manager">
|
<div class="mcp-manager">
|
||||||
|
<!-- CCW Tools MCP Server Card -->
|
||||||
|
<div class="mcp-section mb-6">
|
||||||
|
<div class="ccw-tools-card bg-gradient-to-br from-primary/10 to-primary/5 border-2 ${isCcwToolsInstalled ? 'border-success' : 'border-primary/30'} rounded-lg p-6 hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div class="flex items-start gap-4 flex-1">
|
||||||
|
<div class="shrink-0 w-12 h-12 bg-primary rounded-lg flex items-center justify-center">
|
||||||
|
<i data-lucide="wrench" class="w-6 h-6 text-primary-foreground"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<h3 class="text-lg font-bold text-foreground">CCW Tools MCP</h3>
|
||||||
|
${isCcwToolsInstalled ? `
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-success-light text-success">
|
||||||
|
<i data-lucide="check" class="w-3 h-3"></i>
|
||||||
|
Installed
|
||||||
|
</span>
|
||||||
|
` : `
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-primary/20 text-primary">
|
||||||
|
<i data-lucide="package" class="w-3 h-3"></i>
|
||||||
|
Available
|
||||||
|
</span>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground mb-3">
|
||||||
|
CCW built-in tools for file editing, code search, session management, and more
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<i data-lucide="layers" class="w-3 h-3"></i>
|
||||||
|
15 tools available
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<i data-lucide="zap" class="w-3 h-3"></i>
|
||||||
|
Native integration
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<i data-lucide="shield-check" class="w-3 h-3"></i>
|
||||||
|
Built-in & tested
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0">
|
||||||
|
${isCcwToolsInstalled ? `
|
||||||
|
<button class="px-4 py-2 text-sm bg-muted text-muted-foreground rounded-lg cursor-not-allowed" disabled>
|
||||||
|
<i data-lucide="check" class="w-4 h-4 inline mr-1"></i>
|
||||||
|
Installed
|
||||||
|
</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-2"
|
||||||
|
onclick="installCcwToolsMcp()">
|
||||||
|
<i data-lucide="download" class="w-4 h-4"></i>
|
||||||
|
Install CCW Tools
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Current Project MCP Servers -->
|
<!-- Current Project MCP Servers -->
|
||||||
<div class="mcp-section mb-6">
|
<div class="mcp-section mb-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@@ -259,16 +322,34 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
|||||||
const serverConfig = serverInfo.config;
|
const serverConfig = serverInfo.config;
|
||||||
const usedIn = serverInfo.usedIn || [];
|
const usedIn = serverInfo.usedIn || [];
|
||||||
const command = serverConfig.command || 'N/A';
|
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 `
|
return `
|
||||||
<div class="mcp-server-card mcp-server-available bg-card border border-border border-dashed rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
|
<div class="mcp-server-card mcp-server-available bg-card border border-border border-dashed rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<span><i data-lucide="circle-dashed" class="w-5 h-5 text-muted-foreground"></i></span>
|
<span><i data-lucide="circle-dashed" class="w-5 h-5 text-muted-foreground"></i></span>
|
||||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
<h4 class="font-semibold text-foreground">${escapeHtml(originalName)}</h4>
|
||||||
|
${hasVariant ? `
|
||||||
|
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full" title="Different config from: ${escapeHtml(sourceProject || '')}">
|
||||||
|
${escapeHtml(sourceProjectName || 'variant')}
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
<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"
|
||||||
data-server-name="${escapeHtml(serverName)}"
|
data-server-name="${escapeHtml(originalName)}"
|
||||||
|
data-server-key="${escapeHtml(serverName)}"
|
||||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||||
data-action="add">
|
data-action="add">
|
||||||
${t('mcp.add')}
|
${t('mcp.add')}
|
||||||
@@ -280,8 +361,15 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
|||||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
|
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
|
||||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${argsPreview ? `
|
||||||
|
<div class="flex items-start gap-2 text-muted-foreground">
|
||||||
|
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||||
|
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(argsPreview)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div class="flex items-center gap-2 text-muted-foreground">
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
<span class="text-xs">Used in ${usedIn.length} project${usedIn.length !== 1 ? 's' : ''}</span>
|
<span class="text-xs">Used in ${usedIn.length} project${usedIn.length !== 1 ? 's' : ''}</span>
|
||||||
|
${sourceProjectName ? `<span class="text-xs text-muted-foreground/70">• from ${escapeHtml(sourceProjectName)}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -390,19 +478,19 @@ function attachMcpEventListeners() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add buttons
|
// Add buttons - use btn.dataset instead of e.target.dataset for event bubbling safety
|
||||||
document.querySelectorAll('.mcp-server-card button[data-action="add"]').forEach(btn => {
|
document.querySelectorAll('.mcp-server-card button[data-action="add"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const serverName = e.target.dataset.serverName;
|
const serverName = btn.dataset.serverName;
|
||||||
const serverConfig = JSON.parse(e.target.dataset.serverConfig);
|
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
||||||
await copyMcpServerToProject(serverName, serverConfig);
|
await copyMcpServerToProject(serverName, serverConfig);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove buttons
|
// Remove buttons - use btn.dataset instead of e.target.dataset for event bubbling safety
|
||||||
document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => {
|
document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const serverName = e.target.dataset.serverName;
|
const serverName = btn.dataset.serverName;
|
||||||
if (confirm(t('mcp.removeConfirm', { name: serverName }))) {
|
if (confirm(t('mcp.removeConfirm', { name: serverName }))) {
|
||||||
await removeMcpServerFromProject(serverName);
|
await removeMcpServerFromProject(serverName);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user