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 +54 -22
- package/core/bridge.js +293 -0
- package/core/state.js +68 -0
- package/index.js +10 -951
- package/package.json +3 -1
- 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
|
@@ -53,7 +53,7 @@ Add a new MCP server in settings:
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
-
## 🛠️ Complete Tool Catalog (
|
|
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
|
|
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
|
|
81
|
-
- `browser_press_key`:
|
|
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
|
-
###
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
93
|
-
- `browser_get_network_body`: Fetch raw response body for any recorded
|
|
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
|
|
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
|
-
###
|
|
100
|
-
- `
|
|
101
|
-
- `
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
- `
|
|
111
|
-
- `
|
|
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
|
+
|