Add MCP toggle control

This commit is contained in:
Shantur Rathore
2025-12-14 13:40:32 +00:00
parent 5e8b3fd5c9
commit 7591e5c1c9

View File

@@ -1,4 +1,5 @@
import { Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js" import { Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js"
import Switch from "@suid/material/Switch"
import type { Instance, RawMcpStatus } from "../types/instance" import type { Instance, RawMcpStatus } from "../types/instance"
import { fetchLspStatus, updateInstance } from "../stores/instances" import { fetchLspStatus, updateInstance } from "../stores/instances"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
@@ -49,6 +50,7 @@ const pendingMetadataRequests = new Set<string>()
const InstanceInfo: Component<InstanceInfoProps> = (props) => { const InstanceInfo: Component<InstanceInfoProps> = (props) => {
const [isLoadingMetadata, setIsLoadingMetadata] = createSignal(true) const [isLoadingMetadata, setIsLoadingMetadata] = createSignal(true)
const [pendingMcpActions, setPendingMcpActions] = createSignal<Record<string, "connect" | "disconnect">>({})
const metadata = () => props.instance.metadata const metadata = () => props.instance.metadata
const binaryVersion = () => props.instance.binaryVersion || metadata()?.version const binaryVersion = () => props.instance.binaryVersion || metadata()?.version
@@ -58,6 +60,63 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
} }
const lspServers = () => metadata()?.lspStatus ?? [] const lspServers = () => metadata()?.lspStatus ?? []
const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => {
setPendingMcpActions((prev) => {
const next = { ...prev }
if (action) {
next[name] = action
} else {
delete next[name]
}
return next
})
}
const refreshMcpStatus = async () => {
const client = props.instance.client
if (!client?.mcp?.status) {
return
}
try {
const result = await client.mcp.status()
const status = result.data as RawMcpStatus | undefined
if (!status) return
updateInstance(props.instance.id, {
metadata: {
...(props.instance.metadata ?? {}),
mcpStatus: status,
},
})
} catch (error) {
log.error("Failed to refresh MCP status", error)
}
}
const toggleMcpServer = async (serverName: string, shouldEnable: boolean) => {
const client = props.instance.client
if (!client?.mcp) {
return
}
const action: "connect" | "disconnect" = shouldEnable ? "connect" : "disconnect"
setPendingMcpAction(serverName, action)
try {
if (shouldEnable) {
await client.mcp.connect({ path: { name: serverName } })
} else {
await client.mcp.disconnect({ path: { name: serverName } })
}
await refreshMcpStatus()
} catch (error) {
log.error("Failed to toggle MCP server", { serverName, action, error })
} finally {
setPendingMcpAction(serverName)
}
}
createEffect(() => { createEffect(() => {
const instance = props.instance const instance = props.instance
const instanceId = instance.id const instanceId = instance.id
@@ -257,40 +316,70 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<For each={mcpServers()}> <For each={mcpServers()}>
{(server) => ( {(server) => {
<div class="px-2 py-1.5 rounded border bg-surface-secondary border-base"> const pendingAction = pendingMcpActions()[server.name]
<div class="flex items-center justify-between gap-2"> const isPending = Boolean(pendingAction)
<span class="text-xs text-primary font-medium truncate">{server.name}</span> const isRunning = server.status === "running"
<div class="flex items-center gap-1.5 flex-shrink-0 text-xs text-secondary"> const switchDisabled = isPending || !props.instance.client
<div
class={`status-dot ${ const statusDotClass = () => {
server.status === "running" if (isPending) {
? "ready animate-pulse" return "status-dot animate-pulse"
: server.status === "error" }
? "error" if (server.status === "running") {
: "stopped" return "status-dot ready animate-pulse"
}`} }
/> if (server.status === "error") {
<span> return "status-dot error"
{ }
server.status === "running" return "status-dot stopped"
? "Connected" }
: server.status === "error"
? "Error" const statusDotStyle = () => (isPending ? { background: "var(--status-warning)" } : undefined)
: "Disabled"
} return (
</span> <div class="px-2 py-1.5 rounded border bg-surface-secondary border-base">
</div> <div class="flex items-center justify-between gap-2">
</div> <span class="text-xs text-primary font-medium truncate">{server.name}</span>
<Show when={server.error}> <div class="flex items-center gap-3 flex-shrink-0">
{(error) => ( <div class="flex items-center gap-1.5 text-xs text-secondary">
<div class="text-[11px] mt-1 break-words" style={{ color: "var(--status-error)" }}> <div class={statusDotClass()} style={statusDotStyle()} />
{error()} </div>
<div class="flex items-center gap-1.5">
<Switch
checked={isRunning}
disabled={switchDisabled}
color="success"
size="small"
inputProps={{ "aria-label": `Toggle ${server.name} MCP server` }}
onChange={(_, checked) => {
if (switchDisabled) return
void toggleMcpServer(server.name, Boolean(checked))
}}
/>
<Show when={isPending}>
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</Show>
</div>
</div> </div>
)} </div>
</Show> <Show when={server.error}>
</div> {(error) => (
)} <div class="text-[11px] mt-1 break-words" style={{ color: "var(--status-error)" }}>
{error()}
</div>
)}
</Show>
</div>
)
}}
</For> </For>
</div> </div>
</div> </div>