diff --git a/package-lock.json b/package-lock.json index fa6b34f..12fc9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.5.0", "license": "MIT", "dependencies": { - "@anthropic-ai/claude-code": "^1.0.57", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-javascript": "^6.2.4", @@ -83,26 +82,6 @@ "node": ">=6.0.0" } }, - "node_modules/@anthropic-ai/claude-code": { - "version": "1.0.58", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.58.tgz", - "integrity": "sha512-XcfqklHSCuBRpVV9vZaAGvdJFAyVKb/UHz2VG9osvn1pRqY7e+HhIOU9X7LeI+c116QhmjglGwe+qz4jOC83CQ==", - "license": "SEE LICENSE IN README.md", - "bin": { - "claude": "cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "^0.33.5", - "@img/sharp-darwin-x64": "^0.33.5", - "@img/sharp-linux-arm": "^0.33.5", - "@img/sharp-linux-arm64": "^0.33.5", - "@img/sharp-linux-x64": "^0.33.5", - "@img/sharp-win32-x64": "^0.33.5" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1023,114 +1002,6 @@ "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-ppc64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", @@ -1165,22 +1036,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", @@ -1215,50 +1070,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, "node_modules/@img/sharp-linux-ppc64": { "version": "0.34.3", "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", @@ -1305,28 +1116,6 @@ "@img/sharp-libvips-linux-s390x": "1.2.0" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, "node_modules/@img/sharp-linuxmusl-arm64": { "version": "0.34.3", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", @@ -1433,25 +1222,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1933,7 +1703,9 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", + "optional": true, "os": [ "win32" ] diff --git a/server/routes/mcp.js b/server/routes/mcp.js index 642a2eb..6172291 100644 --- a/server/routes/mcp.js +++ b/server/routes/mcp.js @@ -21,7 +21,7 @@ router.get('/cli/list', async (req, res) => { const { promisify } = await import('util'); const exec = promisify(spawn); - const process = spawn('claude', ['mcp', 'list', '-s', 'user'], { + const process = spawn('claude', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] }); @@ -60,27 +60,30 @@ router.post('/cli/add', async (req, res) => { try { const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body; - console.log('➕ Adding MCP server using Claude CLI:', name); + console.log('➕ Adding MCP server using Claude CLI (user scope):', name); const { spawn } = await import('child_process'); let cliArgs = ['mcp', 'add']; + // Always add with user scope (global availability) + cliArgs.push('--scope', 'user'); + if (type === 'http') { - cliArgs.push('--transport', 'http', name, '-s', 'user', url); + cliArgs.push('--transport', 'http', name, url); // Add headers if provided Object.entries(headers).forEach(([key, value]) => { cliArgs.push('--header', `${key}: ${value}`); }); } else if (type === 'sse') { - cliArgs.push('--transport', 'sse', name, '-s', 'user', url); + cliArgs.push('--transport', 'sse', name, url); // Add headers if provided Object.entries(headers).forEach(([key, value]) => { cliArgs.push('--header', `${key}: ${value}`); }); } else { - // stdio (default): claude mcp add -s user [args...] - cliArgs.push(name, '-s', 'user'); + // stdio (default): claude mcp add --scope user [args...] + cliArgs.push(name); // Add environment variables Object.entries(env).forEach(([key, value]) => { cliArgs.push('-e', `${key}=${value}`); @@ -131,12 +134,39 @@ router.post('/cli/add', async (req, res) => { router.delete('/cli/remove/:name', async (req, res) => { try { const { name } = req.params; + const { scope } = req.query; // Get scope from query params - console.log('đŸ—‘ī¸ Removing MCP server using Claude CLI:', name); + // Handle the ID format (remove scope prefix if present) + let actualName = name; + let actualScope = scope; + + // If the name includes a scope prefix like "local:test", extract it + if (name.includes(':')) { + const [prefix, serverName] = name.split(':'); + actualName = serverName; + actualScope = actualScope || prefix; // Use prefix as scope if not provided in query + } + + console.log('đŸ—‘ī¸ Removing MCP server using Claude CLI:', actualName, 'scope:', actualScope); const { spawn } = await import('child_process'); - const process = spawn('claude', ['mcp', 'remove', '-s', 'user', name], { + // Build command args based on scope + let cliArgs = ['mcp', 'remove']; + + // Add scope flag if it's local scope + if (actualScope === 'local') { + cliArgs.push('--scope', 'local'); + } else if (actualScope === 'user' || !actualScope) { + // User scope is default, but we can be explicit + cliArgs.push('--scope', 'user'); + } + + cliArgs.push(actualName); + + console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' ')); + + const process = spawn('claude', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] }); @@ -179,7 +209,7 @@ router.get('/cli/get/:name', async (req, res) => { const { spawn } = await import('child_process'); - const process = spawn('claude', ['mcp', 'get', '-s', 'user', name], { + const process = spawn('claude', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] }); @@ -213,37 +243,172 @@ router.get('/cli/get/:name', async (req, res) => { } }); +// GET /api/mcp/config/read - Read MCP servers directly from Claude config files +router.get('/config/read', async (req, res) => { + try { + console.log('📖 Reading MCP servers from Claude config files'); + + const homeDir = os.homedir(); + const configPaths = [ + path.join(homeDir, '.claude.json'), + path.join(homeDir, '.claude', 'settings.json') + ]; + + let configData = null; + let configPath = null; + + // Try to read from either config file + for (const filepath of configPaths) { + try { + const fileContent = await fs.readFile(filepath, 'utf8'); + configData = JSON.parse(fileContent); + configPath = filepath; + console.log(`✅ Found Claude config at: ${filepath}`); + break; + } catch (error) { + // File doesn't exist or is not valid JSON, try next + console.log(`â„šī¸ Config not found or invalid at: ${filepath}`); + } + } + + if (!configData) { + return res.json({ + success: false, + message: 'No Claude configuration file found', + servers: [] + }); + } + + // Extract MCP servers from the config + const servers = []; + + // Check for user-scoped MCP servers (at root level) + if (configData.mcpServers && typeof configData.mcpServers === 'object' && Object.keys(configData.mcpServers).length > 0) { + console.log('🔍 Found user-scoped MCP servers:', Object.keys(configData.mcpServers)); + for (const [name, config] of Object.entries(configData.mcpServers)) { + const server = { + id: name, + name: name, + type: 'stdio', // Default type + scope: 'user', // User scope - available across all projects + config: {}, + raw: config // Include raw config for full details + }; + + // Determine transport type and extract config + if (config.command) { + server.type = 'stdio'; + server.config.command = config.command; + server.config.args = config.args || []; + server.config.env = config.env || {}; + } else if (config.url) { + server.type = config.transport || 'http'; + server.config.url = config.url; + server.config.headers = config.headers || {}; + } + + servers.push(server); + } + } + + // Check for local-scoped MCP servers (project-specific) + const currentProjectPath = process.cwd(); + + // Check under 'projects' key + if (configData.projects && configData.projects[currentProjectPath]) { + const projectConfig = configData.projects[currentProjectPath]; + if (projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object' && Object.keys(projectConfig.mcpServers).length > 0) { + console.log(`🔍 Found local-scoped MCP servers for ${currentProjectPath}:`, Object.keys(projectConfig.mcpServers)); + for (const [name, config] of Object.entries(projectConfig.mcpServers)) { + const server = { + id: `local:${name}`, // Prefix with scope for uniqueness + name: name, // Keep original name + type: 'stdio', // Default type + scope: 'local', // Local scope - only for this project + projectPath: currentProjectPath, + config: {}, + raw: config // Include raw config for full details + }; + + // Determine transport type and extract config + if (config.command) { + server.type = 'stdio'; + server.config.command = config.command; + server.config.args = config.args || []; + server.config.env = config.env || {}; + } else if (config.url) { + server.type = config.transport || 'http'; + server.config.url = config.url; + server.config.headers = config.headers || {}; + } + + servers.push(server); + } + } + } + + console.log(`📋 Found ${servers.length} MCP servers in config`); + + res.json({ + success: true, + configPath: configPath, + servers: servers + }); + } catch (error) { + console.error('Error reading Claude config:', error); + res.status(500).json({ + error: 'Failed to read Claude configuration', + details: error.message + }); + } +}); + // Helper functions to parse Claude CLI output function parseClaudeListOutput(output) { - // Parse the output from 'claude mcp list' command - // Format: "name: command/url" or "name: url (TYPE)" const servers = []; const lines = output.split('\n').filter(line => line.trim()); for (const line of lines) { + // Skip the header line + if (line.includes('Checking MCP server health')) continue; + + // Parse lines like "test: test test - ✗ Failed to connect" + // or "server-name: command or description - ✓ Connected" if (line.includes(':')) { const colonIndex = line.indexOf(':'); const name = line.substring(0, colonIndex).trim(); + + // Skip empty names + if (!name) continue; + + // Extract the rest after the name const rest = line.substring(colonIndex + 1).trim(); + // Try to extract description and status + let description = rest; + let status = 'unknown'; let type = 'stdio'; // default type - // Check if it has transport type in parentheses like "(SSE)" or "(HTTP)" - const typeMatch = rest.match(/\((\w+)\)\s*$/); - if (typeMatch) { - type = typeMatch[1].toLowerCase(); - } else if (rest.startsWith('http://') || rest.startsWith('https://')) { - // If it's a URL but no explicit type, assume HTTP + // Check for status indicators + if (rest.includes('✓') || rest.includes('✗')) { + const statusMatch = rest.match(/(.*?)\s*-\s*([✓✗].*)$/); + if (statusMatch) { + description = statusMatch[1].trim(); + status = statusMatch[2].includes('✓') ? 'connected' : 'failed'; + } + } + + // Try to determine type from description + if (description.startsWith('http://') || description.startsWith('https://')) { type = 'http'; } - if (name) { - servers.push({ - name, - type, - status: 'active' - }); - } + servers.push({ + name, + type, + status: status || 'active', + description + }); } } diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx index ddfba0e..8da52fe 100644 --- a/src/components/ToolsSettings.jsx +++ b/src/components/ToolsSettings.jsx @@ -66,7 +66,23 @@ function ToolsSettings({ isOpen, onClose }) { try { const token = localStorage.getItem('auth-token'); - // First try to get servers using Claude CLI + // Try to read directly from config files for complete details + const configResponse = await fetch('/api/mcp/config/read', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (configResponse.ok) { + const configData = await configResponse.json(); + if (configData.success && configData.servers) { + setMcpServers(configData.servers); + return; + } + } + + // Fallback to Claude CLI const cliResponse = await fetch('/api/mcp/cli/list', { headers: { 'Authorization': `Bearer ${token}`, @@ -99,7 +115,7 @@ function ToolsSettings({ isOpen, onClose }) { } } - // Fallback to direct config reading + // Final fallback to direct config reading const response = await fetch('/api/mcp/servers?scope=user', { headers: { 'Authorization': `Bearer ${token}`, @@ -167,8 +183,8 @@ function ToolsSettings({ isOpen, onClose }) { try { const token = localStorage.getItem('auth-token'); - // Use Claude CLI to remove the server - const response = await fetch(`/api/mcp/cli/remove/${serverId}`, { + // Use Claude CLI to remove the server with proper scope + const response = await fetch(`/api/mcp/cli/remove/${serverId}?scope=${scope}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, @@ -362,7 +378,7 @@ function ToolsSettings({ isOpen, onClose }) { setMcpFormData({ name: '', type: 'stdio', - scope: 'user', // Always use user scope + scope: 'user', // Always use user scope for global availability config: { command: '', args: [], @@ -386,7 +402,8 @@ function ToolsSettings({ isOpen, onClose }) { name: server.name, type: server.type, scope: server.scope, - config: { ...server.config } + config: { ...server.config }, + raw: server.raw // Store raw config for display }); } else { resetMcpForm(); @@ -846,8 +863,13 @@ function ToolsSettings({ isOpen, onClose }) { {server.type} - {server.scope} + {server.scope === 'local' ? '📁 local' : server.scope === 'user' ? '👤 user' : server.scope} + {server.projectPath && ( + + {server.projectPath.split('/').pop()} + + )}
@@ -860,6 +882,17 @@ function ToolsSettings({ isOpen, onClose }) { {server.config.args && server.config.args.length > 0 && (
Args: {server.config.args.join(' ')}
)} + {server.config.env && Object.keys(server.config.env).length > 0 && ( +
Environment: {Object.entries(server.config.env).map(([k, v]) => `${k}=${v}`).join(', ')}
+ )} + {server.raw && ( +
+ View full config +
+                                {JSON.stringify(server.raw, null, 2)}
+                              
+
+ )}
{/* Test Results */} @@ -1062,6 +1095,18 @@ function ToolsSettings({ isOpen, onClose }) { {/* Scope is fixed to user - no selection needed */} + {/* Show raw configuration details when editing */} + {editingMcpServer && mcpFormData.raw && ( +
+

+ Configuration Details (from {editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config'}) +

+
+                          {JSON.stringify(mcpFormData.raw, null, 2)}
+                        
+
+ )} + {/* Transport-specific Config */} {mcpFormData.type === 'stdio' && (