import { ChevronDown, ChevronRight, FolderOpen, Loader2, Plus, Trash2 } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import type { SandboxAgent } from "sandbox-agent"; import { formatJson } from "../../utils/format"; type McpEntry = { name: string; config: Record; }; const MCP_DIRECTORY_STORAGE_KEY = "sandbox-agent-inspector-mcp-directory"; const McpTab = ({ getClient, }: { getClient: () => SandboxAgent; }) => { const [directory, setDirectory] = useState(() => { if (typeof window === "undefined") return "/"; try { return window.localStorage.getItem(MCP_DIRECTORY_STORAGE_KEY) ?? "/"; } catch { return "/"; } }); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [collapsedServers, setCollapsedServers] = useState>({}); // Add/edit form state const [editing, setEditing] = useState(false); const [editName, setEditName] = useState(""); const [editJson, setEditJson] = useState(""); const [editError, setEditError] = useState(null); const [saving, setSaving] = useState(false); const loadAll = useCallback(async (dir: string) => { setLoading(true); setError(null); try { const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/mcp.json`; const bytes = await getClient().readFsFile({ path: configPath }); const text = new TextDecoder().decode(bytes); if (!text.trim()) { setEntries([]); return; } const map = JSON.parse(text) as Record>; setEntries( Object.entries(map).map(([name, config]) => ({ name, config })), ); } catch { // File doesn't exist yet or is empty — that's fine setEntries([]); } finally { setLoading(false); } }, [getClient]); useEffect(() => { loadAll(directory); }, [directory, loadAll]); useEffect(() => { try { window.localStorage.setItem(MCP_DIRECTORY_STORAGE_KEY, directory); } catch { // Ignore storage failures. } }, [directory]); const startAdd = () => { setEditing(true); setEditName(""); setEditJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}'); setEditError(null); }; const cancelEdit = () => { setEditing(false); setEditName(""); setEditJson(""); setEditError(null); }; const save = async () => { const name = editName.trim(); if (!name) { setEditError("Name is required"); return; } let parsed: Record; try { parsed = JSON.parse(editJson.trim()); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { setEditError("Must be a JSON object"); return; } } catch { setEditError("Invalid JSON"); return; } setSaving(true); setEditError(null); try { await getClient().setMcpConfig( { directory, mcpName: name }, parsed as Parameters[1], ); cancelEdit(); await loadAll(directory); } catch (err) { setEditError(err instanceof Error ? err.message : "Failed to save"); } finally { setSaving(false); } }; const remove = async (name: string) => { try { await getClient().deleteMcpConfig({ directory, mcpName: name }); await loadAll(directory); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete"); } }; return ( <>
MCP Server Configuration
{!editing && ( )}
setDirectory(e.target.value)} placeholder="/" style={{ flex: 1, fontSize: 11 }} />
{error &&
{error}
} {loading &&
Loading...
} {editing && (
{editName ? `Edit: ${editName}` : "Add MCP Server"}
{ setEditName(e.target.value); setEditError(null); }} placeholder="server-name" style={{ marginBottom: 8, width: "100%", boxSizing: "border-box" }} />