diff --git a/server/claude-cli.js b/server/claude-cli.js index 260957c..251f047 100755 --- a/server/claude-cli.js +++ b/server/claude-cli.js @@ -84,6 +84,84 @@ async function spawnClaude(command, options = {}, ws) { // Add basic flags args.push('--output-format', 'stream-json', '--verbose'); + // Add MCP config flag only if MCP servers are configured + try { + console.log('🔍 Starting MCP config check...'); + // Use already imported modules (fs.promises is imported as fs, path, os) + const fsSync = await import('fs'); // Import synchronous fs methods + console.log('✅ Successfully imported fs sync methods'); + + // Check for MCP config in ~/.claude.json + const claudeConfigPath = path.join(os.homedir(), '.claude.json'); + + console.log(`🔍 Checking for MCP configs in: ${claudeConfigPath}`); + console.log(` Claude config exists: ${fsSync.existsSync(claudeConfigPath)}`); + + let hasMcpServers = false; + + // Check Claude config for MCP servers + if (fsSync.existsSync(claudeConfigPath)) { + try { + const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8')); + + // Check global MCP servers + if (claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) { + console.log(`✅ Found ${Object.keys(claudeConfig.mcpServers).length} global MCP servers`); + hasMcpServers = true; + } + + // Check project-specific MCP servers + if (!hasMcpServers && claudeConfig.claudeProjects) { + const currentProjectPath = process.cwd(); + const projectConfig = claudeConfig.claudeProjects[currentProjectPath]; + if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) { + console.log(`✅ Found ${Object.keys(projectConfig.mcpServers).length} project MCP servers`); + hasMcpServers = true; + } + } + } catch (e) { + console.log(`❌ Failed to parse Claude config:`, e.message); + } + } + + console.log(`🔍 hasMcpServers result: ${hasMcpServers}`); + + if (hasMcpServers) { + // Use Claude config file if it has MCP servers + let configPath = null; + + if (fsSync.existsSync(claudeConfigPath)) { + try { + const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8')); + + // Check if we have any MCP servers (global or project-specific) + const hasGlobalServers = claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0; + const currentProjectPath = process.cwd(); + const projectConfig = claudeConfig.claudeProjects && claudeConfig.claudeProjects[currentProjectPath]; + const hasProjectServers = projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0; + + if (hasGlobalServers || hasProjectServers) { + configPath = claudeConfigPath; + } + } catch (e) { + // No valid config found + } + } + + if (configPath) { + console.log(`📡 Adding MCP config: ${configPath}`); + args.push('--mcp-config', configPath); + } else { + console.log('⚠️ MCP servers detected but no valid config file found'); + } + } + } catch (error) { + // If there's any error checking for MCP configs, don't add the flag + console.log('❌ MCP config check failed:', error.message); + console.log('📍 Error stack:', error.stack); + console.log('Note: MCP config check failed, proceeding without MCP support'); + } + // Add model for new sessions if (!resume) { args.push('--model', 'sonnet'); diff --git a/server/index.js b/server/index.js index c071867..529fa52 100755 --- a/server/index.js +++ b/server/index.js @@ -40,6 +40,7 @@ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSess import { spawnClaude, abortClaudeSession } from './claude-cli.js'; import gitRoutes from './routes/git.js'; import authRoutes from './routes/auth.js'; +import mcpRoutes from './routes/mcp.js'; import { initializeDatabase } from './database/db.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; @@ -131,17 +132,6 @@ async function setupProjectsWatcher() { } // Get the first non-localhost IP address -function getServerIP() { - const interfaces = os.networkInterfaces(); - for (const name of Object.keys(interfaces)) { - for (const iface of interfaces[name]) { - if (iface.family === 'IPv4' && !iface.internal) { - return iface.address; - } - } - } - return 'localhost'; -} const app = express(); const server = http.createServer(app); @@ -183,14 +173,16 @@ app.use('/api/auth', authRoutes); // Git API Routes (protected) app.use('/api/git', authenticateToken, gitRoutes); +// MCP API Routes (protected) +app.use('/api/mcp', authenticateToken, mcpRoutes); + // Static files served after API routes app.use(express.static(path.join(__dirname, '../dist'))); // API Routes (protected) app.get('/api/config', authenticateToken, (req, res) => { // Always use the server's actual IP and port for WebSocket connections - const serverIP = getServerIP(); - const host = `${serverIP}:${PORT}`; +const host = req.headers.host || `${req.hostname}:${PORT}`; const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws'; console.log('Config API called - Returning host:', host, 'Protocol:', protocol); @@ -428,8 +420,6 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) const files = await getFileTree(actualPath, 3, 0, true); const hiddenFiles = files.filter(f => f.name.startsWith('.')); - console.log('📄 Found', files.length, 'files/folders, including', hiddenFiles.length, 'hidden files'); - console.log('🔍 Hidden files:', hiddenFiles.map(f => f.name)); res.json(files); } catch (error) { console.error('❌ File tree error:', error.message); @@ -1006,4 +996,4 @@ async function startServer() { } } -startServer(); \ No newline at end of file +startServer(); diff --git a/server/routes/mcp.js b/server/routes/mcp.js new file mode 100644 index 0000000..642a2eb --- /dev/null +++ b/server/routes/mcp.js @@ -0,0 +1,286 @@ +import express from 'express'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { spawn } from 'child_process'; + +const router = express.Router(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Claude CLI command routes + +// GET /api/mcp/cli/list - List MCP servers using Claude CLI +router.get('/cli/list', async (req, res) => { + try { + console.log('📋 Listing MCP servers using Claude CLI'); + + const { spawn } = await import('child_process'); + const { promisify } = await import('util'); + const exec = promisify(spawn); + + const process = spawn('claude', ['mcp', 'list', '-s', 'user'], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + process.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + process.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + process.on('close', (code) => { + if (code === 0) { + res.json({ success: true, output: stdout, servers: parseClaudeListOutput(stdout) }); + } else { + console.error('Claude CLI error:', stderr); + res.status(500).json({ error: 'Claude CLI command failed', details: stderr }); + } + }); + + process.on('error', (error) => { + console.error('Error running Claude CLI:', error); + res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message }); + }); + } catch (error) { + console.error('Error listing MCP servers via CLI:', error); + res.status(500).json({ error: 'Failed to list MCP servers', details: error.message }); + } +}); + +// POST /api/mcp/cli/add - Add MCP server using Claude CLI +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); + + const { spawn } = await import('child_process'); + + let cliArgs = ['mcp', 'add']; + + if (type === 'http') { + cliArgs.push('--transport', 'http', name, '-s', 'user', 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); + // 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'); + // Add environment variables + Object.entries(env).forEach(([key, value]) => { + cliArgs.push('-e', `${key}=${value}`); + }); + cliArgs.push(command); + if (args && args.length > 0) { + cliArgs.push(...args); + } + } + + console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' ')); + + const process = spawn('claude', cliArgs, { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + process.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + process.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + process.on('close', (code) => { + if (code === 0) { + res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully` }); + } else { + console.error('Claude CLI error:', stderr); + res.status(400).json({ error: 'Claude CLI command failed', details: stderr }); + } + }); + + process.on('error', (error) => { + console.error('Error running Claude CLI:', error); + res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message }); + }); + } catch (error) { + console.error('Error adding MCP server via CLI:', error); + res.status(500).json({ error: 'Failed to add MCP server', details: error.message }); + } +}); + +// DELETE /api/mcp/cli/remove/:name - Remove MCP server using Claude CLI +router.delete('/cli/remove/:name', async (req, res) => { + try { + const { name } = req.params; + + console.log('🗑️ Removing MCP server using Claude CLI:', name); + + const { spawn } = await import('child_process'); + + const process = spawn('claude', ['mcp', 'remove', '-s', 'user', name], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + process.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + process.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + process.on('close', (code) => { + if (code === 0) { + res.json({ success: true, output: stdout, message: `MCP server "${name}" removed successfully` }); + } else { + console.error('Claude CLI error:', stderr); + res.status(400).json({ error: 'Claude CLI command failed', details: stderr }); + } + }); + + process.on('error', (error) => { + console.error('Error running Claude CLI:', error); + res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message }); + }); + } catch (error) { + console.error('Error removing MCP server via CLI:', error); + res.status(500).json({ error: 'Failed to remove MCP server', details: error.message }); + } +}); + +// GET /api/mcp/cli/get/:name - Get MCP server details using Claude CLI +router.get('/cli/get/:name', async (req, res) => { + try { + const { name } = req.params; + + console.log('📄 Getting MCP server details using Claude CLI:', name); + + const { spawn } = await import('child_process'); + + const process = spawn('claude', ['mcp', 'get', '-s', 'user', name], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + process.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + process.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + process.on('close', (code) => { + if (code === 0) { + res.json({ success: true, output: stdout, server: parseClaudeGetOutput(stdout) }); + } else { + console.error('Claude CLI error:', stderr); + res.status(404).json({ error: 'Claude CLI command failed', details: stderr }); + } + }); + + process.on('error', (error) => { + console.error('Error running Claude CLI:', error); + res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message }); + }); + } catch (error) { + console.error('Error getting MCP server details via CLI:', error); + res.status(500).json({ error: 'Failed to get MCP server details', 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) { + if (line.includes(':')) { + const colonIndex = line.indexOf(':'); + const name = line.substring(0, colonIndex).trim(); + const rest = line.substring(colonIndex + 1).trim(); + + 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 + type = 'http'; + } + + if (name) { + servers.push({ + name, + type, + status: 'active' + }); + } + } + } + + console.log('🔍 Parsed Claude CLI servers:', servers); + return servers; +} + +function parseClaudeGetOutput(output) { + // Parse the output from 'claude mcp get ' command + // This is a simple parser - might need adjustment based on actual output format + try { + // Try to extract JSON if present + const jsonMatch = output.match(/\{[\s\S]*\}/); + if (jsonMatch) { + return JSON.parse(jsonMatch[0]); + } + + // Otherwise, parse as text + const server = { raw_output: output }; + const lines = output.split('\n'); + + for (const line of lines) { + if (line.includes('Name:')) { + server.name = line.split(':')[1]?.trim(); + } else if (line.includes('Type:')) { + server.type = line.split(':')[1]?.trim(); + } else if (line.includes('Command:')) { + server.command = line.split(':')[1]?.trim(); + } else if (line.includes('URL:')) { + server.url = line.split(':')[1]?.trim(); + } + } + + return server; + } catch (error) { + return { raw_output: output, parse_error: error.message }; + } +} + +export default router; \ No newline at end of file diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx index ff98a3e..ddfba0e 100755 --- a/src/components/ToolsSettings.jsx +++ b/src/components/ToolsSettings.jsx @@ -3,7 +3,7 @@ import { Button } from './ui/button'; import { Input } from './ui/input'; import { ScrollArea } from './ui/scroll-area'; import { Badge } from './ui/badge'; -import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun } from 'lucide-react'; +import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Play, Globe, Terminal, Zap } from 'lucide-react'; import { useTheme } from '../contexts/ThemeContext'; function ToolsSettings({ isOpen, onClose }) { @@ -17,6 +17,32 @@ function ToolsSettings({ isOpen, onClose }) { const [saveStatus, setSaveStatus] = useState(null); const [projectSortOrder, setProjectSortOrder] = useState('name'); + // MCP server management state + const [mcpServers, setMcpServers] = useState([]); + const [showMcpForm, setShowMcpForm] = useState(false); + const [editingMcpServer, setEditingMcpServer] = useState(null); + const [mcpFormData, setMcpFormData] = useState({ + name: '', + type: 'stdio', + scope: 'user', // Always use user scope + config: { + command: '', + args: [], + env: {}, + url: '', + headers: {}, + timeout: 30000 + } + }); + const [mcpLoading, setMcpLoading] = useState(false); + const [mcpTestResults, setMcpTestResults] = useState({}); + const [mcpConfigTestResult, setMcpConfigTestResult] = useState(null); + const [mcpConfigTesting, setMcpConfigTesting] = useState(false); + const [mcpConfigTested, setMcpConfigTested] = useState(false); + const [mcpServerTools, setMcpServerTools] = useState({}); + const [mcpToolsLoading, setMcpToolsLoading] = useState({}); + const [activeTab, setActiveTab] = useState('tools'); + // Common tool patterns const commonTools = [ 'Bash(git log:*)', @@ -35,13 +61,219 @@ function ToolsSettings({ isOpen, onClose }) { 'WebSearch' ]; + // MCP API functions + const fetchMcpServers = async () => { + try { + const token = localStorage.getItem('auth-token'); + + // First try to get servers using Claude CLI + const cliResponse = await fetch('/api/mcp/cli/list', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (cliResponse.ok) { + const cliData = await cliResponse.json(); + if (cliData.success && cliData.servers) { + // Convert CLI format to our format + const servers = cliData.servers.map(server => ({ + id: server.name, + name: server.name, + type: server.type, + scope: 'user', + config: { + command: server.command || '', + args: server.args || [], + env: server.env || {}, + url: server.url || '', + headers: server.headers || {}, + timeout: 30000 + }, + created: new Date().toISOString(), + updated: new Date().toISOString() + })); + setMcpServers(servers); + return; + } + } + + // Fallback to direct config reading + const response = await fetch('/api/mcp/servers?scope=user', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setMcpServers(data.servers || []); + } else { + console.error('Failed to fetch MCP servers'); + } + } catch (error) { + console.error('Error fetching MCP servers:', error); + } + }; + + const saveMcpServer = async (serverData) => { + try { + const token = localStorage.getItem('auth-token'); + + if (editingMcpServer) { + // For editing, remove old server and add new one + await deleteMcpServer(editingMcpServer.id, 'user'); + } + + // Use Claude CLI to add the server + const response = await fetch('/api/mcp/cli/add', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: serverData.name, + type: serverData.type, + command: serverData.config?.command, + args: serverData.config?.args || [], + url: serverData.config?.url, + headers: serverData.config?.headers || {}, + env: serverData.config?.env || {} + }) + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + await fetchMcpServers(); // Refresh the list + return true; + } else { + throw new Error(result.error || 'Failed to save server via Claude CLI'); + } + } else { + const error = await response.json(); + throw new Error(error.error || 'Failed to save server'); + } + } catch (error) { + console.error('Error saving MCP server:', error); + throw error; + } + }; + + const deleteMcpServer = async (serverId, scope = 'user') => { + try { + const token = localStorage.getItem('auth-token'); + + // Use Claude CLI to remove the server + const response = await fetch(`/api/mcp/cli/remove/${serverId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + await fetchMcpServers(); // Refresh the list + return true; + } else { + throw new Error(result.error || 'Failed to delete server via Claude CLI'); + } + } else { + const error = await response.json(); + throw new Error(error.error || 'Failed to delete server'); + } + } catch (error) { + console.error('Error deleting MCP server:', error); + throw error; + } + }; + + const testMcpServer = async (serverId, scope = 'user') => { + try { + const token = localStorage.getItem('auth-token'); + const response = await fetch(`/api/mcp/servers/${serverId}/test?scope=${scope}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + return data.testResult; + } else { + const error = await response.json(); + throw new Error(error.error || 'Failed to test server'); + } + } catch (error) { + console.error('Error testing MCP server:', error); + throw error; + } + }; + + const testMcpConfiguration = async (formData) => { + try { + const token = localStorage.getItem('auth-token'); + const response = await fetch('/api/mcp/servers/test', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + const data = await response.json(); + return data.testResult; + } else { + const error = await response.json(); + throw new Error(error.error || 'Failed to test configuration'); + } + } catch (error) { + console.error('Error testing MCP configuration:', error); + throw error; + } + }; + + const discoverMcpTools = async (serverId, scope = 'user') => { + try { + const token = localStorage.getItem('auth-token'); + const response = await fetch(`/api/mcp/servers/${serverId}/tools?scope=${scope}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + return data.toolsResult; + } else { + const error = await response.json(); + throw new Error(error.error || 'Failed to discover tools'); + } + } catch (error) { + console.error('Error discovering MCP tools:', error); + throw error; + } + }; + useEffect(() => { if (isOpen) { loadSettings(); } }, [isOpen]); - const loadSettings = () => { + const loadSettings = async () => { try { // Load from localStorage @@ -60,6 +292,9 @@ function ToolsSettings({ isOpen, onClose }) { setSkipPermissions(false); setProjectSortOrder('name'); } + + // Load MCP servers from API + await fetchMcpServers(); } catch (error) { console.error('Error loading tool settings:', error); // Set defaults on error @@ -122,6 +357,149 @@ function ToolsSettings({ isOpen, onClose }) { setDisallowedTools(disallowedTools.filter(t => t !== tool)); }; + // MCP form handling functions + const resetMcpForm = () => { + setMcpFormData({ + name: '', + type: 'stdio', + scope: 'user', // Always use user scope + config: { + command: '', + args: [], + env: {}, + url: '', + headers: {}, + timeout: 30000 + } + }); + setEditingMcpServer(null); + setShowMcpForm(false); + setMcpConfigTestResult(null); + setMcpConfigTested(false); + setMcpConfigTesting(false); + }; + + const openMcpForm = (server = null) => { + if (server) { + setEditingMcpServer(server); + setMcpFormData({ + name: server.name, + type: server.type, + scope: server.scope, + config: { ...server.config } + }); + } else { + resetMcpForm(); + } + setShowMcpForm(true); + }; + + const handleMcpSubmit = async (e) => { + e.preventDefault(); + + setMcpLoading(true); + + try { + await saveMcpServer(mcpFormData); + resetMcpForm(); + setSaveStatus('success'); + } catch (error) { + alert(`Error: ${error.message}`); + setSaveStatus('error'); + } finally { + setMcpLoading(false); + } + }; + + const handleMcpDelete = async (serverId, scope) => { + if (confirm('Are you sure you want to delete this MCP server?')) { + try { + await deleteMcpServer(serverId, scope); + setSaveStatus('success'); + } catch (error) { + alert(`Error: ${error.message}`); + setSaveStatus('error'); + } + } + }; + + const handleMcpTest = async (serverId, scope) => { + try { + setMcpTestResults({ ...mcpTestResults, [serverId]: { loading: true } }); + const result = await testMcpServer(serverId, scope); + setMcpTestResults({ ...mcpTestResults, [serverId]: result }); + } catch (error) { + setMcpTestResults({ + ...mcpTestResults, + [serverId]: { + success: false, + message: error.message, + details: [] + } + }); + } + }; + + const handleMcpToolsDiscovery = async (serverId, scope) => { + try { + setMcpToolsLoading({ ...mcpToolsLoading, [serverId]: true }); + const result = await discoverMcpTools(serverId, scope); + setMcpServerTools({ ...mcpServerTools, [serverId]: result }); + } catch (error) { + setMcpServerTools({ + ...mcpServerTools, + [serverId]: { + success: false, + tools: [], + resources: [], + prompts: [] + } + }); + } finally { + setMcpToolsLoading({ ...mcpToolsLoading, [serverId]: false }); + } + }; + + const updateMcpConfig = (key, value) => { + setMcpFormData(prev => ({ + ...prev, + config: { + ...prev.config, + [key]: value + } + })); + // Reset test status when configuration changes + setMcpConfigTestResult(null); + setMcpConfigTested(false); + }; + + const handleTestConfiguration = async () => { + setMcpConfigTesting(true); + try { + const result = await testMcpConfiguration(mcpFormData); + setMcpConfigTestResult(result); + setMcpConfigTested(true); + } catch (error) { + setMcpConfigTestResult({ + success: false, + message: error.message, + details: [] + }); + setMcpConfigTested(true); + } finally { + setMcpConfigTesting(false); + } + }; + + const getTransportIcon = (type) => { + switch (type) { + case 'stdio': return ; + case 'sse': return ; + case 'http': return ; + default: return ; + } + }; + if (!isOpen) return null; return ( @@ -131,7 +509,7 @@ function ToolsSettings({ isOpen, onClose }) {

- Tools Settings + Settings

+ + + +
- {/* Theme Settings */} -
-
- {isDarkMode ? : } -

- Appearance -

-
-
-
-
-
- Dark Mode -
-
- Toggle between light and dark themes -
-
- -
- - {/* Project Sorting - Moved under Appearance */} -
-
-
-
- Project Sorting -
-
- How projects are ordered in the sidebar -
-
- -
-
-
+ {/* Appearance Tab */} + {activeTab === 'appearance' && ( +
+ {activeTab === 'appearance' && ( +
+ {/* Theme Settings */} +
+
+
+
+
+ Dark Mode
+
+ Toggle between light and dark themes +
+
+ +
+
+
+ + {/* Project Sorting */} +
+
+
+
+
+ Project Sorting +
+
+ How projects are ordered in the sidebar +
+
+ +
+
+
+
+)} + +
+ )} + + {/* Tools Tab */} + {activeTab === 'tools' && ( +
{/* Skip Permissions */}
@@ -393,6 +807,433 @@ function ToolsSettings({ isOpen, onClose }) {
  • "Bash(rm:*)" - Block all rm commands (dangerous)
  • + + {/* MCP Server Management */} +
    +
    + +

    + MCP Servers +

    +
    +
    +

    + Model Context Protocol servers provide additional tools and data sources to Claude +

    +
    + +
    + +
    + + {/* MCP Servers List */} +
    + {mcpServers.map(server => ( +
    +
    +
    +
    + {getTransportIcon(server.type)} + {server.name} + + {server.type} + + + {server.scope} + +
    + +
    + {server.type === 'stdio' && server.config.command && ( +
    Command: {server.config.command}
    + )} + {(server.type === 'sse' || server.type === 'http') && server.config.url && ( +
    URL: {server.config.url}
    + )} + {server.config.args && server.config.args.length > 0 && ( +
    Args: {server.config.args.join(' ')}
    + )} +
    + + {/* Test Results */} + {mcpTestResults[server.id] && ( +
    +
    {mcpTestResults[server.id].message}
    + {mcpTestResults[server.id].details && mcpTestResults[server.id].details.length > 0 && ( +
      + {mcpTestResults[server.id].details.map((detail, i) => ( +
    • • {detail}
    • + ))} +
    + )} +
    + )} + + {/* Tools Discovery Results */} + {mcpServerTools[server.id] && ( +
    +
    Available Tools & Resources
    + + {mcpServerTools[server.id].tools && mcpServerTools[server.id].tools.length > 0 && ( +
    +
    Tools ({mcpServerTools[server.id].tools.length}):
    +
      + {mcpServerTools[server.id].tools.map((tool, i) => ( +
    • + +
      + {tool.name} + {tool.description && tool.description !== 'No description provided' && ( + - {tool.description} + )} +
      +
    • + ))} +
    +
    + )} + + {mcpServerTools[server.id].resources && mcpServerTools[server.id].resources.length > 0 && ( +
    +
    Resources ({mcpServerTools[server.id].resources.length}):
    +
      + {mcpServerTools[server.id].resources.map((resource, i) => ( +
    • + +
      + {resource.name} + {resource.description && resource.description !== 'No description provided' && ( + - {resource.description} + )} +
      +
    • + ))} +
    +
    + )} + + {mcpServerTools[server.id].prompts && mcpServerTools[server.id].prompts.length > 0 && ( +
    +
    Prompts ({mcpServerTools[server.id].prompts.length}):
    +
      + {mcpServerTools[server.id].prompts.map((prompt, i) => ( +
    • + +
      + {prompt.name} + {prompt.description && prompt.description !== 'No description provided' && ( + - {prompt.description} + )} +
      +
    • + ))} +
    +
    + )} + + {(!mcpServerTools[server.id].tools || mcpServerTools[server.id].tools.length === 0) && + (!mcpServerTools[server.id].resources || mcpServerTools[server.id].resources.length === 0) && + (!mcpServerTools[server.id].prompts || mcpServerTools[server.id].prompts.length === 0) && ( +
    No tools, resources, or prompts discovered
    + )} +
    + )} +
    + +
    + + + + +
    +
    +
    + ))} + {mcpServers.length === 0 && ( +
    + No MCP servers configured +
    + )} +
    +
    + + {/* MCP Server Form Modal */} + {showMcpForm && ( +
    +
    +
    +

    + {editingMcpServer ? 'Edit MCP Server' : 'Add MCP Server'} +

    + +
    + +
    + {/* Basic Info */} +
    +
    + + { + setMcpFormData(prev => ({...prev, name: e.target.value})); + setMcpConfigTestResult(null); + setMcpConfigTested(false); + }} + placeholder="my-server" + required + /> +
    + +
    + + +
    +
    + + {/* Scope is fixed to user - no selection needed */} + + {/* Transport-specific Config */} + {mcpFormData.type === 'stdio' && ( +
    +
    + + updateMcpConfig('command', e.target.value)} + placeholder="/path/to/mcp-server" + required + /> +
    + +
    + +