Kaynağa Gözat

fix(app): refresh directory MCP status (#31194)

Luke Parker 2 ay önce
ebeveyn
işleme
a29deb1ee9

+ 2 - 22
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)

+ 2 - 36
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<boolean> }) {
     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

+ 3 - 0
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

+ 34 - 0
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"])
+  })
+})

+ 18 - 0
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<void>
+  disconnect: () => Promise<void>
+  authenticate: () => Promise<void>
+  refresh: () => Promise<void>
+}) {
+  await {
+    connected: input.disconnect,
+    needs_auth: input.authenticate,
+    disabled: input.connect,
+    failed: input.connect,
+    needs_client_registration: input.connect,
+  }[input.status]()
+  await input.refresh()
+}

+ 19 - 0
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),
+      }),
+  }))
+}

+ 23 - 0
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))
+          },
+        })
+      },
+    },
   }
 }