Skip to content

feat: Arc Browser MCP Server (hybrid AppleScript + CDP)#26

Merged
Joao208 merged 6 commits into
mainfrom
joao-barros-/-feat-arc-browser-mcp
Apr 4, 2026
Merged

feat: Arc Browser MCP Server (hybrid AppleScript + CDP)#26
Joao208 merged 6 commits into
mainfrom
joao-barros-/-feat-arc-browser-mcp

Conversation

@Joao208

@Joao208 Joao208 commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

O que muda

Novo MCP server para controlar o Arc Browser diretamente do Kiro/Cursor, usando uma abordagem híbrida:

AppleScript (sempre funciona, sem setup)

  • list_tabs / get_active_tab / open_url / search_tabs / close_tab / focus_tab
  • execute_js — executa JavaScript na tab ativa
  • list_spaces / get_active_space / switch_space

CDP — Chrome DevTools Protocol (precisa abrir o Arc com --remote-debugging-port=9222)

  • screenshot — captura screenshot da página (viewport ou full page)
  • cdp_evaluate — executa JS com suporte a async/await
  • network_capture — intercepta requests/responses por X segundos
  • get_console_logs — captura console.log/warn/error
  • get_cookies — lista cookies da página
  • click / hover / type_text / scroll / wait_for_selector — interação com a página
  • get_page_content — texto da página (fallback pra AppleScript se CDP não disponível)
  • cdp_status — verifica se CDP tá ativo

Setup

# Abrir Arc com CDP habilitado (uma vez)
open -a "Arc" --args --remote-debugging-port=9222

# Config no mcp.json
{
  "arc-browser": {
    "command": "node",
    "args": ["/path/to/arvore-mcp-servers/packages/arc-browser/dist/index.js"],
    "autoApprove": ["*"]
  }
}

Por que não usar só o Playwright MCP?

O Arc MCP acessa o browser real do usuário — com sessões logadas, cookies, tabs abertas. O Playwright abre um browser limpo. São complementares.

Summary by CodeRabbit

  • New Features

    • Added Arc Browser MCP package and CLI for macOS Arc control — tab/space management, page actions, DOM/page interactions (click/type/hover/scroll/wait), screenshots, JS execution, cookie/content extraction, network & console capture via CDP.
  • Documentation

    • Added package and root README entries detailing capabilities, setup modes, and security notes.
  • Chores

    • CI: refresh npm before publishing.
    • ESLint: allowlist added for extra browser/DOM globals.

@coderabbitai

coderabbitai Bot commented Apr 3, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 178f8479-113f-4b9b-9061-95f62f97b049

📥 Commits

Reviewing files that changed from the base of the PR and between 2dee169 and 20dd02e.

📒 Files selected for processing (1)
  • packages/arc-browser/package.json

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

Adds a new package @arvoretech/arc-browser-mcp (Arc Browser MCP server) with AppleScript and CDP integrations, a CLI and TypeScript project files, READMEs, and small CI and ESLint configuration updates.

Changes

Cohort / File(s) Summary
CI & Linting
\.github/workflows/ci.yml, eslint.config.js
CI publish job now runs npm install -g npm@latest before publishing; ESLint globals extended with WebSocket, AbortSignal, and ErrorEvent.
Package manifest & build config
packages/arc-browser/package.json, packages/arc-browser/tsconfig.json
New package manifest for @arvoretech/arc-browser-mcp (ESM, CLI arc-browser-mcp, Node >=22) and a tsconfig targeting ES2022 with strict checks and NodeNext resolution.
AppleScript adapter
packages/arc-browser/src/arc.ts
New Arc AppleScript utilities: promisified osascript, tab/space listing and management, URL open/new/close/focus, JS execution in active tab, and space switching with accessibility keystrokes.
CDP client
packages/arc-browser/src/cdp.ts
New CDPClient class: probe/connect to local CDP, WebSocket request/response correlation with timeouts, event handling, and high-level DOM/input helpers (click/hover/type/scroll/waitForSelector).
MCP server & CLI
packages/arc-browser/src/server.ts, packages/arc-browser/src/index.ts
New exported ArcBrowserMCPServer registering many MCP tools that combine AppleScript and CDP capabilities (tabs/spaces, screenshots, network/console capture, DOM actions, cookies, page content, cdp_status), plus CLI entrypoint and graceful shutdown.
Docs
README.md, packages/arc-browser/README.md
Top-level README section and package README documenting the MCP server, operation modes (AppleScript vs CDP), tool list, setup notes, and security considerations.

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Server as ArcBrowserMCP<br/>(Server)
    participant Arc as Arc Browser<br/>(AppleScript)
    participant CDP as Chrome DevTools<br/>(Local CDP)
    participant Browser as Browser<br/>(Runtime)

    Client->>Server: Invoke tool (e.g., listTabs / screenshot)
    alt AppleScript-based tool
        Server->>Arc: run osascript(...)
        Arc->>Browser: query UI / run script
        Browser-->>Arc: result
        Arc-->>Server: parsed result
    else CDP-based tool
        Server->>Server: ensureCDP()
        alt CDP unavailable
            Server-->>Client: error (CDP unavailable)
        else CDP available
            Server->>CDP: connect / send(method, params)
            CDP->>Browser: execute CDP command
            Browser-->>CDP: response / events
            CDP-->>Server: result / events
        end
    end
    Server-->>Client: return tool result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

"🐰
I tapped a script with fluffy paws so neat,
AppleScript hummed and CDP tapped a beat,
Tabs align, screenshots glow, networks sing,
MCP wakes—Arc Browser learns to spring,
Hoppy server, ready, swift, and fleet!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main addition: a new Arc Browser MCP Server using hybrid AppleScript and CDP technologies.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch joao-barros-/-feat-arc-browser-mcp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
packages/arc-browser/tsconfig.json (1)

4-5: Prefer NodeNext resolution for this ESM package.

With packages/arc-browser/package.json set to "type": "module" and the sources importing .js specifiers, ESNext + legacy Node resolution can type-check against rules that don’t match Node’s real ESM resolution path. Using NodeNext here keeps compile-time resolution aligned with runtime behavior.

♻️ Suggested config
-    "module": "ESNext",
-    "moduleResolution": "Node",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/arc-browser/tsconfig.json` around lines 4 - 5, Change the TypeScript
module resolution to NodeNext to match this package's ESM runtime behavior:
update the tsconfig key "moduleResolution" from "Node" to "NodeNext" so that
imports (including .js specifiers) are resolved at compile time the same way
Node's ESM loader will resolve them; ensure this aligns with "module": "ESNext"
in the same tsconfig and the package.json "type": "module".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 73-75: Replace the non-deterministic npm upgrade in the CI step
named "Install latest npm" by pinning to a tested version (e.g., change install
command from npm@latest to npm@11.12.1) and optionally echo the installed npm
version after installation for transparency; update the run command in that step
to use the pinned version and include a quick npm -v log to record the version
used.

In `@packages/arc-browser/package.json`:
- Around line 1-30: Add an "engines" entry to package.json to declare the
minimum Node runtime (e.g., "node": ">=21.0.0") so the package consumers and npm
enforce a compatible Node version; update the package.json JSON object
(alongside "name", "version", etc.) to include this "engines" field. Reference
the runtime-sensitive code paths—src/index.ts (top-level await), src/cdp.ts
(global WebSocket), and any use of fetch() or AbortSignal.timeout()—to ensure
the declared version covers those features. If you prefer not to bump the
engine, instead add explicit polyfills or shims for fetch, AbortSignal.timeout,
and WebSocket used by those modules and document the polyfills in package.json
instead of the engines field. Ensure package.json remains valid JSON and run npm
pack / npm publish checks after the change.

In `@packages/arc-browser/src/arc.ts`:
- Around line 88-97: The current executeJavaScript function embeds raw JS (only
escaping backslashes/quotes) which breaks on newlines; instead base64-encode the
js payload in Node (replace the escaped logic with something like const encoded
= Buffer.from(js, "utf8").toString("base64")), interpolate that single-line
encoded string into the osascript call, and inside the AppleScript-run JS decode
and execute it (e.g., use window.eval(atob("...")) or new
Function(atob("..."))()) so the full multi-line source is transported safely;
update references to the old escaped variable and ensure executeJavaScript and
the osascript invocation use the new encoded value.

In `@packages/arc-browser/src/cdp.ts`:
- Around line 65-95: connectWs currently calls disconnect() which only clears
the pending map leaving send() promises unresolved; add logic to reject all
in-flight pending commands with a clear Error when the socket is closed or
replaced. Modify disconnect() (or create a helper used by disconnect and
connectWs) to iterate this.pending, call handler.reject(new Error("WebSocket
closed or replaced")) for each entry, then clear the map; also add a WebSocket
"close" (and optionally "error") event listener in connectWs to perform the same
rejection/cleanup for this.ws so any in-flight send() promises are always
rejected when the socket closes or is swapped.
- Around line 117-124: The current on/off API stores handlers in the
eventHandlers Map but off(event) deletes all handlers for that event, causing
races when multiple listeners exist; update the API so on(event, handler)
registers the handler and returns an unsubscribe callback (or implement
off(event, handler) that removes only that handler) and change off to accept the
handler and only remove that specific function from the array stored in
eventHandlers (use the eventHandlers.get(event) array, filter out the provided
handler, and set or delete the entry accordingly); refer to the on and off
methods and the eventHandlers Map to locate and change the logic.

In `@packages/arc-browser/src/server.ts`:
- Around line 250-277: The CDP listeners registered via
cdp.on("Network.requestWillBeSent", ...) and cdp.on("Network.responseReceived",
...) (and the analogous Runtime listeners) are only removed on the happy path;
wrap the enable/send, sleep, and disable sequence in a try/finally so that
cdp.off(...) and the domain disable calls always run; specifically, move the
cdp.off("Network.requestWillBeSent"), cdp.off("Network.responseReceived") and
the cdp.send("Network.disable") into a finally block after the await
cdp.send("Network.enable") and sleep, and apply the same try/finally pattern to
the Runtime.enable/disable capture flow so handlers are always unregistered even
on error.
- Around line 20-30: ensureCDP currently returns the existing CDP socket even
when callers pass a different targetId; update ensureCDP to check
this.cdp.currentTargetId against the requested targetId and, if they differ,
treat it like not connected (call CDPClient.isAvailable() and
this.cdp.connectToPage(targetId) to reconnect to the requested page). Keep the
existing availability check (CDPClient.isAvailable()) and error throw path, and
only skip reconnect when this.cdp.connected is true AND this.cdp.currentTargetId
equals the requested targetId.

---

Nitpick comments:
In `@packages/arc-browser/tsconfig.json`:
- Around line 4-5: Change the TypeScript module resolution to NodeNext to match
this package's ESM runtime behavior: update the tsconfig key "moduleResolution"
from "Node" to "NodeNext" so that imports (including .js specifiers) are
resolved at compile time the same way Node's ESM loader will resolve them;
ensure this aligns with "module": "ESNext" in the same tsconfig and the
package.json "type": "module".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0dd84d8b-91fa-4c2e-972e-f27df17d3e33

📥 Commits

Reviewing files that changed from the base of the PR and between 1fabebb and d3d6821.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • eslint.config.js
  • packages/arc-browser/package.json
  • packages/arc-browser/src/arc.ts
  • packages/arc-browser/src/cdp.ts
  • packages/arc-browser/src/index.ts
  • packages/arc-browser/src/server.ts
  • packages/arc-browser/tsconfig.json

Comment thread .github/workflows/ci.yml
Comment on lines +73 to +75
- name: Install latest npm
run: npm install -g npm@latest

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify npm release drift and Node compatibility before choosing a pin
node -v
npm view npm dist-tags --json
npm view npm@latest engines --json
npm view npm@11.5.1 engines --json

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 605


🏁 Script executed:

cat -n .github/workflows/ci.yml | sed -n '60,85p'

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 920


🏁 Script executed:

rg "trusted.publishing|npm.*>=|npm@" .github/workflows/ci.yml -A 2 -B 2

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 181


🏁 Script executed:

git log --oneline -20

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 184


🏁 Script executed:

rg "11\.5|trusted.*publish|npm.*11" -i

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 59


🏁 Script executed:

find . -name "*.md" -o -name "*.json" | xargs grep -l "npm.*version\|trusted" 2>/dev/null | head -5

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 389


🏁 Script executed:

cat README.md | grep -i "npm\|version\|publish" -A 2 -B 2

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 3953


🏁 Script executed:

cat package.json | grep -A 5 -B 5 "npm\|engines\|publish"

Repository: arvoreeducacao/arvore-mcp-servers

Length of output: 1416


Pin npm to a tested version instead of latest.

Using npm@latest introduces non-determinism in the publish pipeline—npm can update unexpectedly and potentially break the provenance feature. Pin to a known compatible version (e.g., 11.12.1) and optionally log it for transparency.

Proposed change
-      - name: Install latest npm
-        run: npm install -g npm@latest
+      - name: Install npm for trusted publishing
+        run: |
+          npm install -g npm@11.12.1
+          npm --version
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Install latest npm
run: npm install -g npm@latest
- name: Install npm for trusted publishing
run: |
npm install -g npm@11.12.1
npm --version
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 73 - 75, Replace the non-deterministic
npm upgrade in the CI step named "Install latest npm" by pinning to a tested
version (e.g., change install command from npm@latest to npm@11.12.1) and
optionally echo the installed npm version after installation for transparency;
update the run command in that step to use the pinned version and include a
quick npm -v log to record the version used.

Comment thread packages/arc-browser/package.json
Comment thread packages/arc-browser/src/arc.ts
Comment thread packages/arc-browser/src/arc.ts
Comment on lines +65 to +95
private connectWs(url: string): Promise<void> {
return new Promise((resolve, reject) => {
this.disconnect();
this.ws = new WebSocket(url);

this.ws.addEventListener("open", () => resolve());
this.ws.addEventListener("error", (e) =>
reject(new Error(`WebSocket error: ${(e as ErrorEvent).message ?? "unknown"}`))
);

this.ws.addEventListener("message", (event) => {
const msg: CDPMessage = JSON.parse(
typeof event.data === "string" ? event.data : event.data.toString()
);

if (msg.id !== undefined && this.pending.has(msg.id)) {
const handler = this.pending.get(msg.id)!;
this.pending.delete(msg.id);
if (msg.error) {
handler.reject(new Error(`CDP Error: ${msg.error.message}`));
} else {
handler.resolve(msg.result ?? {});
}
}

if (msg.method && msg.params) {
const handlers = this.eventHandlers.get(msg.method) ?? [];
for (const h of handlers) h(msg.params);
}
});
});

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 | 🟠 Major

Reject in-flight CDP commands when the socket closes or is replaced.

connectWs() calls disconnect() before reconnecting, and the shutdown path also uses disconnect(). Right now disconnect() just clears pending, so any in-flight send() promise becomes unresolvable because its timeout sees the entry already gone. The same leak happens on an unexpected socket close because there is no close handler draining the map.

🔧 Suggested cleanup path
+  private failPending(error: Error): void {
+    for (const { reject } of this.pending.values()) {
+      reject(error);
+    }
+    this.pending.clear();
+  }
+
   private connectWs(url: string): Promise<void> {
     return new Promise((resolve, reject) => {
-      this.disconnect();
+      this.disconnect(new Error("CDP connection replaced"));
       this.ws = new WebSocket(url);
 
       this.ws.addEventListener("open", () => resolve());
       this.ws.addEventListener("error", (e) =>
         reject(new Error(`WebSocket error: ${(e as ErrorEvent).message ?? "unknown"}`))
       );
+      this.ws.addEventListener("close", () => {
+        this.failPending(new Error("CDP connection closed"));
+        this.ws = null;
+        this.targetId = null;
+      });
 
       this.ws.addEventListener("message", (event) => {
         const msg: CDPMessage = JSON.parse(
           typeof event.data === "string" ? event.data : event.data.toString()
         );
@@
-  disconnect(): void {
+  disconnect(error = new Error("CDP connection closed")): void {
     if (this.ws) {
       this.ws.close();
       this.ws = null;
     }
-    this.pending.clear();
+    this.failPending(error);
     this.eventHandlers.clear();
     this.targetId = null;
   }

Also applies to: 127-134

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

In `@packages/arc-browser/src/cdp.ts` around lines 65 - 95, connectWs currently
calls disconnect() which only clears the pending map leaving send() promises
unresolved; add logic to reject all in-flight pending commands with a clear
Error when the socket is closed or replaced. Modify disconnect() (or create a
helper used by disconnect and connectWs) to iterate this.pending, call
handler.reject(new Error("WebSocket closed or replaced")) for each entry, then
clear the map; also add a WebSocket "close" (and optionally "error") event
listener in connectWs to perform the same rejection/cleanup for this.ws so any
in-flight send() promises are always rejected when the socket closes or is
swapped.

Comment thread packages/arc-browser/src/cdp.ts Outdated
Comment thread packages/arc-browser/src/server.ts
Comment thread packages/arc-browser/src/server.ts Outdated
- tsconfig: use NodeNext module resolution
- package.json: add engines >= 22, files field
- arc.ts: base64-encode JS payload for safe AppleScript transport
- cdp.ts: reject pending promises on disconnect, granular off()
- server.ts: try/finally for network/console listeners, ensureCDP respects targetId
- Add package README and update root README with arc-browser entry

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
packages/arc-browser/src/cdp.ts (1)

65-75: ⚠️ Potential issue | 🟠 Major

Reject in-flight commands on unsolicited socket close.

disconnect() now rejects pending, but an unexpected close from Arc never reaches that path. Those requests sit until the 30s timeout, and ws/targetId stay stale in the meantime.

♻️ Suggested cleanup path
+  private failPending(error: Error): void {
+    for (const { reject } of this.pending.values()) {
+      reject(error);
+    }
+    this.pending.clear();
+  }
+
   private connectWs(url: string): Promise<void> {
     return new Promise((resolve, reject) => {
       this.disconnect();
-      this.ws = new WebSocket(url);
+      const ws = new WebSocket(url);
+      this.ws = ws;
 
-      this.ws.addEventListener("open", () => resolve());
-      this.ws.addEventListener("error", (e) =>
+      ws.addEventListener("open", () => resolve());
+      ws.addEventListener("error", (e) =>
         reject(new Error(`WebSocket error: ${(e as ErrorEvent).message ?? "unknown"}`))
       );
+      ws.addEventListener("close", () => {
+        if (this.ws !== ws) return;
+        this.failPending(new Error("WebSocket closed"));
+        this.ws = null;
+        this.targetId = null;
+      });
 
-      this.ws.addEventListener("message", (event) => {
+      ws.addEventListener("message", (event) => {
         const msg: CDPMessage = JSON.parse(
           typeof event.data === "string" ? event.data : event.data.toString()
         );
@@
   disconnect(): void {
     if (this.ws) {
       this.ws.close();
       this.ws = null;
     }
-    for (const [id, handler] of this.pending) {
-      handler.reject(new Error("WebSocket closed"));
-      this.pending.delete(id);
-    }
-    this.pending.clear();
+    this.failPending(new Error("WebSocket closed"));
     this.eventHandlers.clear();
     this.targetId = null;
   }

Also applies to: 142-154

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

In `@packages/arc-browser/src/cdp.ts` around lines 65 - 75, The connectWs method
currently only listens for "open", "error", and "message" events, so unexpected
socket "close" events never trigger the existing disconnect logic and in-flight
requests (pending) hang until timeout; update connectWs to add a "close" event
listener that calls the same cleanup path as disconnect() (or invokes
disconnect()) and ensures pending requests are rejected and ws/targetId cleared;
reference the connectWs method, the ws property, the disconnect() method, and
the pending collection to locate where to add the "close" handler and perform
the rejection/cleanup (also apply the same fix to the similar block at the other
location around lines 142-154).
packages/arc-browser/src/server.ts (1)

254-283: ⚠️ Potential issue | 🟠 Major

Start cleanup before *.enable() and unsubscribe by handler.

Both flows register listeners before Network.enable/Runtime.enable, but the try starts after the enable call. If enable fails, those handlers stay attached. Also off("event") still removes every handler for that event, so overlapping captures on the same client tear each other down.

🔧 Safer pattern
-        cdp.on("Network.requestWillBeSent", (p) => {
+        const offRequest = cdp.on("Network.requestWillBeSent", (p) => {
           const req = p.request as { method: string; url: string };
           const id = p.requestId as string;
           if (urlFilter && !req.url.includes(urlFilter)) return;
           requests.set(id, {
             method: req.method,
             url: req.url,
             timestamp: p.timestamp as number,
           });
         });
 
-        cdp.on("Network.responseReceived", (p) => {
+        const offResponse = cdp.on("Network.responseReceived", (p) => {
           const id = p.requestId as string;
           const resp = p.response as { status: number; mimeType: string };
           const existing = requests.get(id);
           if (existing) {
             existing.status = resp.status;
             existing.mimeType = resp.mimeType;
           }
         });
 
-        await cdp.send("Network.enable");
-
         try {
+          await cdp.send("Network.enable");
           await new Promise((r) => setTimeout(r, duration));
         } finally {
-          await cdp.send("Network.disable");
-          cdp.off("Network.requestWillBeSent");
-          cdp.off("Network.responseReceived");
+          offRequest();
+          offResponse();
+          await cdp.send("Network.disable");
         }

Apply the same shape to Runtime.consoleAPICalled.

Also applies to: 358-374

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

In `@packages/arc-browser/src/server.ts` around lines 254 - 283, Handlers for
Network.requestWillBeSent and Network.responseReceived are being registered
before calling cdp.send("Network.enable") and are removed with
cdp.off("Network.requestWillBeSent")/cddp.off("Network.responseReceived") which
either leaves handlers attached if enable fails or removes all handlers for that
event; fix by registering handlers into local named functions (e.g.,
requestWillBeSentHandler, responseReceivedHandler), attach them before enable
but ensure a try/finally that removes those exact handlers
(cdp.off("Network.requestWillBeSent", requestWillBeSentHandler) /
cdp.off("Network.responseReceived", responseReceivedHandler)) and move the
enable call after you have the handlers and the removal logic prepared so enable
failures won't leak listeners; apply the same pattern to
Runtime.consoleAPICalled and the other similar block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/arc-browser/src/cdp.ts`:
- Around line 98-113: The send() method hard-codes a 30s reply timeout causing
longer page-side waits (e.g. waitForSelector) to prematurely fail; update
send(method: string, params: Record<string, unknown> = {}, timeoutMs = 30_000)
to accept an optional timeoutMs and use that instead of the fixed 30_000 when
scheduling the rejection timer, and update callers (notably the waitForSelector
path and the other send usage around the 242-260 region) to pass through their
configured timeoutMs so CDP client-side timeouts match page-side waits; keep
msgId, pending map usage and resolve/reject behavior unchanged.
- Around line 53-63: In connectToPage, the code sets this.targetId before
calling connectWs/disconnect which causes disconnect() to clear it and leave
currentTargetId null; move the assignment of this.targetId = target.id so it
happens after a successful reconnect/attach (i.e., after await
this.connectWs(...) completes and any attach logic returns successfully) so the
field isn't cleared by disconnect(), or alternatively update disconnect() to
avoid clearing this.targetId during a reconnect flow; key symbols:
connectToPage, this.targetId, connectWs, disconnect, currentTargetId.

In `@packages/arc-browser/src/server.ts`:
- Around line 8-10: The server currently stores a single mutable CDPClient
instance (this.cdp) that is reconnected to different targets, which causes
cross-talk between concurrent requests; change to create or obtain an isolated
CDPClient per request/target instead of using the shared this.cdp: remove or
stop mutating the shared this.cdp field and update the code paths that call
connect/reconnect (e.g., the handler that performs network_capture and any
connectToTarget/reconnect logic) to instantiate a new CDPClient for each request
or pull clients from a pool keyed by target/session id, ensuring each request's
finally/cleanup runs against the same client instance it started with (or use a
Map<targetOrSession, CDPClient> to isolate clients by target/session).
- Around line 315-331: The fallback to AppleScript uses
arc.getActiveTab()/arc.executeJavaScript even when a specific CDP targetId was
requested, which can return the wrong page; update the logic in the block around
CDPClient.isAvailable() and the use of targetId so that if targetId is provided
but CDPClient.isAvailable() is false you do not fall back to the active tab:
instead return an explicit error or a clear no-data response. Concretely, inside
the method that calls CDPClient.isAvailable(), check the targetId parameter
before using arc.getActiveTab()/arc.executeJavaScript and if targetId is
non-null and CDP is unavailable, short-circuit (throw or return a structured
error/result) rather than using arc.getActiveTab(); reference ensureCDP,
CDPClient.isAvailable, targetId, arc.getActiveTab and arc.executeJavaScript when
applying the change.

---

Duplicate comments:
In `@packages/arc-browser/src/cdp.ts`:
- Around line 65-75: The connectWs method currently only listens for "open",
"error", and "message" events, so unexpected socket "close" events never trigger
the existing disconnect logic and in-flight requests (pending) hang until
timeout; update connectWs to add a "close" event listener that calls the same
cleanup path as disconnect() (or invokes disconnect()) and ensures pending
requests are rejected and ws/targetId cleared; reference the connectWs method,
the ws property, the disconnect() method, and the pending collection to locate
where to add the "close" handler and perform the rejection/cleanup (also apply
the same fix to the similar block at the other location around lines 142-154).

In `@packages/arc-browser/src/server.ts`:
- Around line 254-283: Handlers for Network.requestWillBeSent and
Network.responseReceived are being registered before calling
cdp.send("Network.enable") and are removed with
cdp.off("Network.requestWillBeSent")/cddp.off("Network.responseReceived") which
either leaves handlers attached if enable fails or removes all handlers for that
event; fix by registering handlers into local named functions (e.g.,
requestWillBeSentHandler, responseReceivedHandler), attach them before enable
but ensure a try/finally that removes those exact handlers
(cdp.off("Network.requestWillBeSent", requestWillBeSentHandler) /
cdp.off("Network.responseReceived", responseReceivedHandler)) and move the
enable call after you have the handlers and the removal logic prepared so enable
failures won't leak listeners; apply the same pattern to
Runtime.consoleAPICalled and the other similar block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 027c91d3-42fd-4402-86cb-a1d55ecad427

📥 Commits

Reviewing files that changed from the base of the PR and between d3d6821 and 32e5e6b.

📒 Files selected for processing (7)
  • README.md
  • packages/arc-browser/README.md
  • packages/arc-browser/package.json
  • packages/arc-browser/src/arc.ts
  • packages/arc-browser/src/cdp.ts
  • packages/arc-browser/src/server.ts
  • packages/arc-browser/tsconfig.json
✅ Files skipped from review due to trivial changes (4)
  • packages/arc-browser/tsconfig.json
  • packages/arc-browser/package.json
  • packages/arc-browser/README.md
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/arc-browser/src/arc.ts

Comment thread packages/arc-browser/src/cdp.ts
Comment thread packages/arc-browser/src/cdp.ts Outdated
Comment on lines +8 to +10
private server: McpServer;
private cdp: CDPClient;

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 | 🔴 Critical

A shared CDPClient will cross-talk between concurrent requests.

Line 16 creates one mutable client for the whole server, and Lines 20-33 reconnect that same instance to different targets. A long-running call like network_capture can therefore be retargeted underneath itself by a later request, then run its finally block against the wrong page.

Use a per-request client, or pool isolated clients by target/session instead of sharing one global this.cdp.

Also applies to: 16-18, 20-35

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

In `@packages/arc-browser/src/server.ts` around lines 8 - 10, The server currently
stores a single mutable CDPClient instance (this.cdp) that is reconnected to
different targets, which causes cross-talk between concurrent requests; change
to create or obtain an isolated CDPClient per request/target instead of using
the shared this.cdp: remove or stop mutating the shared this.cdp field and
update the code paths that call connect/reconnect (e.g., the handler that
performs network_capture and any connectToTarget/reconnect logic) to instantiate
a new CDPClient for each request or pull clients from a pool keyed by
target/session id, ensuring each request's finally/cleanup runs against the same
client instance it started with (or use a Map<targetOrSession, CDPClient> to
isolate clients by target/session).

Comment thread packages/arc-browser/src/server.ts
Joao208 added 2 commits April 3, 2026 22:58
- arc.ts: guard switchSpace for >10 spaces instead of silent fallback
- cdp.ts: set targetId after connectWs to avoid disconnect clearing it
- cdp.ts: configurable send timeout, waitForSelector uses timeoutMs+5s
- server.ts: error when targetId specified but CDP unavailable in get_page_content
- server.ts: add too_many_spaces message to switch_space
@Joao208 Joao208 merged commit d7bc6cb into main Apr 4, 2026
2 checks passed
@Joao208 Joao208 deleted the joao-barros-/-feat-arc-browser-mcp branch April 4, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant