/** * ServerConfigDrawer — unified setup wizard for builtin MCP servers * that require configuration (OAuth tokens, API keys, etc.). * * Three-phase flow: * 1. Setup Guide — prerequisite steps with external links * 2. Credential Form — dynamic fields from backend config schema * 3. Connect & Verify — save, restart, health check * * Works with any `requires_config` type: GOOGLE_OAUTH, SLACK_TOKEN, * GITHUB_TOKEN, NOTION_TOKEN, MS_GRAPH_TOKEN, MS_TEAMS_AUTH. */ import React, { useCallback, useEffect, useState } from 'react'; import { X, ArrowRight, ArrowLeft, Check, ExternalLink, Eye, EyeOff, Loader2, Key, Shield, AlertCircle, CheckCircle, Settings, ToggleLeft, ToggleRight, ChevronDown, RefreshCw, } from 'lucide-react'; import { getBuiltinSetupGuide } from './builtinSetupGuides'; // ── Step indicator ────────────────────────────────────────────────────── function StepIndicator({ current, total }) { return (
{Array.from({ length: total }, (_, i) => (
))} {current + 1} / {total}
); } // ── Toggle switch ──────────────────────────────────────────────────────── function ToggleSwitch({ checked, onChange }) { return (); } // ── Main drawer ───────────────────────────────────────────────────────── export function ServerConfigDrawer({ server, backendUrl, apiKey, onClose, onComplete }) { const [state, setState] = useState('loading'); const [guideStep, setGuideStep] = useState(0); const [config, setConfig] = useState(null); const [fieldValues, setFieldValues] = useState({}); const [showSecrets, setShowSecrets] = useState({}); const [error, setError] = useState(null); const [validationErrors, setValidationErrors] = useState([]); const guide = getBuiltinSetupGuide(server.id); const guideSteps = guide?.prerequisiteSteps ?? []; const headers = { 'Content-Type': 'application/json' }; if (apiKey) headers['x-api-key'] = apiKey; // ── Load config schema from backend ──────────────────────────────── const loadConfig = useCallback(async () => { setState('loading'); try { const res = await fetch(`${backendUrl}/v1/agentic/servers/${encodeURIComponent(server.id)}/config`, { headers: apiKey ? { 'x-api-key': apiKey } : {} }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()); setConfig(data); // Initialize field values from current config const initial = {}; for (const f of data.fields) { // Don't pre-fill masked secrets initial[f.key] = f.type === 'secret' && f.value?.includes('••') ? '' : (f.value || f.default || ''); } setFieldValues(initial); // If already configured, skip guide and go to credentials if (data.configured && guideSteps.length === 0) { setState('credentials'); } else { setState(guideSteps.length > 0 ? 'guide' : 'credentials'); } } catch (e) { setError(e?.message || 'Failed to load config'); setState('error'); } }, [backendUrl, apiKey, server.id, guideSteps.length]); useEffect(() => { void loadConfig(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ── Field value change ───────────────────────────────────────────── const setField = (key, value) => { setFieldValues((prev) => ({ ...prev, [key]: value })); setValidationErrors([]); }; // ── Check if a conditional field should be visible ───────────────── const isFieldVisible = (field) => { if (!field.condition) return true; const [condKey, condVal] = field.condition.split('='); return fieldValues[condKey] === condVal; }; // ── Validate ─────────────────────────────────────────────────────── const validateLocally = () => { if (!config) return false; const errors = []; for (const f of config.fields) { if (!isFieldVisible(f)) continue; if (f.required && !fieldValues[f.key]?.trim()) { errors.push(`${f.label} is required`); } } setValidationErrors(errors); return errors.length === 0; }; // ── Save & restart ───────────────────────────────────────────────── const handleSave = async () => { if (!validateLocally()) return; setState('saving'); setError(null); try { const res = await fetch(`${backendUrl}/v1/agentic/servers/${encodeURIComponent(server.id)}/config`, { method: 'POST', headers, body: JSON.stringify({ fields: fieldValues }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.detail || `HTTP ${res.status}`); } setState('success'); } catch (e) { setError(e?.message || 'Save failed'); setState('error'); } }; // ── Navigation ───────────────────────────────────────────────────── const handleGuideNext = () => { if (guideStep < guideSteps.length - 1) { setGuideStep(guideStep + 1); } else { setState('credentials'); } }; const handleGuideBack = () => { if (guideStep > 0) { setGuideStep(guideStep - 1); } }; const handleBackToCredentials = () => { setState('credentials'); setError(null); setValidationErrors([]); }; // ── Total steps for progress indicator ───────────────────────────── const totalSteps = guideSteps.length + 1; // guide steps + credential step const currentStep = state === 'guide' ? guideStep : guideSteps.length; // ── Render: loading ──────────────────────────────────────────────── const renderLoading = () => (

Loading configuration...

); // ── Render: guide step ───────────────────────────────────────────── const renderGuideStep = () => { const step = guideSteps[guideStep]; if (!step) return null; return (
{/* Current step card */}
{guideStep + 1}

{step.title}

{step.description}

{step.link && ( {step.link.label} )}
{/* Steps overview */}
{guideSteps.map((s, i) => (
{i < guideStep ? : i + 1}
{s.title}
))} {/* Show credential step in overview */}
{guideSteps.length + 1}
Enter credentials
); }; // ── Render: credential form ──────────────────────────────────────── const renderCredentials = () => { if (!config) return null; const visibleFields = config.fields.filter(isFieldVisible); return (
{/* Header info */} {guide && (

Enter the credentials from the previous steps. Your credentials are stored locally in the server's configuration file and are never sent to external services.

)} {/* Validation errors */} {validationErrors.length > 0 && (
{validationErrors.map((err, i) => (
{err}
))}
)} {/* Dynamic fields */} {visibleFields.map((field) => (
{field.type === 'secret' ? () : field.type === 'toggle' ? () : ()}
{field.type === 'toggle' ? ( setField(field.key, v ? 'true' : 'false')}/>) : field.type === 'select' ? (
) : (
setField(field.key, e.target.value)} placeholder={field.placeholder || ''} className="w-full bg-black/30 border border-white/10 rounded-xl px-4 py-3 pr-10 text-sm text-white placeholder-white/25 focus:outline-none focus:border-cyan-500/50 transition-colors font-mono"/> {field.type === 'secret' && ()}
)} {field.hint && (

{field.hint}

)}
))}
); }; // ── Render: saving ───────────────────────────────────────────────── const renderSaving = () => (

Configuring {server.label}

Saving credentials and restarting server...

); // ── Render: success ──────────────────────────────────────────────── const renderSuccess = () => (

{server.label} configured!

Credentials saved. {server.installed ? 'The server has been restarted with the new configuration.' : 'Click "Install" to start the server.'}

); // ── Render: error ────────────────────────────────────────────────── const renderError = () => (

Configuration Failed

{error}

); // ── Main render ──────────────────────────────────────────────────── const showGuide = state === 'guide'; const showCredentials = state === 'credentials'; const showFooter = showGuide || showCredentials; return (
{/* Backdrop */}
{/* Panel */}
e.stopPropagation()}> {/* Header */}

{guide?.title || `Configure ${server.label}`}

{guide?.subtitle && (

{guide.subtitle}

)} {!guide && config?.requires_config && (

Requires: {config.requires_config}

)}
{/* Progress indicator */} {showFooter && totalSteps > 1 && ()}
{/* Body */}
{state === 'loading' && renderLoading()} {showGuide && renderGuideStep()} {showCredentials && renderCredentials()} {state === 'saving' && renderSaving()} {state === 'success' && renderSuccess()} {state === 'error' && renderError()}
{/* Footer navigation */} {showFooter && (
{showGuide ? () : ()}
)}
); }