diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 49d8ee677285..176817bfd18e 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -1,13 +1,10 @@ -import { useMutation, useQueryClient } from "@tanstack/solid-query" import { Component, createMemo, Show } from "solid-js" import { useSync } from "@/context/sync" -import { useSDK } from "@/context/sdk" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" import { useLanguage } from "@/context/language" -import { useQueryOptions } from "@/context/server-sync" -import { pathKey } from "@/utils/path-key" +import { useMcpToggle } from "@/context/mcp" const statusLabels = { connected: "mcp.status.connected", @@ -19,10 +16,7 @@ const statusLabels = { export const DialogSelectMcp: Component = () => { const sync = useSync() - const sdk = useSDK() const language = useLanguage() - const queryClient = useQueryClient() - const queryOptions = useQueryOptions() const items = createMemo(() => Object.entries(sync.data.mcp ?? {}) @@ -30,21 +24,7 @@ export const DialogSelectMcp: Component = () => { .sort((a, b) => a.name.localeCompare(b.name)), ) - const toggle = useMutation(() => ({ - mutationFn: async (name: string) => { - const status = sync.data.mcp[name] - if (status?.status === "connected") { - await sdk.client.mcp.disconnect({ name }) - return - } - if (status?.status === "needs_auth") { - await sdk.client.mcp.auth.authenticate({ name }) - return - } - await sdk.client.mcp.connect({ name }) - }, - onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))), - })) + const toggle = useMcpToggle() const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) const totalCount = createMemo(() => items().length) diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 4857a241f5ee..6bc15857a79f 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -3,7 +3,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Icon } from "@opencode-ai/ui/icon" import { Switch } from "@opencode-ai/ui/switch" import { Tabs } from "@opencode-ai/ui/tabs" -import { useMutation, useQueryClient } from "@tanstack/solid-query" import { showToast } from "@/utils/toast" import { useNavigate } from "@solidjs/router" import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js" @@ -11,14 +10,12 @@ import { createStore } from "solid-js/store" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" -import { useSDK } from "@/context/sdk" import { ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { type ServerHealth } from "@/utils/server-health" -import { useQueryOptions } from "@/context/server-sync" -import { pathKey } from "@/utils/path-key" import { useGlobal } from "@/context/global" import { useSettings } from "@/context/settings" +import { useMcpToggle } from "@/context/mcp" const pluginEmptyMessage = (value: string, file: string): JSXElement => { const parts = value.split(file) @@ -99,37 +96,6 @@ const useDefaultServerKey = ( } } -const useMcpToggleMutation = () => { - const sync = useSync() - const sdk = useSDK() - const language = useLanguage() - const queryClient = useQueryClient() - const queryOptions = useQueryOptions() - - return useMutation(() => ({ - mutationFn: async (name: string) => { - const status = sync.data.mcp[name] - if (status?.status === "connected") { - await sdk.client.mcp.disconnect({ name }) - return - } - if (status?.status === "needs_auth") { - await sdk.client.mcp.auth.authenticate({ name }) - return - } - await sdk.client.mcp.connect({ name }) - }, - onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))), - onError: (err) => { - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), - }) - }, - })) -} - type ServerStatusState = { servers: () => ServerStatusItem[] defaultKey: () => ServerConnection.Key | undefined @@ -311,7 +277,7 @@ export function StatusPopoverBody(props: { shown: Accessor }) { dialogRun += 1 }) const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health)) - const toggleMcp = useMcpToggleMutation() + const toggleMcp = useMcpToggle() const defaultServer = useDefaultServerKey(platform.getDefaultServer) const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts index e8221fd0b088..52fa79fbc4b4 100644 --- a/packages/app/src/context/directory-sync.ts +++ b/packages/app/src/context/directory-sync.ts @@ -609,6 +609,9 @@ export const createDirSyncContext = ( ) }, }, + mcp: { + toggle: (name: string) => serverSync.mcp.toggle(directory, name), + }, absolute, get directory() { return current()[0].path.directory diff --git a/packages/app/src/context/global-sync/mcp.test.ts b/packages/app/src/context/global-sync/mcp.test.ts new file mode 100644 index 000000000000..a292d23df94b --- /dev/null +++ b/packages/app/src/context/global-sync/mcp.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "bun:test" +import { toggleMcp } from "./mcp" + +describe("toggleMcp", () => { + test("runs the status action before refreshing the owning query", async () => { + const calls: string[] = [] + const input = (status: "connected" | "needs_auth" | "disabled") => ({ + status, + connect: async () => { + calls.push("connect") + }, + disconnect: async () => { + calls.push("disconnect") + }, + authenticate: async () => { + calls.push("authenticate") + }, + refresh: async () => { + calls.push("refresh") + }, + }) + + await toggleMcp(input("connected")) + expect(calls).toEqual(["disconnect", "refresh"]) + + calls.length = 0 + await toggleMcp(input("needs_auth")) + expect(calls).toEqual(["authenticate", "refresh"]) + + calls.length = 0 + await toggleMcp(input("disabled")) + expect(calls).toEqual(["connect", "refresh"]) + }) +}) diff --git a/packages/app/src/context/global-sync/mcp.ts b/packages/app/src/context/global-sync/mcp.ts new file mode 100644 index 000000000000..2eeb297b955a --- /dev/null +++ b/packages/app/src/context/global-sync/mcp.ts @@ -0,0 +1,18 @@ +import type { McpStatus } from "@opencode-ai/sdk/v2/client" + +export async function toggleMcp(input: { + status: McpStatus["status"] + connect: () => Promise + disconnect: () => Promise + authenticate: () => Promise + refresh: () => Promise +}) { + await { + connected: input.disconnect, + needs_auth: input.authenticate, + disabled: input.connect, + failed: input.connect, + needs_client_registration: input.connect, + }[input.status]() + await input.refresh() +} diff --git a/packages/app/src/context/mcp.ts b/packages/app/src/context/mcp.ts new file mode 100644 index 000000000000..5c0b7588346a --- /dev/null +++ b/packages/app/src/context/mcp.ts @@ -0,0 +1,19 @@ +import { useMutation } from "@tanstack/solid-query" +import { useLanguage } from "@/context/language" +import { useSync } from "@/context/sync" +import { showToast } from "@/utils/toast" + +export function useMcpToggle() { + const sync = useSync() + const language = useLanguage() + + return useMutation(() => ({ + mutationFn: sync.mcp.toggle, + onError: (error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: error instanceof Error ? error.message : String(error), + }), + })) +} diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx index c222d18753ff..105d40eecab0 100644 --- a/packages/app/src/context/server-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -36,6 +36,7 @@ import { ServerConnection, useServer } from "./server" import { retry } from "@opencode-ai/core/util/retry" import type { ServerScope } from "@/utils/server-scope" import { persisted } from "@/utils/persist" +import { toggleMcp } from "./global-sync/mcp" type GlobalStore = { ready: boolean @@ -481,6 +482,28 @@ export function createServerSyncContextInner(_serverSDK?: ServerSDK) { todo: { set: setSessionTodo, }, + mcp: { + toggle: async (directory: string, name: string) => { + const key = directoryKey(directory) + const sdk = sdkFor(key) + const status = children.child(key, { bootstrap: false })[0].mcp[name].status + await toggleMcp({ + status, + connect: async () => { + await sdk.mcp.connect({ name }) + }, + disconnect: async () => { + await sdk.mcp.disconnect({ name }) + }, + authenticate: async () => { + await sdk.mcp.auth.authenticate({ name }) + }, + refresh: async () => { + await queryClient.refetchQueries(queryOptionsApi.mcp(key)) + }, + }) + }, + }, } }