import React from 'react'; import { Button } from '@/components/ui/button'; import { ButtonLarge } from '@/components/ui/button-large'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { RiAddLine, RiDeleteBinLine, RiMore2Line, RiPlugLine, RiServerLine } from '@remixicon/react'; import { useMcpConfigStore, type McpDraft, type McpServerConfig } from '@/stores/useMcpConfigStore'; import { useMcpStore } from '@/stores/useMcpStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { isVSCodeRuntime } from '@/lib/desktop'; import { cn } from '@/lib/utils'; import { toast } from '@/components/ui'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; interface McpSidebarProps { onItemSelect?: () => void; } // ---- Status dot ---- type StatusTone = 'success' | 'error' | 'warning' | 'idle'; const statusToneFromMcp = (status: string | undefined): StatusTone => { switch (status) { case 'connected': return 'success'; case 'failed': return 'error'; case 'needs_auth': case 'needs_client_registration': return 'warning'; default: return 'idle'; } }; const StatusDot: React.FC<{ tone: StatusTone; enabled: boolean }> = ({ tone, enabled }) => { if (!enabled) { return ( ); } const classes: Record = { success: 'bg-green-500', error: 'bg-destructive', warning: 'bg-yellow-500', idle: 'bg-muted-foreground/40', }; return ( ); }; export const McpSidebar: React.FC = ({ onItemSelect }) => { const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar'; const { mcpServers, selectedMcpName, setSelectedMcp, setMcpDraft, loadMcpConfigs, deleteMcp } = useMcpConfigStore(); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const mcpStatus = useMcpStore((state) => state.getStatusForDirectory(currentDirectory ?? null)); const [deleteTarget, setDeleteTarget] = React.useState(null); const [isDeleting, setIsDeleting] = React.useState(false); React.useEffect(() => { void loadMcpConfigs(); }, [loadMcpConfigs]); const handleCreateNew = () => { const baseName = 'new-mcp-server'; let newName = baseName; let counter = 1; while (mcpServers.some((s) => s.name === newName)) { newName = `${baseName}-${counter}`; counter++; } const draft: McpDraft = { name: newName, scope: 'user', type: 'local', command: [], url: '', environment: [], enabled: true, }; setMcpDraft(draft); setSelectedMcp(newName); onItemSelect?.(); }; const handleDelete = async () => { if (!deleteTarget) return; setIsDeleting(true); const success = await deleteMcp(deleteTarget.name); if (success) { toast.success(`MCP server "${deleteTarget.name}" deleted`); } else { toast.error('Failed to delete MCP server'); } setDeleteTarget(null); setIsDeleting(false); }; return (
{/* Header */}
{mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''}
{/* List */} {mcpServers.length === 0 ? (

No MCP servers configured

Use the + button above to add one

) : ( mcpServers.map((server) => { const runtimeStatus = mcpStatus[server.name]; const tone = statusToneFromMcp(runtimeStatus?.status); const isSelected = selectedMcpName === server.name; return (
{ e.stopPropagation(); setDeleteTarget(server); }} className="text-destructive focus:text-destructive" > Delete
); }) )}
{/* Delete confirm dialog */} { if (!open && !isDeleting) setDeleteTarget(null); }} > Delete MCP Server Are you sure you want to delete "{deleteTarget?.name}"? This will remove it from{' '} opencode.json. {isDeleting ? 'Deleting…' : 'Delete'}
); }; // Re-export for easy sidebar icon usage export { RiServerLine as McpIcon };