agent-browser-bridge 0.0.0-stage → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -2
- package/bin/cli.js +8 -0
- package/core/bridge.js +293 -0
- package/core/state.js +68 -0
- package/index.js +23 -0
- package/package.json +36 -4
- package/tools/browser-data-tools.js +141 -0
- package/tools/diagnostic-tools.js +170 -0
- package/tools/dom-tools.js +295 -0
- package/tools/emulation-tools.js +138 -0
- package/tools/extraction-tools.js +74 -0
- package/tools/media-tools.js +55 -0
- package/tools/navigation-tools.js +71 -0
- package/tools/network-tools.js +197 -0
- package/tools/registry.js +23 -0
- package/tools/tab-tools.js +186 -0
package/README.md
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agent-browser-bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Live multi-tab, multi-profile Chrome DevTools Protocol (CDP) Model Context Protocol (MCP) server for AI coding agents.**
|
|
4
|
+
|
|
5
|
+
Works with **Antigravity**, **Claude Desktop / Claude Code**, **Cursor**, **Windsurf**, and any MCP client.
|
|
6
|
+
|
|
7
|
+
Unlike standard `chrome-devtools-mcp` (which requires launching a fresh, isolated Chrome instance without your logins or extensions), **`agent-browser-bridge` connects directly to your everyday browser profile** (Chrome, Edge, Brave) with all your active sessions, cookies, open tabs, and logged-in web apps intact.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🚀 Quick Start (Zero Install)
|
|
12
|
+
|
|
13
|
+
Add `agent-browser-bridge` to your agent's MCP configuration:
|
|
14
|
+
|
|
15
|
+
### 1. Google Antigravity
|
|
16
|
+
In your workspace `.agents/mcp_config.json` or global config:
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"agent-browser-bridge": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "agent-browser-bridge"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Claude Desktop
|
|
29
|
+
In `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"agent-browser-bridge": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["-y", "agent-browser-bridge"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Cursor & Windsurf
|
|
42
|
+
Add a new MCP server in settings:
|
|
43
|
+
- **Type**: `command`
|
|
44
|
+
- **Command**: `npx -y agent-browser-bridge`
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🧩 Browser Extension Setup
|
|
49
|
+
|
|
50
|
+
1. Install the companion extension from the Chrome Web Store (or load unpacked from source).
|
|
51
|
+
2. Open the extension popup in your browser — ensure it shows **"Connected"** (port `18922`).
|
|
52
|
+
3. Your AI agent now has full, real-time control over the browser profile!
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🛠️ Complete Tool Catalog (68 Tools)
|
|
57
|
+
|
|
58
|
+
### Multi-Profile & Tab Management
|
|
59
|
+
- `browser_list_profiles`: List all connected browser profiles (Work, Personal, Brave, Edge).
|
|
60
|
+
- `browser_list_tabs`: List open tabs across all browser windows.
|
|
61
|
+
- `browser_switch_tab`: Focus a tab and bring its browser window to the foreground.
|
|
62
|
+
- `browser_new_tab`: Open a new tab with an optional URL.
|
|
63
|
+
- `browser_close_tab`: Close a tab by tabId.
|
|
64
|
+
- `browser_duplicate_tab`: Duplicate an existing tab with its state and history.
|
|
65
|
+
- `browser_pin_tab`: Pin or unpin a tab in the browser tab strip.
|
|
66
|
+
- `browser_set_zoom`: Get or set zoom factor (1.0, 1.25, 0.8).
|
|
67
|
+
- `browser_navigate`: Navigate an existing tab or open a new URL.
|
|
68
|
+
- `browser_go_back`: Navigate back in history.
|
|
69
|
+
- `browser_go_forward`: Navigate forward in history.
|
|
70
|
+
- `browser_reload`: Reload tab with optional cache bypass.
|
|
71
|
+
- `browser_scroll`: Scroll viewport or container in any direction with velocity HUD.
|
|
72
|
+
- `browser_read_all_tabs`: Extract visible text and metadata from every open tab at once.
|
|
73
|
+
- `browser_manage_tab_group`: Group, ungroup, colorize, collapse, or expand tabs.
|
|
74
|
+
|
|
75
|
+
### Smart Content & Table Extraction
|
|
76
|
+
- `browser_get_markdown`: Clean Markdown conversion stripping boilerplate/ads.
|
|
77
|
+
- `browser_extract_table`: Structured table extraction to JSON, Markdown, or CSV.
|
|
78
|
+
- `browser_find_in_page`: In-page search (Ctrl+F) with smooth scrolling and match highlighting.
|
|
79
|
+
|
|
80
|
+
### DOM, Visual Refs & CSS Styling
|
|
81
|
+
- `browser_get_interactive_snapshot`: Map interactive elements with deterministic `data-agent-ref` IDs (`ref_1`, `ref_2`...) and coordinates.
|
|
82
|
+
- `browser_take_snapshot`: Accessibility (a11y) tree text snapshot matching `chrome-devtools-mcp`.
|
|
83
|
+
- `browser_get_css_styles`: Inspect computed CSS styles for any element by ref or selector.
|
|
84
|
+
- `browser_wait_for`: Wait for specific text or CSS selector to appear on the page.
|
|
85
|
+
|
|
86
|
+
### Precision Hardware-Level Input
|
|
87
|
+
- `browser_act_on_ref`: Visual highlight overlay + click or text input on a `data-agent-ref`.
|
|
88
|
+
- `browser_click`: Hardware CDP mouse click (by ref, selector, or x/y coordinates).
|
|
89
|
+
- `browser_hover`: Trigger mouse hover states.
|
|
90
|
+
- `browser_drag`: Drag and drop with animated laser trajectory.
|
|
91
|
+
- `browser_fill`: Clear and fill an input or textarea with text.
|
|
92
|
+
- `browser_fill_form`: Fill multiple form fields simultaneously.
|
|
93
|
+
- `browser_press_key`: Physical keyboard key press with 3D mechanical keycap popovers.
|
|
94
|
+
- `browser_type_text`: Type text into current focus or target element.
|
|
95
|
+
- `browser_select_option`: Select dropdown option by value, visible text, or index.
|
|
96
|
+
- `browser_set_checked`: Toggle checkbox or radio button with spring bloom animation.
|
|
97
|
+
- `browser_upload_file`: Upload local file(s) into file input elements (`DOM.setFileInputFiles`).
|
|
98
|
+
- `browser_handle_dialog`: Accept or dismiss JavaScript `alert()`, `confirm()`, or `prompt()` dialogs.
|
|
99
|
+
|
|
100
|
+
### User Context, Bookmarks & Desktop Notifications
|
|
101
|
+
- `browser_search_bookmarks`: Search bookmarks by keyword or folder.
|
|
102
|
+
- `browser_add_bookmark`: Create a new bookmark.
|
|
103
|
+
- `browser_search_history`: Search browsing history by query and time range.
|
|
104
|
+
- `browser_show_notification`: Display native OS desktop notifications.
|
|
105
|
+
- `browser_clipboard`: Read or copy text to/from system clipboard.
|
|
106
|
+
- `browser_download_file`: Trigger file download from URL.
|
|
107
|
+
- `browser_wait_for_download`: Track active downloads until completion.
|
|
108
|
+
- `browser_list_downloads`: List recent downloads in profile.
|
|
109
|
+
|
|
110
|
+
### Media Controls (Audio & Video)
|
|
111
|
+
- `browser_media_control`: Play, pause, seek, mute, volume, playback speed.
|
|
112
|
+
- `browser_mute_tab`: Mute/unmute all audio output for a tab.
|
|
113
|
+
- `browser_get_subtitles`: Extract live captions or video subtitles.
|
|
114
|
+
|
|
115
|
+
### Network Interception, Ad-Blocking & Routing
|
|
116
|
+
- `browser_block_urls`: Wildcard URL pattern blocking (`Network.setBlockedURLs`).
|
|
117
|
+
- `browser_set_extra_headers`: Custom HTTP header injection on outgoing requests.
|
|
118
|
+
- `browser_mock_route`: Real-time CDP Fetch route interception and custom response mocking.
|
|
119
|
+
- `browser_list_network_requests`: List recorded network requests for a tab.
|
|
120
|
+
- `browser_get_network_request`: Get full details and headers for a requestId.
|
|
121
|
+
- `browser_get_network_body`: Fetch raw response body for any recorded request.
|
|
122
|
+
- `browser_get_cookies`: Retrieve cookies for a tab's URL.
|
|
123
|
+
- `browser_set_cookie`: Set a cookie.
|
|
124
|
+
- `browser_clear_cookies`: Clear all cookies for the profile.
|
|
125
|
+
- `browser_storage`: Read, write, or clear `localStorage` / `sessionStorage`.
|
|
126
|
+
|
|
127
|
+
### Device & Network Emulation
|
|
128
|
+
- `browser_resize_page`: Set viewport dimensions (width, height, deviceScaleFactor, mobile).
|
|
129
|
+
- `browser_emulate`: Emulate dark/light mode, CPU throttling, network throttling, geolocation, user-agent.
|
|
130
|
+
- `browser_emulate_timezone`: Override timezone and locale.
|
|
131
|
+
|
|
132
|
+
### Diagnostics, Performance & Raw CDP
|
|
133
|
+
- `browser_screenshot`: Capture high-resolution PNG or JPEG tab screenshot.
|
|
134
|
+
- `browser_print_to_pdf`: Generate PDF document via `Page.printToPDF`.
|
|
135
|
+
- `browser_list_console_messages`: List captured console logs, warnings, errors, and stack traces.
|
|
136
|
+
- `browser_get_telemetry`: Combined telemetry snapshot (console, network, dialogs).
|
|
137
|
+
- `browser_take_heapsnapshot`: V8 heap snapshot for memory leak analysis.
|
|
138
|
+
- `browser_performance_trace`: Start or stop Chrome performance timeline trace.
|
|
139
|
+
- `browser_performance_analyze_insight`: Extract Core Web Vitals (TTFB, FCP, DOMContentLoaded, LoadTime).
|
|
140
|
+
- `browser_eval_js`: Evaluate arbitrary JavaScript in tab context with async/await.
|
|
141
|
+
- `browser_raw_cdp`: Execute ANY raw Chrome DevTools Protocol command.
|
|
142
|
+
- `browser_request_human_takeover`: Pause agent and display takeover banner for manual 2FA/CAPTCHA.
|
|
143
|
+
- `browser_reload_extension`: Hot reload the extension service worker.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 🔒 Security & Privacy
|
|
148
|
+
|
|
149
|
+
- **Local-Only**: Only binds to `127.0.0.1:18922` by default. Never exposes ports to external networks.
|
|
150
|
+
- **Instant Killswitch**: Toggle "Allow Agent Control" off anytime in the extension popup to instantly freeze agent access.
|
|
151
|
+
- **Visual Action Overlay**: Every click and keystroke displays a glowing cyan overlay on your screen so you can clearly see what the agent is doing.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 📄 License
|
|
156
|
+
|
|
157
|
+
Apache-2.0
|
package/bin/cli.js
ADDED
package/core/bridge.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Agent Bridge MCP - Master Server & Relay Architecture (ES Module)
|
|
2
|
+
import http from "http";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import {
|
|
6
|
+
PORT,
|
|
7
|
+
activeProfiles,
|
|
8
|
+
pendingRequests,
|
|
9
|
+
serverState,
|
|
10
|
+
log,
|
|
11
|
+
AUTO_SPAWN,
|
|
12
|
+
spawnIsolatedBrowser,
|
|
13
|
+
} from "./state.js";
|
|
14
|
+
|
|
15
|
+
export async function checkMasterAlive() {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/health`, {
|
|
18
|
+
signal: AbortSignal.timeout(1000),
|
|
19
|
+
cache: "no-store",
|
|
20
|
+
});
|
|
21
|
+
return res.ok;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function startMasterServer() {
|
|
28
|
+
if (serverState.isMasterServer && serverState.httpServer?.listening) return;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (serverState.httpServer) {
|
|
32
|
+
serverState.httpServer.close();
|
|
33
|
+
serverState.httpServer = null;
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
|
|
37
|
+
serverState.httpServer = http.createServer((req, res) => {
|
|
38
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
39
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
40
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
41
|
+
|
|
42
|
+
if (req.method === "OPTIONS") {
|
|
43
|
+
res.writeHead(204);
|
|
44
|
+
res.end();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const reqUrl = new URL(req.url, `http://${req.headers.host || "127.0.0.1"}`);
|
|
49
|
+
|
|
50
|
+
if (reqUrl.pathname === "/health" || reqUrl.pathname === "/") {
|
|
51
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
52
|
+
res.end(JSON.stringify({ status: "ok", profiles: activeProfiles.size, pid: process.pid }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (req.method === "GET" && reqUrl.pathname === "/profiles") {
|
|
57
|
+
const list = Array.from(activeProfiles.values()).map((p) => ({
|
|
58
|
+
id: p.id,
|
|
59
|
+
name: p.name,
|
|
60
|
+
browser: p.browser,
|
|
61
|
+
connectedAt: new Date(p.connectedAt).toISOString(),
|
|
62
|
+
lastActive: new Date(p.lastActive).toISOString(),
|
|
63
|
+
}));
|
|
64
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
65
|
+
res.end(JSON.stringify({ count: list.length, profiles: list }));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (req.method === "POST" && reqUrl.pathname === "/rpc") {
|
|
70
|
+
let body = "";
|
|
71
|
+
req.on("data", (chunk) => (body += chunk));
|
|
72
|
+
req.on("end", async () => {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(body || "{}");
|
|
75
|
+
const targetProfile = parsed.profile || reqUrl.searchParams.get("profile");
|
|
76
|
+
const result = await callProfileExtensionInternal(targetProfile, parsed.action, parsed.params || {});
|
|
77
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
78
|
+
res.end(JSON.stringify({ success: true, result }));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
81
|
+
res.end(JSON.stringify({ success: false, error: err.message }));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
res.writeHead(404);
|
|
88
|
+
res.end();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
serverState.httpServer.on("error", (err) => {
|
|
92
|
+
if (err.code === "EADDRINUSE") {
|
|
93
|
+
log(`Port ${PORT} in use. Running as Relay Client.`);
|
|
94
|
+
serverState.isMasterServer = false;
|
|
95
|
+
} else {
|
|
96
|
+
log("Server error:", err.message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
serverState.wss = new WebSocketServer({ server: serverState.httpServer });
|
|
101
|
+
|
|
102
|
+
serverState.wss.on("connection", (ws) => {
|
|
103
|
+
let currentProfileId = null;
|
|
104
|
+
|
|
105
|
+
ws.on("message", (data) => {
|
|
106
|
+
try {
|
|
107
|
+
const msg = JSON.parse(data.toString());
|
|
108
|
+
|
|
109
|
+
if (msg.type === "register") {
|
|
110
|
+
currentProfileId = msg.profileId;
|
|
111
|
+
activeProfiles.set(currentProfileId, {
|
|
112
|
+
id: msg.profileId,
|
|
113
|
+
name: msg.profileName || "Browser Profile",
|
|
114
|
+
browser: msg.browser || "Chromium",
|
|
115
|
+
ws,
|
|
116
|
+
connectedAt: Date.now(),
|
|
117
|
+
lastActive: Date.now(),
|
|
118
|
+
});
|
|
119
|
+
log(`Profile registered: "${msg.profileName}" (${msg.browser}) [${msg.profileId}]`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (msg.type === "heartbeat") {
|
|
124
|
+
if (currentProfileId && activeProfiles.has(currentProfileId)) {
|
|
125
|
+
const p = activeProfiles.get(currentProfileId);
|
|
126
|
+
p.lastActive = Date.now();
|
|
127
|
+
if (msg.profileName) p.name = msg.profileName;
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (msg.id && pendingRequests.has(msg.id)) {
|
|
133
|
+
const req = pendingRequests.get(msg.id);
|
|
134
|
+
pendingRequests.delete(msg.id);
|
|
135
|
+
clearTimeout(req.timer);
|
|
136
|
+
if (msg.error) {
|
|
137
|
+
req.reject(new Error(msg.error));
|
|
138
|
+
} else {
|
|
139
|
+
req.resolve(msg.result);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log("WebSocket message parse error:", err.message);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
ws.on("close", () => {
|
|
148
|
+
if (currentProfileId && activeProfiles.has(currentProfileId)) {
|
|
149
|
+
const p = activeProfiles.get(currentProfileId);
|
|
150
|
+
log(`Profile disconnected: "${p.name}" [${currentProfileId}]`);
|
|
151
|
+
activeProfiles.delete(currentProfileId);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
ws.on("error", (err) => {
|
|
156
|
+
log("WebSocket client error:", err.message);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
serverState.httpServer.listen(PORT, "127.0.0.1", () => {
|
|
161
|
+
serverState.isMasterServer = true;
|
|
162
|
+
log(`Master Bridge listening on http://127.0.0.1:${PORT} (WebSocket + HTTP Relay)`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function ensureMasterOrRelay() {
|
|
167
|
+
if (serverState.isMasterServer && serverState.httpServer?.listening) return true;
|
|
168
|
+
const alive = await checkMasterAlive();
|
|
169
|
+
if (alive) {
|
|
170
|
+
serverState.isMasterServer = false;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
startMasterServer();
|
|
174
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
175
|
+
return serverState.isMasterServer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function initMasterRelayWatchdog() {
|
|
179
|
+
setInterval(async () => {
|
|
180
|
+
if (!serverState.isMasterServer) {
|
|
181
|
+
const alive = await checkMasterAlive();
|
|
182
|
+
if (!alive) {
|
|
183
|
+
log("Master server offline. Auto-promoting this instance to Master Bridge...");
|
|
184
|
+
await ensureMasterOrRelay();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}, 3500);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function waitForProfile(timeoutMs = 10000) {
|
|
191
|
+
if (activeProfiles.size > 0) return;
|
|
192
|
+
if (AUTO_SPAWN && !serverState.browserProcess) spawnIsolatedBrowser();
|
|
193
|
+
|
|
194
|
+
const start = Date.now();
|
|
195
|
+
while (Date.now() - start < timeoutMs) {
|
|
196
|
+
if (activeProfiles.size > 0) return;
|
|
197
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function getTargetProfile(requestedProfile) {
|
|
202
|
+
await waitForProfile();
|
|
203
|
+
|
|
204
|
+
if (activeProfiles.size === 0) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`No browser profile with "Agent Bridge MCP" extension is currently connected on ws://127.0.0.1:${PORT}.\n` +
|
|
207
|
+
`Please open your browser (Chrome/Edge/Brave) where the extension is loaded and ensure the popup status shows "Connected".`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (requestedProfile) {
|
|
212
|
+
const req = requestedProfile.toLowerCase().trim();
|
|
213
|
+
if (activeProfiles.has(requestedProfile)) {
|
|
214
|
+
return activeProfiles.get(requestedProfile);
|
|
215
|
+
}
|
|
216
|
+
for (const profile of activeProfiles.values()) {
|
|
217
|
+
if (profile.name.toLowerCase() === req || profile.id.toLowerCase() === req) {
|
|
218
|
+
return profile;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const profile of activeProfiles.values()) {
|
|
222
|
+
if (profile.name.toLowerCase().includes(req)) {
|
|
223
|
+
return profile;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const available = Array.from(activeProfiles.values())
|
|
227
|
+
.map((p) => `"${p.name}" (ID: ${p.id})`)
|
|
228
|
+
.join(", ");
|
|
229
|
+
throw new Error(`Profile "${requestedProfile}" not found. Available profiles: ${available}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (activeProfiles.size === 1) {
|
|
233
|
+
return activeProfiles.values().next().value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let mostRecent = null;
|
|
237
|
+
for (const profile of activeProfiles.values()) {
|
|
238
|
+
if (!mostRecent || profile.lastActive > mostRecent.lastActive) {
|
|
239
|
+
mostRecent = profile;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return mostRecent;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function callProfileExtensionInternal(profileQuery, action, params = {}, timeoutMs = 25000) {
|
|
246
|
+
const profile = await getTargetProfile(profileQuery);
|
|
247
|
+
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
if (!profile.ws || profile.ws.readyState !== 1) {
|
|
250
|
+
return reject(new Error(`Browser profile "${profile.name}" WebSocket is not open.`));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const id = crypto.randomUUID();
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
pendingRequests.delete(id);
|
|
256
|
+
reject(new Error(`Extension request timed out (${action}) on profile "${profile.name}".`));
|
|
257
|
+
}, timeoutMs);
|
|
258
|
+
|
|
259
|
+
pendingRequests.set(id, { resolve, reject, timer });
|
|
260
|
+
profile.ws.send(JSON.stringify({ id, action, params }));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function callProfileExtension(profileQuery, action, params = {}, timeoutMs = 25000) {
|
|
265
|
+
if (!serverState.isMasterServer) {
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/rpc`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: { "Content-Type": "application/json" },
|
|
270
|
+
body: JSON.stringify({ profile: profileQuery, action, params }),
|
|
271
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
272
|
+
});
|
|
273
|
+
const data = await res.json();
|
|
274
|
+
if (!data.success) throw new Error(data.error);
|
|
275
|
+
return data.result;
|
|
276
|
+
} catch (err) {
|
|
277
|
+
log(`Relay call failed (${err.message}). Checking Master health...`);
|
|
278
|
+
const masterStillAlive = await checkMasterAlive();
|
|
279
|
+
if (!masterStillAlive) {
|
|
280
|
+
log("Master server died. Promoting this instance to Master Bridge...");
|
|
281
|
+
startMasterServer();
|
|
282
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
283
|
+
if (serverState.isMasterServer) {
|
|
284
|
+
return callProfileExtensionInternal(profileQuery, action, params, timeoutMs);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return callProfileExtensionInternal(profileQuery, action, params, timeoutMs);
|
|
292
|
+
}
|
|
293
|
+
|
package/core/state.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Agent Bridge MCP - Server State & Configuration (ES Module)
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export const EXTENSION_PATH = path.resolve(__dirname, "../../extension");
|
|
10
|
+
export const PORT = Number(process.env.MCP_BROWSER_PORT) || 18922;
|
|
11
|
+
export const AUTO_SPAWN = process.env.AUTO_SPAWN === "true";
|
|
12
|
+
|
|
13
|
+
// Track connected browser profiles: profileId -> { id, name, browser, ws, lastActive, tabCount }
|
|
14
|
+
export const activeProfiles = new Map();
|
|
15
|
+
export const pendingRequests = new Map();
|
|
16
|
+
|
|
17
|
+
export const serverState = {
|
|
18
|
+
isMasterServer: false,
|
|
19
|
+
httpServer: null,
|
|
20
|
+
wss: null,
|
|
21
|
+
browserProcess: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function log(...args) {
|
|
25
|
+
console.error("[Agent Bridge MCP]", ...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function jsonResult(data) {
|
|
29
|
+
return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveChromeBinary() {
|
|
33
|
+
if (process.env.CHROME_BIN) return process.env.CHROME_BIN;
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
const candidates = [
|
|
36
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
37
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
38
|
+
path.join(process.env.LOCALAPPDATA || "", "Google\\Chrome\\Application\\chrome.exe"),
|
|
39
|
+
];
|
|
40
|
+
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
41
|
+
}
|
|
42
|
+
if (process.platform === "darwin") {
|
|
43
|
+
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
44
|
+
}
|
|
45
|
+
return "google-chrome";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function spawnIsolatedBrowser() {
|
|
49
|
+
if (serverState.browserProcess) return;
|
|
50
|
+
const profileDir = path.join(os.homedir(), ".agent-browser-profile");
|
|
51
|
+
const chromeBin = resolveChromeBinary();
|
|
52
|
+
log(`Spawning isolated fallback Chrome instance at ${profileDir}...`);
|
|
53
|
+
|
|
54
|
+
serverState.browserProcess = spawn(
|
|
55
|
+
chromeBin,
|
|
56
|
+
[
|
|
57
|
+
`--user-data-dir=${profileDir}`,
|
|
58
|
+
`--load-extension=${EXTENSION_PATH}`,
|
|
59
|
+
"--silent-debugger-extension-api",
|
|
60
|
+
"--no-first-run",
|
|
61
|
+
"--no-default-browser-check",
|
|
62
|
+
"about:blank",
|
|
63
|
+
],
|
|
64
|
+
{ detached: true, stdio: "ignore" }
|
|
65
|
+
);
|
|
66
|
+
serverState.browserProcess.unref();
|
|
67
|
+
}
|
|
68
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Agent Bridge MCP Server - Main Entrypoint (ES Module)
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { ensureMasterOrRelay, initMasterRelayWatchdog } from "./core/bridge.js";
|
|
6
|
+
import { registerAllTools } from "./tools/registry.js";
|
|
7
|
+
|
|
8
|
+
// 1. Initialize Master Server or Relay Client
|
|
9
|
+
await ensureMasterOrRelay();
|
|
10
|
+
initMasterRelayWatchdog();
|
|
11
|
+
|
|
12
|
+
// 2. Initialize MCP Server
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "custom-browser-bridge",
|
|
15
|
+
version: "2.2.0",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 3. Register All 68 Tools Across Modules
|
|
19
|
+
registerAllTools(server);
|
|
20
|
+
|
|
21
|
+
// 4. Start Transport
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-browser-bridge",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Live multi-tab, multi-profile Chrome DevTools MCP bridge for AI agents (Antigravity, Claude, Cursor, Windsurf)",
|
|
5
|
+
"author": "thisisdarkstar",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agent-browser-bridge": "bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"index.js",
|
|
14
|
+
"core",
|
|
15
|
+
"tools",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node index.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"model-context-protocol",
|
|
24
|
+
"mcp-server",
|
|
25
|
+
"chrome-extension",
|
|
26
|
+
"browser-control",
|
|
27
|
+
"devtools",
|
|
28
|
+
"antigravity",
|
|
29
|
+
"claude",
|
|
30
|
+
"cursor"
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.6.0",
|
|
35
|
+
"ws": "^8.18.0",
|
|
36
|
+
"zod": "^3.23.8"
|
|
37
|
+
}
|
|
38
|
+
}
|