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"}
)
}