"use client"; import { motion, AnimatePresence } from "framer-motion"; import React, { useState, useEffect } from "react"; import { CheckCircle, XCircle, AlertCircle, Key, Copy, Trash2, Upload, Plug, Plus, ChevronDown, ChevronRight, TestTube, Globe, Brain, Database, Package, Loader2, Shield, Lock, ClipboardPaste, Edit, Eye, EyeOff, ExternalLink } from "lucide-react"; import { useQuery, useMutation } from "convex/react"; import { api } from "@/convex/_generated/api"; import { toast } from "sonner"; import { useUser } from "@clerk/nextjs"; import { Id } from "@/convex/_generated/dataModel"; import PasteConfigModal from "./PasteConfigModal"; interface SettingsPanelProps { isOpen: boolean; onClose: () => void; } interface ServerAPIConfig { anthropicConfigured: boolean; groqConfigured: boolean; openaiConfigured: boolean; hasKeys: boolean; } interface MCPServer { _id: Id<"mcpServers">; userId: string; name: string; url: string; description?: string; category: string; authType: string; accessToken?: string; tools?: string[]; connectionStatus: string; lastTested?: string; lastError?: string; enabled: boolean; isOfficial: boolean; headers?: any; createdAt: string; updatedAt: string; } interface MCPCardProps { server: MCPServer; onTest: () => void; onEdit: () => void; onDelete: () => void; onToggle: () => void; isExpanded: boolean; onExpandToggle: () => void; isTesting: boolean; } export default function SettingsPanel({ isOpen, onClose }: SettingsPanelProps) { const { user } = useUser(); const [serverConfig, setServerConfig] = useState(null); const [loading, setLoading] = useState(true); const [newKeyName, setNewKeyName] = useState(""); const [generatedKey, setGeneratedKey] = useState(null); const [showAddLLMKey, setShowAddLLMKey] = useState(false); const [selectedProvider, setSelectedProvider] = useState<'anthropic' | 'openai' | 'groq' | null>(null); const apiKeys = useQuery(api.apiKeys.list, {}); const generateKey = useMutation(api.apiKeys.generate); const revokeKey = useMutation(api.apiKeys.revoke); // LLM Keys queries and mutations const userLLMKeys = useQuery(api.userLLMKeys.getUserLLMKeys, user?.id ? { userId: user.id } : "skip" ); const upsertLLMKey = useMutation(api.userLLMKeys.upsertLLMKey); const deleteLLMKey = useMutation(api.userLLMKeys.deleteLLMKey); const toggleLLMKeyActive = useMutation(api.userLLMKeys.toggleKeyActive); // MCP Registry state const mcpServers = useQuery(api.mcpServers.listUserMCPs, user?.id ? { userId: user.id } : "skip" ) as MCPServer[] | undefined; const addMCPServer = useMutation(api.mcpServers.addMCPServer); const updateMCPServer = useMutation(api.mcpServers.updateMCPServer); const deleteMCPServer = useMutation(api.mcpServers.deleteMCPServer); const toggleMCPEnabled = useMutation(api.mcpServers.toggleMCPEnabled); const seedOfficialMCPs = useMutation(api.mcpServers.seedOfficialMCPs); const updateConnectionStatus = useMutation(api.mcpServers.updateConnectionStatus); const cleanupOfficialMCPs = useMutation(api.mcpServers.cleanupOfficialMCPs); const [expandedMCPs, setExpandedMCPs] = useState>(new Set()); const [testingMCPs, setTestingMCPs] = useState>(new Set()); const [showAddMCPModal, setShowAddMCPModal] = useState(false); const [showPasteConfigModal, setShowPasteConfigModal] = useState(false); const [editingMCP, setEditingMCP] = useState(null); useEffect(() => { const loadConfig = async () => { try { const response = await fetch('/api/config'); if (response.ok) { const config = await response.json(); setServerConfig(config); } } catch (error) { console.warn('Failed to load server API config:', error); } finally { setLoading(false); } }; if (isOpen) { loadConfig(); // Clean up any official MCPs and seed them if (user?.id && mcpServers) { // First clean up any official MCPs cleanupOfficialMCPs({ userId: user.id }).catch(console.error); // Then seed official MCPs if none exist const hasOfficialMCPs = mcpServers.some(mcp => mcp.isOfficial); if (!hasOfficialMCPs) { seedOfficialMCPs({ userId: user.id }).catch(console.error); } } } }, [isOpen, user?.id, mcpServers?.length]); if (!isOpen) return null; const StatusIcon = ({ configured }: { configured: boolean }) => configured ? ( ) : ( ); return ( e.stopPropagation()} > {/* Header - Fixed */}

Configuration Status

API keys are configured in your .env.local file

{/* Content - Scrollable */}
{loading ? (

Loading configuration...

) : (
{/* LLM Providers */}

LLM Providers

{/* Provider Cards with Keys */}
{['anthropic', 'openai', 'groq'].map(provider => { const providerKey = userLLMKeys?.find(k => k.provider === provider && k.isActive); const hasEnvKey = provider === 'anthropic' ? serverConfig?.anthropicConfigured : provider === 'openai' ? serverConfig?.openaiConfigured : serverConfig?.groqConfigured; return (

{provider}

{providerKey ? (

Key: {providerKey.keyPrefix} {providerKey.label && `(${providerKey.label})`}

) : hasEnvKey ? (

Using environment variable

) : (

Not configured

)}
{providerKey && ( <> )}
); })}

Note: Your API keys take priority over environment variables. Keys are encrypted and stored securely per user.

{/* Integrations */}

Integrations

Composio

{/* API Keys */}

Your API Keys

{/* Show generated key once */} {generatedKey && (

Save this key now!

You won't be able to see it again.

{generatedKey}
)} {/* Generate new key */}
setNewKeyName(e.target.value)} className="flex-1 px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black placeholder:text-black-alpha-32" />
{/* List existing keys */}
{apiKeys?.map((key) => (

{key.name}

{key.keyPrefix}

Used {key.usageCount} times {key.lastUsedAt && `• Last used ${new Date(key.lastUsedAt).toLocaleDateString()}`}

))} {(!apiKeys || apiKeys.length === 0) && !generatedKey && (

No API keys yet. Generate one to access workflows via API.

)}
{/* MCP Registry */}

MCP Registry

{/* MCP Cards */}
{mcpServers?.map((server) => ( { const newExpanded = new Set(expandedMCPs); if (newExpanded.has(server._id)) { newExpanded.delete(server._id); } else { newExpanded.add(server._id); } setExpandedMCPs(newExpanded); }} onToggle={async () => { await toggleMCPEnabled({ id: server._id }); toast.success(`${server.name} ${server.enabled ? 'disabled' : 'enabled'}`); }} onTest={async () => { setTestingMCPs(prev => new Set(Array.from(prev).concat(server._id))); try { // Actually test the connection and discover tools const response = await fetch('/api/test-mcp-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: server.url, authToken: server.accessToken, headers: server.headers, }), }); const result = await response.json(); if (result.success) { // Update with real discovered tools await updateConnectionStatus({ id: server._id, status: "connected", tools: result.tools || [] }); toast.success(`Connected to ${server.name} - ${result.tools?.length || 0} tools discovered`); } else { await updateConnectionStatus({ id: server._id, status: "error", error: result.error || "Connection failed" }); toast.error(`Failed to connect to ${server.name}`, { description: result.error || result.details, }); } } catch (error) { await updateConnectionStatus({ id: server._id, status: "error", error: error instanceof Error ? error.message : "Connection failed" }); toast.error(`Failed to connect to ${server.name}`); } finally { setTestingMCPs(prev => { const newSet = new Set(prev); newSet.delete(server._id); return newSet; }); } }} onEdit={() => { setEditingMCP(server); setShowAddMCPModal(true); }} onDelete={async () => { if (confirm(`Delete ${server.name}?`)) { await deleteMCPServer({ id: server._id }); toast.success(`${server.name} deleted`); } }} /> ))} {(!mcpServers || mcpServers.length === 0) && (

No MCP servers configured yet

)}
{/* Info Box */}

How to Configure

LLM & Integration keys are set in .env.local. MCP servers can be imported from cursor config above.

)}
{/* Footer - Fixed */}
{/* Add MCP Modal */} {showAddMCPModal && ( { setShowAddMCPModal(false); setEditingMCP(null); }} editingServer={editingMCP} onSave={async (data) => { try { if (editingMCP) { // Update existing server await updateMCPServer({ id: editingMCP._id, ...data }); toast.success(`${data.name} updated`); setShowAddMCPModal(false); setEditingMCP(null); } else if (user?.id) { // If tools already discovered via Test Connection button, use those if (data.tools && data.tools.length > 0) { await addMCPServer({ userId: user.id, ...data, }); toast.success(`${data.name} added with ${data.tools.length} tools`); } else { // Otherwise, test the connection first to discover tools const loadingToast = toast.loading(`Testing connection to ${data.name}...`); const testResponse = await fetch('/api/test-mcp-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: data.url, authToken: data.accessToken, headers: data.headers, }), }); const testResult = await testResponse.json(); if (!testResult.success) { toast.error(`Failed to connect to ${data.name}`, { description: testResult.error || 'Unable to discover tools', id: loadingToast, }); return; // Don't save if connection fails } // Save with discovered tools await addMCPServer({ userId: user.id, ...data, tools: testResult.tools || [], }); toast.success(`${data.name} added with ${testResult.tools?.length || 0} tools`, { id: loadingToast, }); } } setShowAddMCPModal(false); } catch (error) { toast.error('Failed to save MCP server'); } }} /> )} {/* Add/Edit LLM Key Modal */} {showAddLLMKey && ( { setShowAddLLMKey(false); setSelectedProvider(null); }} selectedProvider={selectedProvider} onSave={async (provider, apiKey, label) => { if (user?.id) { await upsertLLMKey({ userId: user.id, provider, apiKey, label }); toast.success(`${provider} API key saved`); setShowAddLLMKey(false); setSelectedProvider(null); } }} /> )} {/* Paste Config Modal */} {showPasteConfigModal && ( setShowPasteConfigModal(false)} onSave={async (servers) => { try { if (!user?.id) { toast.error('User not authenticated'); return; } // Save all servers from the pasted config const importedServers: any[] = []; for (const serverData of servers) { // Check if server with same name already exists const existingServer = mcpServers?.find(s => s.name === serverData.name); let serverId: string; if (existingServer) { // Update existing server await updateMCPServer({ id: existingServer._id, ...serverData, }); serverId = existingServer._id; } else { // Add new server const newServerId = await addMCPServer({ userId: user.id, ...serverData, }); serverId = newServerId; } importedServers.push({ ...serverData, _id: serverId }); } // Test all imported servers automatically toast.info(`Testing ${importedServers.length} imported server${importedServers.length > 1 ? 's' : ''}...`); for (const server of importedServers) { try { const testResponse = await fetch('/api/test-mcp-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: server.url, authToken: server.accessToken, headers: server.headers, }), }); const testResult = await testResponse.json(); if (testResult.success) { // Update with discovered tools await updateConnectionStatus({ id: server._id, status: "connected", tools: testResult.tools || [] }); toast.success(`✅ ${server.name}: ${testResult.tools?.length || 0} tools discovered`); } else { await updateConnectionStatus({ id: server._id, status: "error", error: testResult.error || "Connection failed" }); toast.error(`❌ ${server.name}: ${testResult.error || 'Connection failed'}`); } } catch (error) { await updateConnectionStatus({ id: server._id, status: "error", error: error instanceof Error ? error.message : "Test failed" }); toast.error(`❌ ${server.name}: Test failed`); } } setShowPasteConfigModal(false); } catch (error) { toast.error('Failed to import MCP configuration'); console.error('Import error:', error); } }} /> )}
); } // MCP Card Component function MCPCard({ server, isExpanded, isTesting, onExpandToggle, onToggle, onTest, onEdit, onDelete }: MCPCardProps) { const getCategoryIcon = () => { switch (server.category) { case 'web': return ; case 'ai': return ; case 'data': return ; default: return ; } }; const getStatusColor = () => { switch (server.connectionStatus) { case 'connected': return 'text-heat-100'; case 'error': return 'text-accent-black'; default: return 'text-black-alpha-32'; } }; const getStatusIcon = () => { if (isTesting) { return ; } switch (server.connectionStatus) { case 'connected': return ; case 'error': return ; default: return ; } }; const getAuthIcon = () => { switch (server.authType) { case 'api-key': return ; case 'bearer': return ; case 'oauth-coming-soon': return ; default: return null; } }; return (
{/* Header */}
{getCategoryIcon()}

{server.name}

{server.isOfficial && ( Official )} {getAuthIcon()}
{server.description && (

{server.description}

)}
{getStatusIcon()}
{/* Expanded Content */} {isExpanded && (
{/* URL & Auth */}

URL

{server.url}

Authentication

{server.authType === 'oauth-coming-soon' ? 'OAuth (Coming Soon)' : server.authType.replace('-', ' ')} {server.accessToken && •••••••}
{/* Tools */} {server.tools && server.tools.length > 0 && (

Available Tools ({server.tools.length})

{server.tools.map((tool) => ( {tool} ))}
)} {/* Status */}
{server.lastTested && ( Last tested: {new Date(server.lastTested).toLocaleString()} )} {server.lastError && ( Error: {server.lastError} )}
{/* Actions */}
{!server.isOfficial && ( )}
)}
); } // Add MCP Modal Component interface AddMCPModalProps { isOpen: boolean; onClose: () => void; editingServer?: MCPServer | null; onSave: (data: { name: string; url: string; description?: string; category: string; authType: string; accessToken?: string; tools?: string[]; headers?: any; }) => Promise; } function AddMCPModal({ isOpen, onClose, onSave, editingServer }: AddMCPModalProps) { const [formData, setFormData] = useState({ name: editingServer?.name || '', url: editingServer?.url || '', description: editingServer?.description || '', category: editingServer?.category || 'custom', authType: editingServer?.authType || 'none', accessToken: editingServer?.accessToken || '' }); const [isTesting, setIsTesting] = useState(false); const [discoveredTools, setDiscoveredTools] = useState(editingServer?.tools || null); if (!isOpen) return null; return ( e.stopPropagation()} >

{editingServer ? 'Edit MCP Server' : 'Add MCP Server'}

setFormData({ ...formData, name: e.target.value })} placeholder="e.g., My MCP Server" className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black" />
setFormData({ ...formData, url: e.target.value })} placeholder="https://mcp.example.com" className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono" />
setFormData({ ...formData, description: e.target.value })} placeholder="Brief description of this MCP server" className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black" />
{(formData.authType === 'api-key' || formData.authType === 'bearer') && (
setFormData({ ...formData, accessToken: e.target.value })} placeholder={formData.authType === 'api-key' ? 'sk-...' : 'Bearer token'} className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono" />
)} {/* Test Connection Button */}
{/* Discovered Tools */} {discoveredTools && discoveredTools.length > 0 && (
{discoveredTools.map((tool) => ( {tool} ))}
)}
); } // Add LLM Key Modal Component interface AddLLMKeyModalProps { isOpen: boolean; onClose: () => void; selectedProvider: 'anthropic' | 'openai' | 'groq' | null; onSave: (provider: string, apiKey: string, label?: string) => Promise; } function AddLLMKeyModal({ isOpen, onClose, selectedProvider, onSave }: AddLLMKeyModalProps) { const [formData, setFormData] = useState({ provider: selectedProvider || 'anthropic', apiKey: '', label: '', }); const [showKey, setShowKey] = useState(false); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (selectedProvider) { setFormData(prev => ({ ...prev, provider: selectedProvider })); } }, [selectedProvider]); if (!isOpen) return null; const getProviderHelpLink = (provider: string) => { switch (provider) { case 'anthropic': return 'https://console.anthropic.com/settings/keys'; case 'openai': return 'https://platform.openai.com/api-keys'; case 'groq': return 'https://console.groq.com/keys'; default: return '#'; } }; return ( e.stopPropagation()} >

{selectedProvider ? `Update ${selectedProvider} API Key` : 'Add LLM API Key'}

setFormData({ ...formData, apiKey: e.target.value })} placeholder={ formData.provider === 'anthropic' ? 'sk-ant-...' : formData.provider === 'openai' ? 'sk-proj-...' : 'gsk_...' } className="w-full pr-32 px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black font-mono" />
Get your {formData.provider} API key →
setFormData({ ...formData, label: e.target.value })} placeholder="e.g., Production, Development" className="w-full px-12 py-8 bg-background-base border border-border-faint rounded-8 text-body-small text-accent-black" />

Security: Your API key will be encrypted and stored securely. It will only be accessible by you and never shared across users.

); }