"use client"; import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { PlusCircle, ServerIcon, X, Terminal, Globe, ExternalLink, Trash2, CheckCircle, Plus, Cog, Edit2, Eye, EyeOff, AlertTriangle, RefreshCw, Power } from "lucide-react"; import { toast } from "sonner"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"; import { KeyValuePair, MCPServer, ServerStatus, useMCP } from "@/lib/context/mcp-context"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./ui/tooltip"; // Default template for a new MCP server const INITIAL_NEW_SERVER: Omit = { name: '', url: '', type: 'sse', command: 'node', args: [], env: [], headers: [] }; interface MCPServerManagerProps { servers: MCPServer[]; onServersChange: (servers: MCPServer[]) => void; selectedServers: string[]; onSelectedServersChange: (serverIds: string[]) => void; open: boolean; onOpenChange: (open: boolean) => void; } // Check if a key name might contain sensitive information const isSensitiveKey = (key: string): boolean => { const sensitivePatterns = [ /key/i, /token/i, /secret/i, /password/i, /pass/i, /auth/i, /credential/i ]; return sensitivePatterns.some(pattern => pattern.test(key)); }; // Mask a sensitive value const maskValue = (value: string): string => { if (!value) return ''; if (value.length < 8) return '••••••'; return value.substring(0, 3) + '•'.repeat(Math.min(10, value.length - 4)) + value.substring(value.length - 1); }; // Update the StatusIndicator to use Tooltip component const StatusIndicator = ({ status, onClick, hoverInfo }: { status?: ServerStatus, onClick?: () => void, hoverInfo?: string }) => { const isClickable = !!onClick; const hasHoverInfo = !!hoverInfo; const className = `flex-shrink-0 flex items-center gap-1 ${isClickable ? 'cursor-pointer' : ''}`; const statusIndicator = (status: ServerStatus | undefined) => { switch (status) { case 'connected': return (
Connected
); case 'connecting': return (
Connecting
); case 'error': return (
Error
); case 'disconnected': default: return (
Disconnected
); } }; // Use Tooltip if we have hover info if (hasHoverInfo) { return ( {statusIndicator(status)} {hoverInfo} ); } // Otherwise just return the status indicator return statusIndicator(status); }; export const MCPServerManager = ({ servers, onServersChange, selectedServers, onSelectedServersChange, open, onOpenChange }: MCPServerManagerProps) => { const [newServer, setNewServer] = useState>(INITIAL_NEW_SERVER); const [view, setView] = useState<'list' | 'add'>('list'); const [newEnvVar, setNewEnvVar] = useState({ key: '', value: '' }); const [newHeader, setNewHeader] = useState({ key: '', value: '' }); const [editingServerId, setEditingServerId] = useState(null); const [showSensitiveEnvValues, setShowSensitiveEnvValues] = useState>({}); const [showSensitiveHeaderValues, setShowSensitiveHeaderValues] = useState>({}); const [editingEnvIndex, setEditingEnvIndex] = useState(null); const [editingHeaderIndex, setEditingHeaderIndex] = useState(null); const [editedEnvValue, setEditedEnvValue] = useState(''); const [editedHeaderValue, setEditedHeaderValue] = useState(''); // Add access to the MCP context for server control const { startServer, stopServer, updateServerStatus } = useMCP(); const resetAndClose = () => { setView('list'); setNewServer(INITIAL_NEW_SERVER); setNewEnvVar({ key: '', value: '' }); setNewHeader({ key: '', value: '' }); setShowSensitiveEnvValues({}); setShowSensitiveHeaderValues({}); setEditingEnvIndex(null); setEditingHeaderIndex(null); onOpenChange(false); }; const addServer = () => { if (!newServer.name) { toast.error("Server name is required"); return; } if (newServer.type === 'sse' && !newServer.url) { toast.error("Server URL is required for HTTP or SSE transport"); return; } if (newServer.type === 'stdio' && (!newServer.command || !newServer.args?.length)) { toast.error("Command and at least one argument are required for stdio transport"); return; } const id = crypto.randomUUID(); const updatedServers = [...servers, { ...newServer, id }]; onServersChange(updatedServers); toast.success(`Added MCP server: ${newServer.name}`); setView('list'); setNewServer(INITIAL_NEW_SERVER); setNewEnvVar({ key: '', value: '' }); setNewHeader({ key: '', value: '' }); setShowSensitiveEnvValues({}); setShowSensitiveHeaderValues({}); }; const removeServer = (id: string, e: React.MouseEvent) => { e.stopPropagation(); const updatedServers = servers.filter(server => server.id !== id); onServersChange(updatedServers); // If the removed server was selected, remove it from selected servers if (selectedServers.includes(id)) { onSelectedServersChange(selectedServers.filter(serverId => serverId !== id)); } toast.success("Server removed"); }; const toggleServer = (id: string) => { if (selectedServers.includes(id)) { // Remove from selected servers but DON'T stop the server onSelectedServersChange(selectedServers.filter(serverId => serverId !== id)); const server = servers.find(s => s.id === id); if (server) { toast.success(`Disabled MCP server: ${server.name}`); } } else { // Add to selected servers onSelectedServersChange([...selectedServers, id]); const server = servers.find(s => s.id === id); if (server) { // Auto-start the server if it's disconnected if (!server.status || server.status === 'disconnected' || server.status === 'error') { updateServerStatus(server.id, 'connecting'); startServer(id) .then(success => { if (success) { console.log(`Server ${server.name} successfully connected`); } else { console.error(`Failed to connect server ${server.name}`); } }) .catch(error => { console.error(`Error connecting server ${server.name}:`, error); updateServerStatus(server.id, 'error', `Failed to connect: ${error instanceof Error ? error.message : String(error)}`); }); } toast.success(`Enabled MCP server: ${server.name}`); } } }; const clearAllServers = () => { if (selectedServers.length > 0) { // Just deselect all servers without stopping them onSelectedServersChange([]); toast.success("All MCP servers disabled"); resetAndClose(); } }; const handleArgsChange = (value: string) => { try { // Try to parse as JSON if it starts with [ (array) const argsArray = value.trim().startsWith('[') ? JSON.parse(value) : value.split(' ').filter(Boolean); setNewServer({ ...newServer, args: argsArray }); } catch (error) { // If parsing fails, just split by spaces setNewServer({ ...newServer, args: value.split(' ').filter(Boolean) }); } }; const addEnvVar = () => { if (!newEnvVar.key) return; setNewServer({ ...newServer, env: [...(newServer.env || []), { ...newEnvVar }] }); setNewEnvVar({ key: '', value: '' }); }; const removeEnvVar = (index: number) => { const updatedEnv = [...(newServer.env || [])]; updatedEnv.splice(index, 1); setNewServer({ ...newServer, env: updatedEnv }); // Clean up visibility state for this index const updatedVisibility = { ...showSensitiveEnvValues }; delete updatedVisibility[index]; setShowSensitiveEnvValues(updatedVisibility); // If currently editing this value, cancel editing if (editingEnvIndex === index) { setEditingEnvIndex(null); } }; const startEditEnvValue = (index: number, value: string) => { setEditingEnvIndex(index); setEditedEnvValue(value); }; const saveEditedEnvValue = () => { if (editingEnvIndex !== null) { const updatedEnv = [...(newServer.env || [])]; updatedEnv[editingEnvIndex] = { ...updatedEnv[editingEnvIndex], value: editedEnvValue }; setNewServer({ ...newServer, env: updatedEnv }); setEditingEnvIndex(null); } }; const addHeader = () => { if (!newHeader.key) return; setNewServer({ ...newServer, headers: [...(newServer.headers || []), { ...newHeader }] }); setNewHeader({ key: '', value: '' }); }; const removeHeader = (index: number) => { const updatedHeaders = [...(newServer.headers || [])]; updatedHeaders.splice(index, 1); setNewServer({ ...newServer, headers: updatedHeaders }); // Clean up visibility state for this index const updatedVisibility = { ...showSensitiveHeaderValues }; delete updatedVisibility[index]; setShowSensitiveHeaderValues(updatedVisibility); // If currently editing this value, cancel editing if (editingHeaderIndex === index) { setEditingHeaderIndex(null); } }; const startEditHeaderValue = (index: number, value: string) => { setEditingHeaderIndex(index); setEditedHeaderValue(value); }; const saveEditedHeaderValue = () => { if (editingHeaderIndex !== null) { const updatedHeaders = [...(newServer.headers || [])]; updatedHeaders[editingHeaderIndex] = { ...updatedHeaders[editingHeaderIndex], value: editedHeaderValue }; setNewServer({ ...newServer, headers: updatedHeaders }); setEditingHeaderIndex(null); } }; const toggleSensitiveEnvValue = (index: number) => { setShowSensitiveEnvValues(prev => ({ ...prev, [index]: !prev[index] })); }; const toggleSensitiveHeaderValue = (index: number) => { setShowSensitiveHeaderValues(prev => ({ ...prev, [index]: !prev[index] })); }; const hasAdvancedConfig = (server: MCPServer) => { return (server.env && server.env.length > 0) || (server.headers && server.headers.length > 0); }; // Editing support const startEditing = (server: MCPServer) => { setEditingServerId(server.id); setNewServer({ name: server.name, url: server.url, type: server.type, command: server.command, args: server.args, env: server.env, headers: server.headers }); setView('add'); // Reset sensitive value visibility states setShowSensitiveEnvValues({}); setShowSensitiveHeaderValues({}); setEditingEnvIndex(null); setEditingHeaderIndex(null); }; const handleFormCancel = () => { if (view === 'add') { setView('list'); setEditingServerId(null); setNewServer(INITIAL_NEW_SERVER); setShowSensitiveEnvValues({}); setShowSensitiveHeaderValues({}); setEditingEnvIndex(null); setEditingHeaderIndex(null); } else { resetAndClose(); } }; const updateServer = () => { if (!newServer.name) { toast.error("Server name is required"); return; } if (newServer.type === 'sse' && !newServer.url) { toast.error("Server URL is required for HTTP or SSE transport"); return; } if (newServer.type === 'stdio' && (!newServer.command || !newServer.args?.length)) { toast.error("Command and at least one argument are required for stdio transport"); return; } const updated = servers.map(s => s.id === editingServerId ? { ...newServer, id: editingServerId! } : s ); onServersChange(updated); toast.success(`Updated MCP server: ${newServer.name}`); setView('list'); setEditingServerId(null); setNewServer(INITIAL_NEW_SERVER); setShowSensitiveEnvValues({}); setShowSensitiveHeaderValues({}); }; // Update functions to control servers const toggleServerStatus = async (server: MCPServer, e: React.MouseEvent) => { e.stopPropagation(); if (!server.status || server.status === 'disconnected' || server.status === 'error') { try { updateServerStatus(server.id, 'connecting'); const success = await startServer(server.id); if (success) { toast.success(`Started server: ${server.name}`); } else { toast.error(`Failed to start server: ${server.name}`); } } catch (error) { updateServerStatus(server.id, 'error', `Error: ${error instanceof Error ? error.message : String(error)}`); toast.error(`Error starting server: ${error instanceof Error ? error.message : String(error)}`); } } else { try { const success = await stopServer(server.id); if (success) { toast.success(`Stopped server: ${server.name}`); } else { toast.error(`Failed to stop server: ${server.name}`); } } catch (error) { toast.error(`Error stopping server: ${error instanceof Error ? error.message : String(error)}`); } } }; // Update function to restart a server const restartServer = async (server: MCPServer, e: React.MouseEvent) => { e.stopPropagation(); try { // First stop it if (server.status === 'connected' || server.status === 'connecting') { await stopServer(server.id); } // Then start it again (with delay to ensure proper cleanup) setTimeout(async () => { updateServerStatus(server.id, 'connecting'); const success = await startServer(server.id); if (success) { toast.success(`Restarted server: ${server.name}`); } else { toast.error(`Failed to restart server: ${server.name}`); } }, 500); } catch (error) { updateServerStatus(server.id, 'error', `Error: ${error instanceof Error ? error.message : String(error)}`); toast.error(`Error restarting server: ${error instanceof Error ? error.message : String(error)}`); } }; // UI element to display the correct server URL const getServerDisplayUrl = (server: MCPServer): string => { // Always show the configured URL or command, not the sandbox URL return server.type === 'sse' ? server.url : `${server.command} ${server.args?.join(' ')}`; }; // Update the hover info function to return richer content const getServerStatusHoverInfo = (server: MCPServer): string | undefined => { // For connected stdio servers, show the sandbox URL as hover info if (server.type === 'stdio' && server.status === 'connected' && server.sandboxUrl) { return `Running at: ${server.sandboxUrl}`; } // For error status, show the error message if (server.status === 'error' && server.errorMessage) { return `Error: ${server.errorMessage}`; } return undefined; }; return ( MCP Server Configuration Connect to Model Context Protocol servers to access additional AI tools. {selectedServers.length > 0 && ( {selectedServers.length} server{selectedServers.length !== 1 ? 's' : ''} currently active )} {view === 'list' ? (
{servers.length > 0 ? (

Available Servers

Select multiple servers to combine their tools
{servers .sort((a, b) => { const aActive = selectedServers.includes(a.id); const bActive = selectedServers.includes(b.id); if (aActive && !bActive) return -1; if (!aActive && bActive) return 1; return 0; }) .map((server) => { const isActive = selectedServers.includes(server.id); const isRunning = server.status === 'connected' || server.status === 'connecting'; return (
{/* Server Header with Type Badge and Actions */}
{server.type === 'sse' ? ( ) : ( )}

{server.name}

{hasAdvancedConfig(server) && ( )}
{server.type === "stdio" ? "STDIO" : server.url?.endsWith("/sse") ? "SSE" : "HTTP"} {/* Status indicator */} server.errorMessage && toast.error(server.errorMessage)} hoverInfo={getServerStatusHoverInfo(server)} /> {/* Server actions */}
{/* Server Details */}

{getServerDisplayUrl(server)}

{/* Action Button */}
); })}
) : (

No MCP Servers Added

Add your first MCP server to access additional AI tools

)}
) : (

{editingServerId ? "Edit MCP Server" : "Add New MCP Server"}

setNewServer({ ...newServer, name: e.target.value })} placeholder="My MCP Server" className="relative z-0" />

Choose how to connect to your MCP server:

{newServer.type === 'sse' ? (
setNewServer({ ...newServer, url: e.target.value })} placeholder="https://mcp.example.com/token/mcp" className="relative z-0" />

Full URL to the HTTP or SSE endpoint of the MCP server

) : ( <>
setNewServer({ ...newServer, command: e.target.value })} placeholder="node" className="relative z-0" />

Executable to run (e.g., node, python)

handleArgsChange(e.target.value)} placeholder="src/mcp-server.js --port 3001" className="relative z-0" />

Space-separated arguments or JSON array

)} {/* Advanced Configuration */} Environment Variables
setNewEnvVar({ ...newEnvVar, key: e.target.value })} placeholder="API_KEY" className="h-8 relative z-0" />
setNewEnvVar({ ...newEnvVar, value: e.target.value })} placeholder="your-secret-key" className="h-8 relative z-0" type="text" />
{newServer.env && newServer.env.length > 0 ? (
{newServer.env.map((env, index) => (
{env.key} = {editingEnvIndex === index ? (
setEditedEnvValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && saveEditedEnvValue()} autoFocus />
) : ( <> {isSensitiveKey(env.key) && !showSensitiveEnvValues[index] ? maskValue(env.value) : env.value} {isSensitiveKey(env.key) && ( )} )}
))}
) : (

No environment variables added

)}

Environment variables will be passed to the MCP server process.

{newServer.type === 'sse' ? 'HTTP Headers' : 'Additional Configuration'}
setNewHeader({ ...newHeader, key: e.target.value })} placeholder="Authorization" className="h-8 relative z-0" />
setNewHeader({ ...newHeader, value: e.target.value })} placeholder="Bearer token123" className="h-8 relative z-0" />
{newServer.headers && newServer.headers.length > 0 ? (
{newServer.headers.map((header, index) => (
{header.key} : {editingHeaderIndex === index ? (
setEditedHeaderValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && saveEditedHeaderValue()} autoFocus />
) : ( <> {isSensitiveKey(header.key) && !showSensitiveHeaderValues[index] ? maskValue(header.value) : header.value} {isSensitiveKey(header.key) && ( )} )}
))}
) : (

No {newServer.type === 'sse' ? 'headers' : 'additional configuration'} added

)}

{newServer.type === 'sse' ? 'HTTP headers will be sent with requests to the HTTP or SSE endpoint.' : 'Additional configuration parameters for the stdio transport.'}

)} {/* Persistent fixed footer with buttons */}
{view === 'list' ? ( <> ) : ( <> )}
); };