import { useState, useEffect } from 'react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Badge } from './ui/badge'; import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn } from 'lucide-react'; import { useTheme } from '../contexts/ThemeContext'; import { useTasksSettings } from '../contexts/TasksSettingsContext'; import StandaloneShell from './StandaloneShell'; import ClaudeLogo from './ClaudeLogo'; import CursorLogo from './CursorLogo'; function Settings({ isOpen, onClose, projects = [] }) { const { isDarkMode, toggleDarkMode } = useTheme(); const { tasksEnabled, setTasksEnabled, isTaskMasterInstalled, isTaskMasterReady, installationStatus, isCheckingInstallation } = useTasksSettings(); const [allowedTools, setAllowedTools] = useState([]); const [disallowedTools, setDisallowedTools] = useState([]); const [newAllowedTool, setNewAllowedTool] = useState(''); const [newDisallowedTool, setNewDisallowedTool] = useState(''); const [skipPermissions, setSkipPermissions] = useState(false); const [isSaving, setIsSaving] = useState(false); const [saveStatus, setSaveStatus] = useState(null); const [projectSortOrder, setProjectSortOrder] = useState('name'); const [mcpServers, setMcpServers] = useState([]); const [showMcpForm, setShowMcpForm] = useState(false); const [editingMcpServer, setEditingMcpServer] = useState(null); const [mcpFormData, setMcpFormData] = useState({ name: '', type: 'stdio', scope: 'user', projectPath: '', // For local scope config: { command: '', args: [], env: {}, url: '', headers: {}, timeout: 30000 }, jsonInput: '', // For JSON import importMode: 'form' // 'form' or 'json' }); const [mcpLoading, setMcpLoading] = useState(false); const [mcpTestResults, setMcpTestResults] = useState({}); const [mcpServerTools, setMcpServerTools] = useState({}); const [mcpToolsLoading, setMcpToolsLoading] = useState({}); const [activeTab, setActiveTab] = useState('tools'); const [jsonValidationError, setJsonValidationError] = useState(''); const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor' // Cursor-specific states const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]); const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]); const [cursorSkipPermissions, setCursorSkipPermissions] = useState(false); const [newCursorCommand, setNewCursorCommand] = useState(''); const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState(''); const [cursorMcpServers, setCursorMcpServers] = useState([]); // Login modal states const [showLoginModal, setShowLoginModal] = useState(false); const [loginProvider, setLoginProvider] = useState(''); // 'claude' or 'cursor' const [selectedProject, setSelectedProject] = useState(null); // Common tool patterns for Claude const commonTools = [ 'Bash(git log:*)', 'Bash(git diff:*)', 'Bash(git status:*)', 'Write', 'Read', 'Edit', 'Glob', 'Grep', 'MultiEdit', 'Task', 'TodoWrite', 'TodoRead', 'WebFetch', 'WebSearch' ]; // Common shell commands for Cursor const commonCursorCommands = [ 'Shell(ls)', 'Shell(mkdir)', 'Shell(cd)', 'Shell(cat)', 'Shell(echo)', 'Shell(git status)', 'Shell(git diff)', 'Shell(git log)', 'Shell(npm install)', 'Shell(npm run)', 'Shell(python)', 'Shell(node)' ]; // Fetch Cursor MCP servers const fetchCursorMcpServers = async () => { try { const token = localStorage.getItem('auth-token'); const response = await fetch('/api/cursor/mcp', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); setCursorMcpServers(data.servers || []); } else { console.error('Failed to fetch Cursor MCP servers'); } } catch (error) { console.error('Error fetching Cursor MCP servers:', error); } }; // MCP API functions const fetchMcpServers = async () => { try { const token = localStorage.getItem('auth-token'); // 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}`, '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; } } // Final 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, scope: serverData.scope, projectPath: serverData.projectPath, 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 with proper scope const response = await fetch(`/api/mcp/cli/remove/${serverId}?scope=${scope}`, { 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 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 = async () => { try { // Load Claude settings from localStorage const savedSettings = localStorage.getItem('claude-settings'); if (savedSettings) { const settings = JSON.parse(savedSettings); setAllowedTools(settings.allowedTools || []); setDisallowedTools(settings.disallowedTools || []); setSkipPermissions(settings.skipPermissions || false); setProjectSortOrder(settings.projectSortOrder || 'name'); } else { // Set defaults setAllowedTools([]); setDisallowedTools([]); setSkipPermissions(false); setProjectSortOrder('name'); } // Load Cursor settings from localStorage const savedCursorSettings = localStorage.getItem('cursor-tools-settings'); if (savedCursorSettings) { const cursorSettings = JSON.parse(savedCursorSettings); setCursorAllowedCommands(cursorSettings.allowedCommands || []); setCursorDisallowedCommands(cursorSettings.disallowedCommands || []); setCursorSkipPermissions(cursorSettings.skipPermissions || false); } else { // Set Cursor defaults setCursorAllowedCommands([]); setCursorDisallowedCommands([]); setCursorSkipPermissions(false); } // Load MCP servers from API await fetchMcpServers(); // Load Cursor MCP servers await fetchCursorMcpServers(); } catch (error) { console.error('Error loading tool settings:', error); // Set defaults on error setAllowedTools([]); setDisallowedTools([]); setSkipPermissions(false); setProjectSortOrder('name'); } }; // Login handlers const handleClaudeLogin = () => { setLoginProvider('claude'); setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() }); setShowLoginModal(true); }; const handleCursorLogin = () => { setLoginProvider('cursor'); setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() }); setShowLoginModal(true); }; const handleLoginComplete = (exitCode) => { if (exitCode === 0) { // Login successful - could show a success message here } setShowLoginModal(false); }; const saveSettings = () => { setIsSaving(true); setSaveStatus(null); try { // Save Claude settings const claudeSettings = { allowedTools, disallowedTools, skipPermissions, projectSortOrder, lastUpdated: new Date().toISOString() }; // Save Cursor settings const cursorSettings = { allowedCommands: cursorAllowedCommands, disallowedCommands: cursorDisallowedCommands, skipPermissions: cursorSkipPermissions, lastUpdated: new Date().toISOString() }; // Save to localStorage localStorage.setItem('claude-settings', JSON.stringify(claudeSettings)); localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings)); setSaveStatus('success'); setTimeout(() => { onClose(); }, 1000); } catch (error) { console.error('Error saving tool settings:', error); setSaveStatus('error'); } finally { setIsSaving(false); } }; const addAllowedTool = (tool) => { if (tool && !allowedTools.includes(tool)) { setAllowedTools([...allowedTools, tool]); setNewAllowedTool(''); } }; const removeAllowedTool = (tool) => { setAllowedTools(allowedTools.filter(t => t !== tool)); }; const addDisallowedTool = (tool) => { if (tool && !disallowedTools.includes(tool)) { setDisallowedTools([...disallowedTools, tool]); setNewDisallowedTool(''); } }; const removeDisallowedTool = (tool) => { setDisallowedTools(disallowedTools.filter(t => t !== tool)); }; // MCP form handling functions const resetMcpForm = () => { setMcpFormData({ name: '', type: 'stdio', scope: 'user', // Default to user scope projectPath: '', config: { command: '', args: [], env: {}, url: '', headers: {}, timeout: 30000 }, jsonInput: '', importMode: 'form' }); setEditingMcpServer(null); setShowMcpForm(false); setJsonValidationError(''); }; const openMcpForm = (server = null) => { if (server) { setEditingMcpServer(server); setMcpFormData({ name: server.name, type: server.type, scope: server.scope, projectPath: server.projectPath || '', config: { ...server.config }, raw: server.raw, // Store raw config for display importMode: 'form', // Always use form mode when editing jsonInput: '' }); } else { resetMcpForm(); } setShowMcpForm(true); }; const handleMcpSubmit = async (e) => { e.preventDefault(); setMcpLoading(true); try { if (mcpFormData.importMode === 'json') { // Use JSON import endpoint const token = localStorage.getItem('auth-token'); const response = await fetch('/api/mcp/cli/add-json', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: mcpFormData.name, jsonConfig: mcpFormData.jsonInput, scope: mcpFormData.scope, projectPath: mcpFormData.projectPath }) }); if (response.ok) { const result = await response.json(); if (result.success) { await fetchMcpServers(); // Refresh the list resetMcpForm(); setSaveStatus('success'); } else { throw new Error(result.error || 'Failed to add server via JSON'); } } else { const error = await response.json(); throw new Error(error.error || 'Failed to add server'); } } else { // Use regular form-based save 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 } })); }; const getTransportIcon = (type) => { switch (type) { case 'stdio': return ; case 'sse': return ; case 'http': return ; default: return ; } }; if (!isOpen) return null; return (

Settings

{/* Tab Navigation */}
{/* 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' && (
{/* Provider Tabs */}
{/* Claude Content */} {toolsProvider === 'claude' && (
{/* Skip Permissions */}

Permission Settings

{/* Claude Login */}

Authentication

Claude CLI Login
Sign in to your Claude account to enable AI features
{/* Allowed Tools */}

Allowed Tools

Tools that are automatically allowed without prompting for permission

setNewAllowedTool(e.target.value)} placeholder='e.g., "Bash(git log:*)" or "Write"' onKeyPress={(e) => { if (e.key === 'Enter') { addAllowedTool(newAllowedTool); } }} className="flex-1 h-10 touch-manipulation" style={{ fontSize: '16px' }} />
{/* Common tools quick add */}

Quick add common tools:

{commonTools.map(tool => ( ))}
{allowedTools.map(tool => (
{tool}
))} {allowedTools.length === 0 && (
No allowed tools configured
)}
{/* Disallowed Tools */}

Disallowed Tools

Tools that are automatically blocked without prompting for permission

setNewDisallowedTool(e.target.value)} placeholder='e.g., "Bash(rm:*)" or "Write"' onKeyPress={(e) => { if (e.key === 'Enter') { addDisallowedTool(newDisallowedTool); } }} className="flex-1 h-10 touch-manipulation" style={{ fontSize: '16px' }} />
{disallowedTools.map(tool => (
{tool}
))} {disallowedTools.length === 0 && (
No disallowed tools configured
)}
{/* Help Section */}

Tool Pattern Examples:

  • "Bash(git log:*)" - Allow all git log commands
  • "Bash(git diff:*)" - Allow all git diff commands
  • "Write" - Allow all Write tool usage
  • "Read" - Allow all Read tool usage
  • "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 === 'local' ? '📁 local' : server.scope === 'user' ? '👤 user' : server.scope} {server.projectPath && ( {server.projectPath.split('/').pop()} )}
{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(' ')}
)} {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 */} {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'}

{!editingMcpServer && (
)} {/* Show current scope when editing */} {mcpFormData.importMode === 'form' && editingMcpServer && (
{mcpFormData.scope === 'user' ? : } {mcpFormData.scope === 'user' ? 'User (Global)' : 'Project (Local)'} {mcpFormData.scope === 'local' && mcpFormData.projectPath && ( - {mcpFormData.projectPath} )}

Scope cannot be changed when editing an existing server

)} {/* Scope Selection - Moved to top, disabled when editing */} {mcpFormData.importMode === 'form' && !editingMcpServer && (

{mcpFormData.scope === 'user' ? 'User scope: Available across all projects on your machine' : 'Local scope: Only available in the selected project' }

{/* Project Selection for Local Scope */} {mcpFormData.scope === 'local' && !editingMcpServer && (
{mcpFormData.projectPath && (

Path: {mcpFormData.projectPath}

)}
)}
)} {/* Basic Info */}
{ setMcpFormData(prev => ({...prev, name: e.target.value})); }} placeholder="my-server" required />
{mcpFormData.importMode === 'form' && (
)}
{/* Show raw configuration details when editing */} {editingMcpServer && mcpFormData.raw && mcpFormData.importMode === 'form' && (

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

                          {JSON.stringify(mcpFormData.raw, null, 2)}
                        
)} {/* JSON Import Mode */} {mcpFormData.importMode === 'json' && (