mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +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 ==========
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
const allServers = {};
|
||||
const configHashes = {}; // Track unique configs per server name
|
||||
|
||||
// Collect global servers first
|
||||
for (const [name, serverConfig] of Object.entries(mcpGlobalServers)) {
|
||||
const hash = getMcpConfigHash(serverConfig);
|
||||
allServers[name] = {
|
||||
config: serverConfig,
|
||||
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)) {
|
||||
const servers = config.mcpServers || {};
|
||||
for (const [name, serverConfig] of Object.entries(servers)) {
|
||||
if (!allServers[name]) {
|
||||
allServers[name] = {
|
||||
config: serverConfig,
|
||||
usedIn: [],
|
||||
isGlobal: false
|
||||
};
|
||||
const hash = getMcpConfigHash(serverConfig);
|
||||
|
||||
if (!configHashes[name]) {
|
||||
// First occurrence of this server name
|
||||
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');
|
||||
}
|
||||
}
|
||||
// ========== 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]);
|
||||
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");
|
||||
|
||||
|
||||
container.innerHTML = `
|
||||
<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 -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -259,16 +322,34 @@ 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 `
|
||||
<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-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>
|
||||
<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>
|
||||
<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-action="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="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</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">
|
||||
<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>
|
||||
@@ -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 => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = e.target.dataset.serverName;
|
||||
const serverConfig = JSON.parse(e.target.dataset.serverConfig);
|
||||
const serverName = btn.dataset.serverName;
|
||||
const serverConfig = JSON.parse(btn.dataset.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 => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = e.target.dataset.serverName;
|
||||
const serverName = btn.dataset.serverName;
|
||||
if (confirm(t('mcp.removeConfirm', { name: serverName }))) {
|
||||
await removeMcpServerFromProject(serverName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user