HomePilot / frontend /src /ui /mcp /AddServerDrawer.tsx
HomePilot Deploy Bot
chore(hf): sync HomePilot to HF Space
23b413b
import React, { useState } from 'react'
import { X, Plus, RefreshCw } from 'lucide-react'
type Props = {
backendUrl: string
apiKey?: string
onClose: () => void
onRegistered: () => void
}
const TRANSPORTS = ['SSE', 'STREAMABLEHTTP', 'HTTP', 'STDIO'] as const
export function AddServerDrawer({ backendUrl, apiKey, onClose, onRegistered }: Props) {
const [name, setName] = useState('')
const [url, setUrl] = useState('')
const [transport, setTransport] = useState<string>('SSE')
const [description, setDescription] = useState('')
const [autoRefresh, setAutoRefresh] = useState(true)
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const canSubmit = name.trim() && url.trim() && !busy
const handleSubmit = async () => {
if (!canSubmit) return
setBusy(true)
setError(null)
try {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (apiKey) headers['x-api-key'] = apiKey
const res = await fetch(`${backendUrl}/v1/agentic/register/gateway`, {
method: 'POST',
headers,
body: JSON.stringify({
name: name.trim(),
url: url.trim(),
transport,
description: description.trim(),
auto_refresh: autoRefresh,
}),
})
const data = await res.json()
if (!res.ok || !data.ok) {
throw new Error(data.detail || `HTTP ${res.status}`)
}
setSuccess(true)
setTimeout(() => {
onRegistered()
onClose()
}, 1000)
} catch (e: any) {
setError(e?.message || 'Registration failed')
} finally {
setBusy(false)
}
}
return (
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div
className="relative w-full max-w-md bg-[#0b0b12] border-l border-white/10 h-full overflow-y-auto animate-slide-in-right"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="sticky top-0 bg-[#0b0b12]/95 backdrop-blur border-b border-white/10 px-6 py-4 flex items-center justify-between z-10">
<h2 className="text-base font-semibold text-white">Add MCP Server</h2>
<button
onClick={onClose}
className="p-2 text-white/50 hover:text-white hover:bg-white/10 rounded-xl transition-colors"
>
<X size={18} />
</button>
</div>
{/* Form */}
<div className="px-6 py-6 space-y-5">
{/* Name */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Server Name *
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. my-mcp-server"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors"
/>
</div>
{/* URL */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Endpoint URL *
</label>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="e.g. http://localhost:9101/rpc"
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors"
/>
</div>
{/* Transport */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Transport
</label>
<div className="flex gap-2">
{TRANSPORTS.map((t) => (
<button
key={t}
onClick={() => setTransport(t)}
className={`px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors ${
transport === t
? 'bg-purple-500/20 border-purple-500/50 text-purple-300'
: 'bg-white/5 border-white/10 text-white/40 hover:text-white/60'
}`}
>
{t}
</button>
))}
</div>
</div>
{/* Description */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description..."
rows={3}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors resize-none"
/>
</div>
{/* Auto-refresh toggle */}
<label className="flex items-center gap-3 cursor-pointer">
<div
onClick={() => setAutoRefresh(!autoRefresh)}
className={`w-10 h-5 rounded-full transition-colors relative ${
autoRefresh ? 'bg-purple-500' : 'bg-white/10'
}`}
>
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
autoRefresh ? 'left-5' : 'left-0.5'
}`} />
</div>
<span className="text-sm text-white/70">Auto-discover tools after registration</span>
</label>
{/* Error / Success */}
{error && (
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-xl px-4 py-3">
{error}
</div>
)}
{success && (
<div className="text-sm text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 rounded-xl px-4 py-3">
Server registered successfully!
</div>
)}
{/* Submit */}
<button
onClick={handleSubmit}
disabled={!canSubmit}
className="w-full flex items-center justify-center gap-2 bg-purple-500 hover:bg-purple-600 disabled:bg-white/10 disabled:text-white/30 px-4 py-3 rounded-xl text-sm font-semibold transition-all"
>
{busy ? (
<RefreshCw size={16} className="animate-spin" />
) : (
<Plus size={16} />
)}
{busy ? 'Registering...' : 'Register Gateway'}
</button>
</div>
</div>
<style>{`
@keyframes slide-in-right {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
.animate-slide-in-right {
animation: slide-in-right 0.2s ease-out;
}
`}</style>
</div>
)
}