mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: add semantic graph design for static code analysis
- Introduced a comprehensive design document for a Code Semantic Graph aimed at enhancing static analysis capabilities. - Defined the architecture, core components, and implementation steps for analyzing function calls, data flow, and dependencies. - Included detailed specifications for nodes and edges in the graph, along with database schema for storage. - Outlined phases for implementation, technical challenges, success metrics, and application scenarios.
This commit is contained in:
@@ -77,7 +77,7 @@ function getMcpServersFromFile(filePath) {
|
||||
*/
|
||||
function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
|
||||
try {
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizePathForFileSystem(projectPath);
|
||||
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
||||
|
||||
// Read existing .mcp.json or create new structure
|
||||
@@ -115,7 +115,7 @@ function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
|
||||
*/
|
||||
function removeMcpServerFromMcpJson(projectPath, serverName) {
|
||||
try {
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizePathForFileSystem(projectPath);
|
||||
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
||||
|
||||
if (!existsSync(mcpJsonPath)) {
|
||||
@@ -238,22 +238,43 @@ function getMcpConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize project path for .claude.json (Windows backslash format)
|
||||
* Normalize path to filesystem format (for accessing .mcp.json files)
|
||||
* Always uses forward slashes for cross-platform compatibility
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeProjectPathForConfig(path) {
|
||||
// Convert forward slashes to backslashes for Windows .claude.json format
|
||||
let normalized = path.replace(/\//g, '\\');
|
||||
|
||||
// Handle /d/path format -> D:\path
|
||||
if (normalized.match(/^\\[a-zA-Z]\\/)) {
|
||||
function normalizePathForFileSystem(path) {
|
||||
let normalized = path.replace(/\\/g, '/');
|
||||
|
||||
// Handle /d/path format -> D:/path
|
||||
if (normalized.match(/^\/[a-zA-Z]\//)) {
|
||||
normalized = normalized.charAt(1).toUpperCase() + ':' + normalized.slice(2);
|
||||
}
|
||||
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize project path to match existing format in .claude.json
|
||||
* Checks both forward slash and backslash formats to find existing entry
|
||||
* @param {string} path
|
||||
* @param {Object} claudeConfig - Optional existing config to check format
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeProjectPathForConfig(path, claudeConfig = null) {
|
||||
// IMPORTANT: Always normalize to forward slashes to prevent duplicate entries
|
||||
// (e.g., prevents both "D:/Claude_dms3" and "D:\\Claude_dms3")
|
||||
let normalizedForward = path.replace(/\\/g, '/');
|
||||
|
||||
// Handle /d/path format -> D:/path
|
||||
if (normalizedForward.match(/^\/[a-zA-Z]\//)) {
|
||||
normalizedForward = normalizedForward.charAt(1).toUpperCase() + ':' + normalizedForward.slice(2);
|
||||
}
|
||||
|
||||
// ALWAYS return forward slash format to prevent duplicates
|
||||
return normalizedForward;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle MCP server enabled/disabled
|
||||
* @param {string} projectPath
|
||||
@@ -270,7 +291,7 @@ function toggleMcpServerEnabled(projectPath, serverName, enable) {
|
||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
||||
|
||||
if (!config.projects || !config.projects[normalizedPath]) {
|
||||
return { error: `Project not found: ${normalizedPath}` };
|
||||
@@ -332,7 +353,7 @@ function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyC
|
||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
||||
|
||||
// Create project entry if it doesn't exist
|
||||
if (!config.projects) {
|
||||
@@ -387,8 +408,8 @@ function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyC
|
||||
*/
|
||||
function removeMcpServerFromProject(projectPath, serverName) {
|
||||
try {
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath);
|
||||
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
||||
const normalizedPathForFile = normalizePathForFileSystem(projectPath);
|
||||
const mcpJsonPath = join(normalizedPathForFile, '.mcp.json');
|
||||
|
||||
let removedFromMcpJson = false;
|
||||
let removedFromClaudeJson = false;
|
||||
@@ -409,6 +430,9 @@ function removeMcpServerFromProject(projectPath, serverName) {
|
||||
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
// Get normalized path that matches existing config format
|
||||
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
||||
|
||||
if (config.projects && config.projects[normalizedPath]) {
|
||||
const projectConfig = config.projects[normalizedPath];
|
||||
|
||||
@@ -597,11 +621,13 @@ export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// API: Copy MCP server to project
|
||||
if (pathname === '/api/mcp-copy-server' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { projectPath, serverName, serverConfig } = body;
|
||||
const { projectPath, serverName, serverConfig, configType } = body;
|
||||
if (!projectPath || !serverName || !serverConfig) {
|
||||
return { error: 'projectPath, serverName, and serverConfig are required', status: 400 };
|
||||
}
|
||||
return addMcpServerToProject(projectPath, serverName, serverConfig);
|
||||
// configType: 'mcp' = use .mcp.json (default), 'claude' = use .claude.json
|
||||
const useLegacyConfig = configType === 'claude';
|
||||
return addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyConfig);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -733,7 +733,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
}
|
||||
|
||||
try {
|
||||
const configPath = join(projectPath, '.claude', 'rules', 'active_memory.md');
|
||||
const configPath = join(projectPath, '.claude', 'CLAUDE.md');
|
||||
const configJsonPath = join(projectPath, '.claude', 'active_memory_config.json');
|
||||
const enabled = existsSync(configPath);
|
||||
let lastSync: string | null = null;
|
||||
@@ -784,16 +784,12 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return;
|
||||
}
|
||||
|
||||
const rulesDir = join(projectPath, '.claude', 'rules');
|
||||
const claudeDir = join(projectPath, '.claude');
|
||||
const configPath = join(rulesDir, 'active_memory.md');
|
||||
const configPath = join(claudeDir, 'CLAUDE.md');
|
||||
const configJsonPath = join(claudeDir, 'active_memory_config.json');
|
||||
|
||||
if (enabled) {
|
||||
// Enable: Create directories and initial file
|
||||
if (!existsSync(rulesDir)) {
|
||||
mkdirSync(rulesDir, { recursive: true });
|
||||
}
|
||||
if (!existsSync(claudeDir)) {
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
@@ -803,8 +799,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
writeFileSync(configJsonPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
// Create initial active_memory.md with header
|
||||
const initialContent = `# Active Memory
|
||||
// Create initial CLAUDE.md with header
|
||||
const initialContent = `# CLAUDE.md - Project Memory
|
||||
|
||||
> Auto-generated understanding of frequently accessed files.
|
||||
> Last updated: ${new Date().toISOString()}
|
||||
@@ -867,7 +863,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Active Memory - Sync (analyze hot files using CLI and update active_memory.md)
|
||||
// API: Active Memory - Sync (analyze hot files using CLI and update CLAUDE.md)
|
||||
if (pathname === '/api/memory/active/sync' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||
@@ -882,8 +878,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return;
|
||||
}
|
||||
|
||||
const rulesDir = join(projectPath, '.claude', 'rules');
|
||||
const configPath = join(rulesDir, 'active_memory.md');
|
||||
const claudeDir = join(projectPath, '.claude');
|
||||
const configPath = join(claudeDir, 'CLAUDE.md');
|
||||
|
||||
// Get hot files from memory store - with fallback
|
||||
let hotFiles: any[] = [];
|
||||
@@ -903,8 +899,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
return isAbsolute(filePath) ? filePath : join(projectPath, filePath);
|
||||
}).filter((p: string) => existsSync(p));
|
||||
|
||||
// Build the active memory content header
|
||||
let content = `# Active Memory
|
||||
// Build the CLAUDE.md content header
|
||||
let content = `# CLAUDE.md - Project Memory
|
||||
|
||||
> Auto-generated understanding of frequently accessed files using ${tool.toUpperCase()}.
|
||||
> Last updated: ${new Date().toISOString()}
|
||||
@@ -942,14 +938,29 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
||||
});
|
||||
|
||||
if (result.success && result.execution?.output) {
|
||||
// Extract stdout from output object
|
||||
cliOutput = typeof result.execution.output === 'string'
|
||||
? result.execution.output
|
||||
: result.execution.output.stdout || '';
|
||||
// Extract stdout from output object with proper serialization
|
||||
const output = result.execution.output;
|
||||
if (typeof output === 'string') {
|
||||
cliOutput = output;
|
||||
} else if (output && typeof output === 'object') {
|
||||
// Handle object output - extract stdout or serialize the object
|
||||
if (output.stdout && typeof output.stdout === 'string') {
|
||||
cliOutput = output.stdout;
|
||||
} else if (output.stderr && typeof output.stderr === 'string') {
|
||||
cliOutput = output.stderr;
|
||||
} else {
|
||||
// Last resort: serialize the entire object as JSON
|
||||
cliOutput = JSON.stringify(output, null, 2);
|
||||
}
|
||||
} else {
|
||||
cliOutput = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Add CLI output to content
|
||||
content += cliOutput + '\n\n---\n\n';
|
||||
// Add CLI output to content (only if not empty)
|
||||
if (cliOutput && cliOutput.trim()) {
|
||||
content += cliOutput + '\n\n---\n\n';
|
||||
}
|
||||
|
||||
} catch (cliErr) {
|
||||
// Fallback to basic analysis if CLI fails
|
||||
@@ -1007,8 +1018,8 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!existsSync(rulesDir)) {
|
||||
mkdirSync(rulesDir, { recursive: true });
|
||||
if (!existsSync(claudeDir)) {
|
||||
mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file
|
||||
|
||||
@@ -87,15 +87,23 @@ async function toggleMcpServer(serverName, enable) {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyMcpServerToProject(serverName, serverConfig) {
|
||||
async function copyMcpServerToProject(serverName, serverConfig, configType = null) {
|
||||
try {
|
||||
// If configType not specified, ask user to choose
|
||||
if (!configType) {
|
||||
const choice = await showConfigTypeDialog();
|
||||
if (!choice) return null; // User cancelled
|
||||
configType = choice;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/mcp-copy-server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
projectPath: projectPath,
|
||||
serverName: serverName,
|
||||
serverConfig: serverConfig
|
||||
serverConfig: serverConfig,
|
||||
configType: configType // 'claude' for .claude.json, 'mcp' for .mcp.json
|
||||
})
|
||||
});
|
||||
|
||||
@@ -105,7 +113,8 @@ async function copyMcpServerToProject(serverName, serverConfig) {
|
||||
if (result.success) {
|
||||
await loadMcpConfig();
|
||||
renderMcpManager();
|
||||
showRefreshToast(`MCP server "${serverName}" added to project`, 'success');
|
||||
const location = configType === 'mcp' ? '.mcp.json' : '.claude.json';
|
||||
showRefreshToast(`MCP server "${serverName}" added to project (${location})`, 'success');
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
@@ -115,6 +124,53 @@ async function copyMcpServerToProject(serverName, serverConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// Show dialog to let user choose config type
|
||||
function showConfigTypeDialog() {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||
dialog.innerHTML = `
|
||||
<div class="bg-card border border-border rounded-lg shadow-lg p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-lg font-semibold mb-4">${t('mcp.chooseInstallLocation')}</h3>
|
||||
<div class="space-y-3 mb-6">
|
||||
<button class="config-type-option w-full text-left px-4 py-3 border border-border rounded-lg hover:bg-accent hover:border-primary transition-all" data-type="claude">
|
||||
<div class="font-medium">${t('mcp.installToClaudeJson')}</div>
|
||||
<div class="text-sm text-muted-foreground mt-1">${t('mcp.claudeJsonDesc')}</div>
|
||||
</button>
|
||||
<button class="config-type-option w-full text-left px-4 py-3 border border-border rounded-lg hover:bg-accent hover:border-primary transition-all" data-type="mcp">
|
||||
<div class="font-medium">${t('mcp.installToMcpJson')}</div>
|
||||
<div class="text-sm text-muted-foreground mt-1">${t('mcp.mcpJsonDesc')}</div>
|
||||
</button>
|
||||
</div>
|
||||
<button class="cancel-btn w-full px-4 py-2 border border-border rounded-lg hover:bg-accent transition-colors">${t('common.cancel')}</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
const options = dialog.querySelectorAll('.config-type-option');
|
||||
options.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
resolve(btn.dataset.type);
|
||||
document.body.removeChild(dialog);
|
||||
});
|
||||
});
|
||||
|
||||
const cancelBtn = dialog.querySelector('.cancel-btn');
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
resolve(null);
|
||||
document.body.removeChild(dialog);
|
||||
});
|
||||
|
||||
// Close on backdrop click
|
||||
dialog.addEventListener('click', (e) => {
|
||||
if (e.target === dialog) {
|
||||
resolve(null);
|
||||
document.body.removeChild(dialog);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function removeMcpServerFromProject(serverName) {
|
||||
try {
|
||||
const response = await fetch('/api/mcp-remove-server', {
|
||||
|
||||
@@ -431,7 +431,31 @@ const i18n = {
|
||||
'mcp.jsonFormatsHint': 'Supports {"servers": {...}}, {"mcpServers": {...}}, and direct server config formats.',
|
||||
'mcp.previewServers': 'Preview (servers to be added):',
|
||||
'mcp.create': 'Create',
|
||||
|
||||
'mcp.chooseInstallLocation': 'Choose Installation Location',
|
||||
'mcp.installToClaudeJson': 'Install to .claude.json',
|
||||
'mcp.installToMcpJson': 'Install to .mcp.json (Recommended)',
|
||||
'mcp.claudeJsonDesc': 'Save in root .claude.json projects section (shared config)',
|
||||
'mcp.mcpJsonDesc': 'Save in project .mcp.json file (recommended for version control)',
|
||||
|
||||
// MCP Templates
|
||||
'mcp.templates': 'MCP Templates',
|
||||
'mcp.savedTemplates': 'saved templates',
|
||||
'mcp.saveAsTemplate': 'Save as Template',
|
||||
'mcp.enterTemplateName': 'Enter template name',
|
||||
'mcp.enterTemplateDesc': 'Enter template description (optional)',
|
||||
'mcp.enterServerName': 'Enter server name',
|
||||
'mcp.templateSaved': 'Template "{name}" saved successfully',
|
||||
'mcp.templateSaveFailed': 'Failed to save template: {error}',
|
||||
'mcp.templateNotFound': 'Template "{name}" not found',
|
||||
'mcp.templateInstalled': 'Server "{name}" installed successfully',
|
||||
'mcp.templateInstallFailed': 'Failed to install template: {error}',
|
||||
'mcp.deleteTemplate': 'Delete Template',
|
||||
'mcp.deleteTemplateConfirm': 'Delete template "{name}"?',
|
||||
'mcp.templateDeleted': 'Template "{name}" deleted successfully',
|
||||
'mcp.templateDeleteFailed': 'Failed to delete template: {error}',
|
||||
'mcp.toProject': 'To Project',
|
||||
'mcp.toGlobal': 'To Global',
|
||||
|
||||
// Hook Manager
|
||||
'hook.projectHooks': 'Project Hooks',
|
||||
'hook.projectFile': '.claude/settings.json',
|
||||
@@ -1346,6 +1370,11 @@ const i18n = {
|
||||
'mcp.jsonFormatsHint': '支持 {"servers": {...}}、{"mcpServers": {...}} 和直接服务器配置格式。',
|
||||
'mcp.previewServers': '预览(将添加的服务器):',
|
||||
'mcp.create': '创建',
|
||||
'mcp.chooseInstallLocation': '选择安装位置',
|
||||
'mcp.installToClaudeJson': '安装到 .claude.json',
|
||||
'mcp.installToMcpJson': '安装到 .mcp.json(推荐)',
|
||||
'mcp.claudeJsonDesc': '保存在根目录 .claude.json projects 字段下(共享配置)',
|
||||
'mcp.mcpJsonDesc': '保存在项目 .mcp.json 文件中(推荐用于版本控制)',
|
||||
|
||||
// Hook Manager
|
||||
'hook.projectHooks': '项目钩子',
|
||||
|
||||
@@ -43,6 +43,9 @@ async function renderMcpManager() {
|
||||
await loadMcpConfig();
|
||||
}
|
||||
|
||||
// Load MCP templates
|
||||
await loadMcpTemplates();
|
||||
|
||||
const currentPath = projectPath.replace(/\//g, '\\');
|
||||
const projectData = mcpAllProjects[currentPath] || {};
|
||||
const projectServers = projectData.mcpServers || {};
|
||||
@@ -269,6 +272,77 @@ async function renderMcpManager() {
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- MCP Templates Section -->
|
||||
${mcpTemplates.length > 0 ? `
|
||||
<div class="mcp-section mt-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<i data-lucide="layout-template" class="w-5 h-5"></i>
|
||||
${t('mcp.templates')}
|
||||
</h3>
|
||||
<span class="text-sm text-muted-foreground">${mcpTemplates.length} ${t('mcp.savedTemplates')}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
${mcpTemplates.map(template => `
|
||||
<div class="mcp-template-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="font-semibold text-foreground truncate flex items-center gap-2">
|
||||
<i data-lucide="layout-template" class="w-4 h-4 shrink-0"></i>
|
||||
<span class="truncate">${escapeHtml(template.name)}</span>
|
||||
</h4>
|
||||
${template.description ? `
|
||||
<p class="text-xs text-muted-foreground mt-1 line-clamp-2">${escapeHtml(template.description)}</p>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1 mb-3">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">cmd</span>
|
||||
<span class="truncate text-xs" title="${escapeHtml(template.serverConfig.command)}">${escapeHtml(template.serverConfig.command)}</span>
|
||||
</div>
|
||||
${template.serverConfig.args && template.serverConfig.args.length > 0 ? `
|
||||
<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(template.serverConfig.args.join(' '))}">${escapeHtml(template.serverConfig.args.slice(0, 2).join(' '))}${template.serverConfig.args.length > 2 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between 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"
|
||||
data-template-name="${escapeHtml(template.name)}"
|
||||
data-scope="project"
|
||||
data-action="install-template"
|
||||
title="${t('mcp.installToProject')}">
|
||||
<i data-lucide="download" class="w-3 h-3"></i>
|
||||
${t('mcp.toProject')}
|
||||
</button>
|
||||
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
|
||||
data-template-name="${escapeHtml(template.name)}"
|
||||
data-scope="global"
|
||||
data-action="install-template"
|
||||
title="${t('mcp.installToGlobal')}">
|
||||
<i data-lucide="globe" class="w-3 h-3"></i>
|
||||
${t('mcp.toGlobal')}
|
||||
</button>
|
||||
</div>
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-template-name="${escapeHtml(template.name)}"
|
||||
data-action="delete-template"
|
||||
title="${t('mcp.deleteTemplate')}">
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- All Projects MCP Overview Table -->
|
||||
<div class="mcp-section mt-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -402,15 +476,25 @@ function renderProjectAvailableServerCard(entry) {
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
||||
data-scope="${source === 'global' ? 'global' : 'project'}"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between 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"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
||||
data-scope="${source === 'global' ? 'global' : 'project'}"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
<button class="text-xs text-success hover:text-success/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
||||
data-action="save-as-template"
|
||||
title="${t('mcp.saveAsTemplate')}">
|
||||
<i data-lucide="save" class="w-3 h-3"></i>
|
||||
${t('mcp.saveAsTemplate')}
|
||||
</button>
|
||||
</div>
|
||||
${canRemove ? `
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
@@ -617,4 +701,156 @@ function attachMcpEventListeners() {
|
||||
await copyMcpInstallCommand(serverName, serverConfig, scope);
|
||||
});
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 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) {
|
||||
showNotification(t('mcp.templateSaved', { name: templateName }), 'success');
|
||||
await loadMcpTemplates();
|
||||
await renderMcpManager(); // Refresh view
|
||||
} else {
|
||||
showNotification(t('mcp.templateSaveFailed', { error: data.error }), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MCP] Save template error:', error);
|
||||
showNotification(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) {
|
||||
showNotification(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);
|
||||
}
|
||||
|
||||
showNotification(t('mcp.templateInstalled', { name: serverName }), 'success');
|
||||
await renderMcpManager();
|
||||
} catch (error) {
|
||||
console.error('[MCP] Install from template error:', error);
|
||||
showNotification(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) {
|
||||
showNotification(t('mcp.templateDeleted', { name: templateName }), 'success');
|
||||
await loadMcpTemplates();
|
||||
await renderMcpManager();
|
||||
} else {
|
||||
showNotification(t('mcp.templateDeleteFailed', { error: data.error }), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MCP] Delete template error:', error);
|
||||
showNotification(t('mcp.templateDeleteFailed', { error: error.message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,9 +588,21 @@ function closeRuleCreateModal(event) {
|
||||
|
||||
function selectRuleLocation(location) {
|
||||
ruleCreateState.location = location;
|
||||
// Re-render modal
|
||||
closeRuleCreateModal();
|
||||
openRuleCreateModal();
|
||||
|
||||
// Update button styles without re-rendering modal
|
||||
const buttons = document.querySelectorAll('.location-btn');
|
||||
buttons.forEach(btn => {
|
||||
const isProject = btn.querySelector('.font-medium')?.textContent?.includes(t('rules.projectRules'));
|
||||
const isUser = btn.querySelector('.font-medium')?.textContent?.includes(t('rules.userRules'));
|
||||
|
||||
if ((isProject && location === 'project') || (isUser && location === 'user')) {
|
||||
btn.classList.remove('border-border', 'hover:border-primary/50');
|
||||
btn.classList.add('border-primary', 'bg-primary/10');
|
||||
} else {
|
||||
btn.classList.remove('border-primary', 'bg-primary/10');
|
||||
btn.classList.add('border-border', 'hover:border-primary/50');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRuleConditional() {
|
||||
|
||||
@@ -569,9 +569,21 @@ function closeSkillCreateModal(event) {
|
||||
|
||||
function selectSkillLocation(location) {
|
||||
skillCreateState.location = location;
|
||||
// Re-render modal
|
||||
closeSkillCreateModal();
|
||||
openSkillCreateModal();
|
||||
|
||||
// Update button styles without re-rendering modal
|
||||
const buttons = document.querySelectorAll('.location-btn');
|
||||
buttons.forEach(btn => {
|
||||
const isProject = btn.querySelector('.font-medium')?.textContent?.includes(t('skills.projectSkills'));
|
||||
const isUser = btn.querySelector('.font-medium')?.textContent?.includes(t('skills.userSkills'));
|
||||
|
||||
if ((isProject && location === 'project') || (isUser && location === 'user')) {
|
||||
btn.classList.remove('border-border', 'hover:border-primary/50');
|
||||
btn.classList.add('border-primary', 'bg-primary/10');
|
||||
} else {
|
||||
btn.classList.remove('border-primary', 'bg-primary/10');
|
||||
btn.classList.add('border-border', 'hover:border-primary/50');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function switchSkillCreateMode(mode) {
|
||||
|
||||
Reference in New Issue
Block a user