import { useEffect, useState } from 'react'; import { classNames } from '~/utils/classNames'; import { Dialog, DialogRoot, DialogClose, DialogTitle, DialogButton } from '~/components/ui/Dialog'; import { useMCPConfig, type MCPConfig } from '~/lib/hooks/useMCPConfig'; import { IconButton } from '~/components/ui/IconButton'; // Example MCP configuration that users can load const EXAMPLE_MCP_CONFIG: MCPConfig = { mcpServers: { everything: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-everything'], }, git: { command: 'uvx', args: ['mcp-server-git'], }, 'remote-sse': { type: 'sse', url: 'http://localhost:8000/sse', }, }, }; type ServerStatus = Record; type ServerErrors = Record; type ServerTools = Record; export function McpConnection() { const { config, updateConfig, lastUpdate, isLoading } = useMCPConfig(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [configText, setConfigText] = useState(''); const [error, setError] = useState(null); const [serverStatus, setServerStatus] = useState({}); const [serverErrors, setServerErrors] = useState({}); const [serverTools, setServerTools] = useState({}); const [checkingServers, setCheckingServers] = useState(false); const [configTextParsed, setConfigTextParsed] = useState(null); const [expandedServer, setExpandedServer] = useState(null); const [isInitialLoad, setIsInitialLoad] = useState(true); // Initialize config text from config useEffect(() => { if (config) { setConfigText(JSON.stringify(config, null, 2)); setConfigTextParsed(config); } else { setConfigText(JSON.stringify({ mcpServers: {} }, null, 2)); setConfigTextParsed({ mcpServers: {} }); } }, [config, lastUpdate]); // Check server availability on initial load useEffect(() => { if (isInitialLoad && configTextParsed?.mcpServers && Object.keys(configTextParsed.mcpServers).length > 0) { checkServerAvailability(); setIsInitialLoad(false); } }, [configTextParsed, isInitialLoad]); // Reset initial load flag when config is updated externally useEffect(() => { setIsInitialLoad(true); }, [lastUpdate]); // Parse the textarea content when it changes useEffect(() => { try { const parsed = JSON.parse(configText) as MCPConfig; setConfigTextParsed(parsed); if (error?.includes('JSON')) { setError(null); } } catch (e) { setConfigTextParsed(null); setError(`Invalid JSON format: ${e instanceof Error ? e.message : String(e)}`); } }, [configText, error]); const handleSave = () => { try { const parsed = JSON.parse(configText) as MCPConfig; updateConfig(parsed); setError(null); setIsDialogOpen(false); } catch { setError('Invalid JSON format'); } }; const handleLoadExample = () => { setConfigText(JSON.stringify(EXAMPLE_MCP_CONFIG, null, 2)); setError(null); }; const checkServerAvailability = async () => { try { const parsed = JSON.parse(configText) as MCPConfig; if (!parsed?.mcpServers) { setError('No servers configured or invalid configuration'); return; } setCheckingServers(true); setError(null); setServerErrors({}); setServerTools({}); try { const response = await fetch('/api/mcp-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mcpServers: parsed.mcpServers }), }); if (!response.ok) { throw new Error(`Server responded with ${response.status}: ${response.statusText}`); } const data = await response.json(); if (typeof data === 'object' && data !== null && 'serverStatus' in data && 'serverErrors' in data) { setServerStatus(data.serverStatus as ServerStatus); setServerErrors((data.serverErrors as ServerErrors) || ({} as ServerErrors)); if ('serverTools' in data) { setServerTools((data.serverTools as ServerTools) || ({} as ServerTools)); } } else { throw new Error('Invalid response format from server'); } } catch (e) { setError(`Failed to check server availability: ${e instanceof Error ? e.message : String(e)}`); } finally { setCheckingServers(false); } } catch (e) { setError(`Invalid JSON format: ${e instanceof Error ? e.message : String(e)}`); setCheckingServers(false); } }; const toggleServerExpanded = (serverName: string) => { setExpandedServer(expandedServer === serverName ? null : serverName); }; const handleDialogOpen = (open: boolean) => { setIsDialogOpen(open); if ( open && configTextParsed?.mcpServers && Object.keys(configTextParsed.mcpServers).length > 0 && Object.keys(serverStatus).length === 0 ) { checkServerAvailability(); } }; const formatToolSchema = (toolName: string, toolSchema: any) => { if (!toolSchema) { return null; } try { const parameters = toolSchema.parameters?.properties || {}; return (
{toolName}
{toolSchema.description || 'No description available'}
{Object.keys(parameters).length > 0 && (
Parameters:
{Object.entries(parameters).map(([paramName, paramDetails]: [string, any]) => (
{paramName} {paramDetails.required && *} {paramDetails.type || 'any'} {paramDetails.description ? ` - ${paramDetails.description}` : ''}
))}
)}
); } catch (e) { return (
Error parsing tool schema: {e instanceof Error ? e.message : String(e)}
); } }; const StatusBadge = ({ status }: { status: 'checking' | 'available' | 'unavailable' }) => { const badgeStyles = { checking: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', available: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', unavailable: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', }; const text = { checking: 'Checking...', available: 'Available', unavailable: 'Unavailable', }; const icon = status === 'checking' ? (
) : null; return ( {icon} {text[status]} ); }; const renderServerList = () => { if (!configTextParsed?.mcpServers) { return

Invalid configuration or no servers defined

; } const serverEntries = Object.entries(configTextParsed.mcpServers); if (serverEntries.length === 0) { return

No MCP servers configured

; } return (
{serverEntries.map(([serverName, serverConfig]) => { const isAvailable = serverStatus[serverName]; const statusKnown = serverName in serverStatus; const errorMessage = serverErrors[serverName]; const serverToolsData = serverTools[serverName]; const isExpanded = expandedServer === serverName; return (
toggleServerExpanded(serverName)} className="flex items-center gap-1 text-bolt-elements-textPrimary" >
{serverName}
{serverConfig.type === 'sse' ? ( SSE: {serverConfig.url} ) : ( {serverConfig.command} {serverConfig.args?.join(' ')} )}
{checkingServers ? ( ) : ( statusKnown && )}
{/* Display error message if server is unavailable */} {statusKnown && !isAvailable && errorMessage && (
Error: {errorMessage}
)} {/* Display tool schemas if server is expanded */} {isExpanded && statusKnown && isAvailable && serverToolsData && (
Available Tools:
{Object.keys(serverToolsData).length === 0 ? (
No tools available
) : (
{Object.entries(serverToolsData).map(([toolName, toolSchema]) => (
{formatToolSchema(toolName, toolSchema)}
))}
)}
)}
); })}
); }; return (
setIsDialogOpen(!isDialogOpen)} title="Configure MCP" className="transition-all"> {isLoading ? (
) : ( MCP )}
{isDialogOpen && (
MCP MCP Configuration
{renderServerList()}