From ea9bf513a36266e104e520b4869afea176788864 Mon Sep 17 00:00:00 2001 From: MocA-Love <64681295+MocA-Love@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:28:49 +0900 Subject: [PATCH] fix(desktop): resolve actual desktop-mcp bin path for setup snippet desktop-mcp is a workspace-private TS bin with no global install, so `claude mcp add superset-browser -s local -- desktop-mcp` registered a command the shell can't resolve (Status: failed). browserAutomation.getMcpStatus now resolves the bin at runtime (`bun /packages/desktop-mcp/src/bin.ts` in dev; falls back to the bare name in packaged builds) and returns it as `serverCommand`. The snippet generator builds a Claude CLI / TOML block from those exact argv tokens so copy-pasting the instructions actually starts the server. --- .../trpc/routers/browser-automation/index.ts | 34 ++++++++++++++++-- .../SessionConnectModal.tsx | 12 +++++-- .../src/renderer/stores/browser-automation.ts | 36 ++++++++++++++----- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/browser-automation/index.ts b/apps/desktop/src/lib/trpc/routers/browser-automation/index.ts index 864eebc95d..16845ec1b4 100644 --- a/apps/desktop/src/lib/trpc/routers/browser-automation/index.ts +++ b/apps/desktop/src/lib/trpc/routers/browser-automation/index.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "node:events"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { browserAutomationBindings, projects, @@ -11,6 +11,7 @@ import { } from "@superset/local-db"; import { observable } from "@trpc/server/observable"; import { and, eq, ne } from "drizzle-orm"; +import { app } from "electron"; import { localDb } from "main/lib/local-db"; import { getProcessName, getProcessTree } from "main/lib/terminal/port-scanner"; import { getTerminalHostClient } from "main/lib/terminal-host/client"; @@ -288,6 +289,33 @@ const CLAUDE_SETTINGS_JSON_PATH = join(homedir(), ".claude", "settings.json"); const CLAUDE_CONFIG_PATHS = [CLAUDE_USER_JSON_PATH, CLAUDE_SETTINGS_JSON_PATH]; const CODEX_CONFIG_PATH = join(homedir(), ".codex", "config.toml"); +/** + * Resolve the absolute command Claude / Codex should spawn to start the + * superset-browser MCP server. `desktop-mcp` is a workspace-private bin + * (TypeScript, no compiled output), so it is not on PATH. We point + * users at `bun /packages/desktop-mcp/src/bin.ts` instead — bun + * can execute TypeScript directly. + * + * In packaged production builds the source tree isn't available, so we + * fall back to the bare `desktop-mcp` name and let the user pick the + * command themselves. (Browser automation is dev-only today.) + */ +function resolveDesktopMcpCommand(): { + command: string; + args: string[]; + available: boolean; +} { + if (!app.isPackaged) { + // `app.getAppPath()` is the `apps/desktop` directory in dev. + const repoRoot = resolve(app.getAppPath(), "../.."); + const binPath = join(repoRoot, "packages/desktop-mcp/src/bin.ts"); + if (existsSync(binPath)) { + return { command: "bun", args: [binPath], available: true }; + } + } + return { command: "desktop-mcp", args: [], available: false }; +} + export interface TerminalAgentSession { paneId: string; workspaceId: string; @@ -374,12 +402,14 @@ export const createBrowserAutomationRouter = () => { claudeReadyByWorkspaceId[workspaceId] = localScope || projectScope; } const codexReady = detectCodexMcp(CODEX_CONFIG_PATH); + const serverCommand = resolveDesktopMcpCommand(); return { claudeHomeReady, claudeReadyByWorkspaceId, codexReady, claudeConfigPath: CLAUDE_USER_JSON_PATH, codexConfigPath: CODEX_CONFIG_PATH, + serverCommand, }; }), diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx index d721db9e87..077a15fbb7 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/SessionConnectModal/SessionConnectModal.tsx @@ -16,6 +16,7 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { type AutomationSession, getSnippetForSession, + type ServerCommand, useBrowserAutomationStore, } from "renderer/stores/browser-automation"; import { useTabsStore } from "renderer/stores/tabs/store"; @@ -143,10 +144,14 @@ export function SessionConnectModal({ } }; + const serverCommand = mcpStatus?.serverCommand; + const handleCopySnippet = async () => { if (!session) return; try { - await navigator.clipboard.writeText(getSnippetForSession(session)); + await navigator.clipboard.writeText( + getSnippetForSession(session, serverCommand), + ); toast.success("Configuration snippet copied"); } catch { toast.error("Failed to copy snippet"); @@ -244,6 +249,7 @@ export function SessionConnectModal({ ? (mcpStatus?.codexConfigPath ?? null) : (mcpStatus?.claudeConfigPath ?? null) } + serverCommand={serverCommand} onCopy={handleCopySnippet} /> ) @@ -448,13 +454,15 @@ function DetailItem({ function SetupPanel({ session, mcpConfigPath, + serverCommand, onCopy, }: { session: AutomationSession; mcpConfigPath: string | null; + serverCommand?: ServerCommand; onCopy: () => void; }) { - const snippet = getSnippetForSession(session); + const snippet = getSnippetForSession(session, serverCommand); return (
diff --git a/apps/desktop/src/renderer/stores/browser-automation.ts b/apps/desktop/src/renderer/stores/browser-automation.ts index f62229cb6a..e48ee7d8c7 100644 --- a/apps/desktop/src/renderer/stores/browser-automation.ts +++ b/apps/desktop/src/renderer/stores/browser-automation.ts @@ -64,18 +64,36 @@ export const useBrowserAutomationStore = create( }), ); -export function getSnippetForSession(session: AutomationSession): string { +export interface ServerCommand { + command: string; + args: string[]; + available: boolean; +} + +function tomlString(value: string): string { + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} + +function shellQuote(value: string): string { + return /^[\w@./:+-]+$/.test(value) + ? value + : `'${value.replace(/'/g, "'\\''")}'`; +} + +export function getSnippetForSession( + session: AutomationSession, + server?: ServerCommand, +): string { + const cmd = server?.command ?? "desktop-mcp"; + const args = server?.args ?? []; if (session.provider === "Codex") { - // TOML is section-based, so appending this block to an existing - // config.toml is safe. The bin comes from packages/desktop-mcp. + const argsToml = args.map(tomlString).join(", "); return `[mcp_servers.superset-browser] -command = "desktop-mcp" -args = []`; +command = ${tomlString(cmd)} +args = [${argsToml}]`; } - // For Claude Code we recommend `claude mcp add` so the user's - // ~/.claude.json is updated in-place instead of manually merging a - // standalone JSON document (which would produce invalid JSON). - return `claude mcp add superset-browser -s local -- desktop-mcp`; + const parts = [cmd, ...args].map(shellQuote).join(" "); + return `claude mcp add superset-browser -s local -- ${parts}`; } function formatRelativeTime(ts: number | null | undefined): string {