agent-browser-bridge 2.0.0 → 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 CHANGED
@@ -53,7 +53,7 @@ Add a new MCP server in settings:
53
53
 
54
54
  ---
55
55
 
56
- ## 🛠️ Complete Tool Catalog (39 Tools)
56
+ ## 🛠️ Complete Tool Catalog (68 Tools)
57
57
 
58
58
  ### Multi-Profile & Tab Management
59
59
  - `browser_list_profiles`: List all connected browser profiles (Work, Personal, Brave, Edge).
@@ -61,9 +61,21 @@ Add a new MCP server in settings:
61
61
  - `browser_switch_tab`: Focus a tab and bring its browser window to the foreground.
62
62
  - `browser_new_tab`: Open a new tab with an optional URL.
63
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).
64
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.
65
70
  - `browser_reload`: Reload tab with optional cache bypass.
71
+ - `browser_scroll`: Scroll viewport or container in any direction with velocity HUD.
66
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.
67
79
 
68
80
  ### DOM, Visual Refs & CSS Styling
69
81
  - `browser_get_interactive_snapshot`: Map interactive elements with deterministic `data-agent-ref` IDs (`ref_1`, `ref_2`...) and coordinates.
@@ -75,40 +87,60 @@ Add a new MCP server in settings:
75
87
  - `browser_act_on_ref`: Visual highlight overlay + click or text input on a `data-agent-ref`.
76
88
  - `browser_click`: Hardware CDP mouse click (by ref, selector, or x/y coordinates).
77
89
  - `browser_hover`: Trigger mouse hover states.
78
- - `browser_drag`: Drag and drop from source element/coords to destination element/coords.
90
+ - `browser_drag`: Drag and drop with animated laser trajectory.
79
91
  - `browser_fill`: Clear and fill an input or textarea with text.
80
- - `browser_fill_form`: Fill multiple form fields simultaneously given a map of `{ selector_or_ref: value }`.
81
- - `browser_press_key`: Press keyboard keys or shortcuts (`Enter`, `Tab`, `Escape`, `Backspace`, `ArrowDown`).
92
+ - `browser_fill_form`: Fill multiple form fields simultaneously.
93
+ - `browser_press_key`: Physical keyboard key press with 3D mechanical keycap popovers.
82
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.
83
97
  - `browser_upload_file`: Upload local file(s) into file input elements (`DOM.setFileInputFiles`).
84
98
  - `browser_handle_dialog`: Accept or dismiss JavaScript `alert()`, `confirm()`, or `prompt()` dialogs.
85
99
 
86
- ### Device & Network Emulation
87
- - `browser_resize_page`: Set viewport dimensions (width, height, deviceScaleFactor, mobile).
88
- - `browser_emulate`: Emulate dark/light color scheme, CPU throttling, network conditions, Geolocation, or User Agent.
89
-
90
- ### Network, Cookies & Local Storage
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.
91
119
  - `browser_list_network_requests`: List recorded network requests for a tab.
92
- - `browser_get_network_request`: Get full details and headers for a specific requestId.
93
- - `browser_get_network_body`: Fetch raw response body for any recorded network request.
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.
94
122
  - `browser_get_cookies`: Retrieve cookies for a tab's URL.
95
- - `browser_set_cookie`: Set a cookie (name, value, domain, path, secure, httpOnly).
123
+ - `browser_set_cookie`: Set a cookie.
96
124
  - `browser_clear_cookies`: Clear all cookies for the profile.
97
125
  - `browser_storage`: Read, write, or clear `localStorage` / `sessionStorage`.
98
126
 
99
- ### Console, Telemetry & Diagnostics
100
- - `browser_list_console_messages`: List captured console logs, warnings, errors, and uncaught exceptions with stack traces.
101
- - `browser_get_telemetry`: Combined telemetry snapshot (console logs, network requests, dialog events).
102
-
103
- ### Performance, Memory & Screenshots
104
- - `browser_screenshot`: Capture high-resolution PNG tab screenshot.
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).
105
137
  - `browser_take_heapsnapshot`: V8 heap snapshot for memory leak analysis.
106
138
  - `browser_performance_trace`: Start or stop Chrome performance timeline trace.
107
139
  - `browser_performance_analyze_insight`: Extract Core Web Vitals (TTFB, FCP, DOMContentLoaded, LoadTime).
108
-
109
- ### JavaScript Execution & Raw CDP Escape Hatch
110
- - `browser_eval_js`: Evaluate arbitrary JavaScript in the tab context with async/await.
111
- - `browser_raw_cdp`: Execute ANY Chrome DevTools Protocol command with custom parameters.
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.
112
144
 
113
145
  ---
114
146
 
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
+