Skip to content

Desktop MCP toggle can fail or block on resource catalog refresh #48386

Open

Description

@mapix-etnc

Description

After a successful MCP toggle connect, the Desktop UI synchronously refetches the global MCP resource catalog across all connected servers. A slow or unsupported resources/list call therefore blocks the toggle or surfaces a false failure. The impact rises with more connected MCP servers, but one problematic server is sufficient.

Expected behavior

The MCP toggle should complete after the MCP connection status is refreshed. Resource catalog discovery should run independently and best-effort; a slow, unsupported, or rejected resources/list must not block or fail the toggle.

Steps to reproduce

  1. Configure at least one MCP server whose resources/list response is slow or unsupported.
  2. Open the Desktop app and enable that MCP server.
  3. Observe that the toggle waits for resource discovery or reports a failure despite the connection succeeding.

Validation

The cause was reproduced and validated locally with a focused unit-test scenario in which resource discovery remains pending after connect: the toggle resolves without waiting for it. A rejection case also verifies that the background resource refresh is safely handled without an unhandled rejection or toggle failure.

Additional context

This is a Desktop MCP toggle flow issue; no credentials, private endpoints, or local paths are required to reproduce the behavior.

Activity

  1. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the clear report. I could confirm this on the latest dev (1e7ba46f8).

    I couldn't run the Desktop app here, so I ran the toggle logic from useMcpToggle (packages/app/src/providers/connect/mcp.ts) in a unit test, with the MCP connect and the status refresh succeeding:

    • When the resource catalog refresh never finishes, the toggle stays pending even though the server is already connected.
    • When the resource catalog refresh fails, the toggle fails with that error. In the app that shows up as a "request failed" toast, even though the connection worked.

    This happens because the toggle waits for the resource catalog refresh before it checks the connection status. One caveat: servers that don't support resources are skipped, and slow listings are cut off by a timeout. So in practice the problem shows up with slow servers that do support resources, or when the refresh fails.

    The script below reproduces both cases. Copy it to packages/app/src/providers/connect/ in a dev checkout and run it with bun test from packages/app.

    Reproduction script (mcp-repro.test.ts)
    // Reproduction for anomalyco/opencode#48386: the MCP toggle awaits the resource catalog refresh.
    // Place at packages/app/src/providers/connect/mcp-repro.test.ts in a dev checkout, then: cd packages/app && bun test src/providers/connect/mcp-repro.test.ts
    import { describe, expect, mock, test } from "bun:test"
    
    let resourceSync: () => Promise<unknown> = async () => {}
    const toasts: unknown[] = []
    const calls: string[] = []
    
    mock.module("@tanstack/solid-query", () => ({ useMutation: (fn: () => unknown) => fn() }))
    mock.module("@/runtime/i18n/language", () => ({ useLanguage: () => ({ t: (k: string) => k }) }))
    mock.module("@/runtime/platform/platform", () => ({ usePlatform: () => ({ openExternal() {} }) }))
    mock.module("@/shell/notifications/toast", () => ({ showToast: (t: unknown) => toasts.push(t) }))
    mock.module("@/runtime/server/client", () => ({
      useServerSDK: () => ({
        api: {
          mcp: {
            list: async () => ({ data: [{ name: "slow", status: { status: "disabled" } }] }),
            connect: async () => calls.push("connect"),
            disconnect: async () => calls.push("disconnect"),
          },
        },
      }),
    }))
    mock.module("@/runtime/server/current", () => ({
      useData: () => ({
        location: {
          default: () => ({ directory: "/proj" }),
          mcp: {
            server: {
              invalidate() {},
              sync: async () => calls.push("server.sync"),
              list: () => [{ name: "slow", status: { status: "connected" } }],
            },
            resource: { invalidate() {}, sync: () => resourceSync() },
          },
        },
      }),
    }))
    
    const { useMcpToggle } = await import("./mcp")
    
    describe("#48386 MCP toggle vs resource catalog refresh", () => {
      test("toggle should not wait for a pending resource refresh", async () => {
        resourceSync = () => new Promise(() => {}) // resources/list never answers
        const opts = useMcpToggle() as any
        const result = await Promise.race([
          opts.mutationFn("slow").then(() => "resolved"),
          new Promise((r) => setTimeout(() => r("still pending after 500ms"), 500)),
        ])
        console.log("pending resource refresh ->", result, calls)
        expect(result).toBe("resolved")
      })
    
      test("toggle should not fail when the resource refresh rejects", async () => {
        resourceSync = async () => {
          throw new Error("resources/list failed")
        }
        const opts = useMcpToggle() as any
        const result = await opts.mutationFn("slow").then(
          () => "resolved",
          (e: Error) => `rejected: ${e.message}`,
        )
        console.log("rejected resource refresh ->", result)
        expect(result).toBe("resolved")
      })
    })

    Run it: bun mcp-repro.test.ts

  2. mapix-etnc commented on Nov 2, 2026

    @mapix-etnc
    Author
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions