import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js" import { useData } from "../context/data" import { useClient } from "../context/client" import { Keymap } from "../context/keymap" import { pipe, sortBy } from "remeda" import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useTheme } from "../context/theme" import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core" import type { McpServer } from "@opencode-ai/client" import { useClipboard } from "../context/clipboard" import { useToast } from "../ui/toast" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { useConfig } from "../config" import { getScrollAcceleration } from "../util/scroll" function statusError(status: McpServer["status"]) { if (status.status === "failed" || status.status === "needs_client_registration") return status.error return undefined } function Status(props: { enabled: boolean; loading: boolean }) { const theme = useTheme("elevated") if (props.loading) return ⋯ Loading if (props.enabled) { return ✓ Enabled } return ○ Disabled } export function DialogMcp() { const data = useData() const dialog = useDialog() const client = useClient() const toast = useToast() const theme = useTheme("elevated") const [focused, setFocused] = createSignal() const [detail, setDetail] = createSignal() const [loading, setLoading] = createSignal(null) const servers = createMemo(() => pipe( data.location.mcp.server.list() ?? [], sortBy((server) => server.name), ), ) createEffect(() => { if (focused()) return const first = servers()[0] if (first) setFocused(first.name) }) const options = createMemo(() => { const loadingMcp = loading() return servers().map((server) => ({ value: server.name, title: server.name, description: server.status.status, footer: , })) }) const focusedError = createMemo(() => { const name = focused() const server = servers().find((entry) => entry.name === name) return server ? statusError(server.status) : undefined }) const open = (name: string | undefined) => { const server = servers().find((entry) => entry.name === name) if (!server || !statusError(server.status)) return setDetail(server) } // Connected servers disconnect; everything else (disabled, failed, needs_auth) retries a // connection. The mcp.status.changed event refreshes the list, so no manual sync is needed. const toggle = (name: string) => { if (loading() !== null) return const server = servers().find((entry) => entry.name === name) if (!server || server.status.status === "pending") return setLoading(name) const current = data.location.default() const input = { server: name, location: { directory: current.directory, workspace: current.workspaceID } } const call = server.status.status === "connected" ? client.api.mcp.disconnect(input) : client.api.mcp.connect(input) void call.catch(toast.error).finally(() => setLoading(null)) } return ( setFocused(option.value as string)} onSelect={(option) => open(option.value as string)} actions={[ { title: "toggle", command: "dialog.mcp.toggle", onTrigger: (option) => { setFocused(option.value as string) toggle(option.value as string) }, }, ]} footer={ enter to view error } /> } > {(server) => ( { setDetail() dialog.setSize("medium") }} /> )} ) } function DialogMcpError(props: { server: McpServer; onBack: () => void }) { const dialog = useDialog() const clipboard = useClipboard() const toast = useToast() const theme = useTheme("elevated") const overlayTheme = useTheme("overlay") const dimensions = useTerminalDimensions() const config = useConfig().data const [copied, setCopied] = createSignal(false) const error = () => statusError(props.server.status) ?? "Unknown MCP connection error" const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5)) let scroll: ScrollBoxRenderable | undefined onMount(() => dialog.setSize("large")) const copy = () => { if (!clipboard.write) return void clipboard .write(error()) .then(() => setCopied(true)) .catch(toast.error) } Keymap.createLayer(() => ({ mode: "modal", commands: [{ bind: "escape", title: "Back to MCP servers", group: "Dialog", run: props.onBack }], })) useKeyboard((event) => { if (event.name === "c") return copy() if (event.name === "up") return scroll?.scrollBy(-1) if (event.name === "down") return scroll?.scrollBy(1) if (event.name === "pageup") return scroll?.scrollBy(-height()) if (event.name === "pagedown") return scroll?.scrollBy(height()) if (event.name === "home") return scroll?.scrollTo(0) if (event.name === "end" && scroll) return scroll.scrollTo(scroll.scrollHeight) }) return ( MCP server: {props.server.name} esc back ✗ Failed (scroll = element)} height={height()} scrollbarOptions={{ visible: false }} scrollAcceleration={getScrollAcceleration(config)} > {error()} ↑↓ scroll {copied() ? "✓ copied" : "c copy details"} ) }