diff --git a/apps/desktop/src/main/lib/browser-mcp-bridge/server.ts b/apps/desktop/src/main/lib/browser-mcp-bridge/server.ts index 56418bcdcaa..f16eab3cbd7 100644 --- a/apps/desktop/src/main/lib/browser-mcp-bridge/server.ts +++ b/apps/desktop/src/main/lib/browser-mcp-bridge/server.ts @@ -37,15 +37,6 @@ import { getBoundPaneForSession, resolvePpidToSession } from "./pane-resolver"; const RUNTIME_INFO_PATH = join(SUPERSET_HOME_DIR, "browser-mcp.json"); -const MAX_JSON_BODY_BYTES = 8 * 1024 * 1024; - -class PayloadTooLargeError extends Error { - readonly status = 413; - constructor() { - super(`request body exceeds ${MAX_JSON_BODY_BYTES} bytes`); - } -} - async function resolvePaneFromRequest( req: IncomingMessage, ): Promise< @@ -75,21 +66,6 @@ async function resolvePaneFromRequest( return { paneId, sessionId: resolved.sessionId }; } -async function _readJson(req: IncomingMessage): Promise { - const chunks: Buffer[] = []; - let total = 0; - for await (const chunk of req) { - const buf = chunk as Buffer; - total += buf.length; - if (total > MAX_JSON_BODY_BYTES) throw new PayloadTooLargeError(); - chunks.push(buf); - } - const raw = Buffer.concat(chunks).toString("utf8"); - return raw ? (JSON.parse(raw) as T) : ({} as T); -} -// Kept for follow-up endpoints that accept bodies; silences unused hint. -void _readJson; - function send(res: ServerResponse, status: number, body: unknown): void { res.statusCode = status; res.setHeader("content-type", "application/json"); @@ -206,9 +182,6 @@ export async function startBrowserMcpBridge(): Promise { return send(res, 404, { error: "not found" }); } catch (error) { - if (error instanceof PayloadTooLargeError) { - return send(res, 413, { error: error.message }); - } console.error("[browser-mcp-bridge]", error); return send(res, 500, { error: error instanceof Error ? error.message : String(error), diff --git a/packages/superset-browser-mcp/README.md b/packages/superset-browser-mcp/README.md new file mode 100644 index 00000000000..e9124791f9d --- /dev/null +++ b/packages/superset-browser-mcp/README.md @@ -0,0 +1,73 @@ +# @superset/superset-browser-mcp + +Small stdio MCP server that bridges an LLM session to a Superset desktop app +browser pane. Superset ships the compiled binary at +`/Contents/Resources/resources/superset-browser-mcp/superset-browser-mcp`; +`claude mcp add superset-browser -s user -- ` registers it into +Claude Code, `codex mcp add superset-browser -- ` into Codex. + +## What it does (and what it doesn't) + +Actual browser automation — click, navigate, screenshot, DOM inspection — is +delegated to mature external CDP (Chrome DevTools Protocol) MCPs: + +- [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) +- [`browser-use`](https://github.com/browser-use/browser-use) +- [`playwright-mcp`](https://github.com/microsoft/playwright-mcp) + +This MCP's only job is **binding routing**: give the LLM a URL that scopes +CDP down to the one Superset pane that the user attached to the session. + +Tools: + +- `get_cdp_endpoint` — returns `{ webSocketDebuggerUrl, httpBase, targetId, … }` + for the pane currently bound to this LLM session. Plug those into any + external CDP MCP and it only sees that pane. +- `get_connected_pane` — returns `{ bound, paneId, url, title, sessionId }` + as a sanity check before handing the endpoint to another tool. + +## Architecture + +``` +Claude / Codex session + │ (stdio tool call: get_cdp_endpoint) + ▼ +packages/superset-browser-mcp (this package) + │ HTTP over loopback, ~/.superset/browser-mcp.json + ▼ +apps/desktop main process + ├── session resolver (PPID → terminal pane → LLM session) + ├── binding store (sessionId ↔ paneId) + └── CDP filter proxy + │ ws(s)://…/cdp//devtools/page/ + ▼ +Chromium --remote-debugging-port (random port) + │ filter: only the bound pane's target is visible + ▼ +External CDP MCP (chrome-devtools-mcp / browser-use / …) +``` + +## Flow + +1. User opens a browser pane in Superset and hits **Connect**, binding it to + their running Claude / Codex terminal session. The binding is persisted in + Superset's local DB so it survives restarts. +2. Claude / Codex spawns this MCP. It talks to the Superset bridge over the + loopback port written to `~/.superset/browser-mcp.json` (workspace-scoped + via `SUPERSET_HOME_DIR`). +3. `get_cdp_endpoint` returns a per-session-token URL pointing at the filter + proxy. The LLM uses it to configure chrome-devtools-mcp / browser-use / + etc. from the same session. +4. The external CDP client sees exactly one page target — the bound pane — + via `/json/list`. Sibling panes and the workspace shell are invisible. +5. Re-binding in the UI re-routes the filtered endpoint to the new pane; the + next CDP connection picks up the swap automatically. + +## Dev notes + +Source lives at `src/bin.ts`; tools at `src/tools/index.ts`. The binary is +produced by `bun build --compile` and copied into the Electron app via +`extraResources` — see the desktop app's `electron-builder.ts`. + +For the full roadmap (including the PRs that shipped this stack) see +`plan.md` at the repo root. diff --git a/packages/superset-browser-mcp/src/tools/index.ts b/packages/superset-browser-mcp/src/tools/index.ts index 8c20b770c17..8bae76f1b4c 100644 --- a/packages/superset-browser-mcp/src/tools/index.ts +++ b/packages/superset-browser-mcp/src/tools/index.ts @@ -22,12 +22,13 @@ interface CdpEndpointResponse { } /** - * This MCP is intentionally kept minimal. The plan (see ./plan.md in - * the repo root) is to expose the bound pane as a filtered CDP endpoint - * so users can drive it with mature external browser MCPs - * (chrome-devtools-mcp, browser-use, playwright-mcp). The tools here - * are only the metadata shim LLMs need to verify the binding; the CDP - * endpoint itself ships in follow-up PRs. + * This MCP is intentionally kept minimal. Browser automation primitives + * (click / navigate / screenshot / DOM inspection) are delegated to + * mature external CDP-speaking MCPs — chrome-devtools-mcp, browser-use, + * playwright-mcp, etc. — connected to the bound pane via the filtered + * CDP endpoint that `get_cdp_endpoint` returns. The two tools here are + * the handoff: `get_cdp_endpoint` for the URL, `get_connected_pane` as + * a metadata sanity check. */ export function registerTools(server: McpServer, client: BridgeClient): void { server.registerTool( diff --git a/plan.md b/plan.md index e4e4c100efb..ea4e120180e 100644 --- a/plan.md +++ b/plan.md @@ -43,6 +43,9 @@ ## Phase B PR ロードマップ +> **Status (2026-04-21):** PR1 / PR2 / PR3 マージ済。PR4 (本 PR) で +> プラン上の last-mile クリーンアップを行い、Phase B 完了。 + ### PR1: CDP エンドポイント公開(pane → Chromium targetId 解決) - Superset 起動時に `--remote-debugging-port=0` (ランダム port) を有効化 @@ -98,7 +101,7 @@ - Connect 後の pane 情報画面 (ReadyPanel) にも「この pane の CDP endpoint」 リンクを常設し、別の LLM クライアントから直接叩けるように -### PR4: 古い自作 tools の整理 +### PR4: 古い自作 tools の整理 (本 PR) - PR1〜3 で代替が整い次第、以下の MCP tools を deprecated にするか削除: - `navigate` / `screenshot` / `evaluate_js` / `get_console_logs`