Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions apps/desktop/src/main/lib/browser-mcp-bridge/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -75,21 +66,6 @@ async function resolvePaneFromRequest(
return { paneId, sessionId: resolved.sessionId };
}

async function _readJson<T>(req: IncomingMessage): Promise<T> {
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");
Expand Down Expand Up @@ -206,9 +182,6 @@ export async function startBrowserMcpBridge(): Promise<BridgeHandle> {

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),
Expand Down
73 changes: 73 additions & 0 deletions packages/superset-browser-mcp/README.md
Original file line number Diff line number Diff line change
@@ -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
`<app>/Contents/Resources/resources/superset-browser-mcp/superset-browser-mcp`;
`claude mcp add superset-browser -s user -- <that path>` registers it into
Claude Code, `codex mcp add superset-browser -- <that path>` 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/<token>/devtools/page/<targetId>
Chromium --remote-debugging-port (random port)
│ filter: only the bound pane's target is visible
External CDP MCP (chrome-devtools-mcp / browser-use / …)
```
Comment on lines +31 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

コードブロックに言語指定を追加してください

アーキテクチャ図のフェンスコードブロックに言語指定がありません。プレーンテキスト図の場合は ```text を使用することで、markdownlint の警告を解消できます。

📝 修正案
-```
+```text
 Claude / Codex session
         │  (stdio tool call: get_cdp_endpoint)
         ▼
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/superset-browser-mcp/README.md` around lines 31 - 48, The fenced
code block in the README architecture diagram lacks a language tag which
triggers markdownlint warnings; update the opening fence from ``` to ```text
(and keep the closing ```), e.g. change the block starting with "Claude / Codex
session" to begin with "```text" so the diagram is explicitly marked as plain
text and the lint warning is resolved.


## 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.
13 changes: 7 additions & 6 deletions packages/superset-browser-mcp/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) を有効化
Expand Down Expand Up @@ -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`
Expand Down
Loading