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/index.js CHANGED
@@ -1,964 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // Agent Bridge MCP Server - Main Entrypoint (ES Module)
1
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { WebSocketServer } from "ws";
4
- import { z } from "zod";
5
- import crypto from "crypto";
6
- import { spawn } from "child_process";
7
- import path from "path";
8
- import os from "os";
9
- import fs from "fs";
10
- import http from "http";
11
- import { fileURLToPath } from "url";
5
+ import { ensureMasterOrRelay, initMasterRelayWatchdog } from "./core/bridge.js";
6
+ import { registerAllTools } from "./tools/registry.js";
12
7
 
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- const EXTENSION_PATH = path.resolve(__dirname, "../extension");
15
- const PORT = Number(process.env.MCP_BROWSER_PORT) || 18922;
16
- const AUTO_SPAWN = process.env.AUTO_SPAWN === "true";
17
-
18
- // Track connected browser profiles: profileId -> { id, name, browser, ws, lastActive, tabCount }
19
- const activeProfiles = new Map();
20
- const pendingRequests = new Map();
21
- let browserProcess = null;
22
-
23
- function log(...args) {
24
- console.error("[Agent Bridge MCP]", ...args);
25
- }
26
-
27
- // 1. Multi-Instance Master / Relay Architecture
28
- // If an instance of Antigravity is already running server/index.js, subsequent Antigravity
29
- // windows will automatically operate as Relay Clients, forwarding requests to the Master
30
- // via HTTP RPC on port 18922. Every Antigravity instance shares the same live browser!
31
- let isMasterServer = false;
32
- let httpServer = null;
33
- let wss = null;
34
-
35
- async function checkMasterAlive() {
36
- try {
37
- const res = await fetch(`http://127.0.0.1:${PORT}/health`, {
38
- signal: AbortSignal.timeout(1000),
39
- cache: "no-store",
40
- });
41
- return res.ok;
42
- } catch {
43
- return false;
44
- }
45
- }
46
-
47
- function startMasterServer() {
48
- if (isMasterServer && httpServer?.listening) return;
49
-
50
- try {
51
- if (httpServer) {
52
- httpServer.close();
53
- httpServer = null;
54
- }
55
- } catch {}
56
-
57
- httpServer = http.createServer((req, res) => {
58
- res.setHeader("Access-Control-Allow-Origin", "*");
59
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
60
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
61
-
62
- if (req.method === "OPTIONS") {
63
- res.writeHead(204);
64
- res.end();
65
- return;
66
- }
67
-
68
- if (req.url === "/health" || req.url === "/") {
69
- res.writeHead(200, { "Content-Type": "application/json" });
70
- res.end(JSON.stringify({ status: "ok", profiles: activeProfiles.size, pid: process.pid }));
71
- return;
72
- }
73
-
74
- if (req.method === "GET" && req.url === "/profiles") {
75
- const list = Array.from(activeProfiles.values()).map((p) => ({
76
- id: p.id,
77
- name: p.name,
78
- browser: p.browser,
79
- connectedAt: new Date(p.connectedAt).toISOString(),
80
- lastActive: new Date(p.lastActive).toISOString(),
81
- }));
82
- res.writeHead(200, { "Content-Type": "application/json" });
83
- res.end(JSON.stringify({ count: list.length, profiles: list }));
84
- return;
85
- }
86
-
87
- if (req.method === "POST" && req.url === "/rpc") {
88
- let body = "";
89
- req.on("data", (chunk) => (body += chunk));
90
- req.on("end", async () => {
91
- try {
92
- const { profile, action, params } = JSON.parse(body);
93
- const result = await callProfileExtensionInternal(profile, action, params);
94
- res.writeHead(200, { "Content-Type": "application/json" });
95
- res.end(JSON.stringify({ success: true, result }));
96
- } catch (err) {
97
- res.writeHead(500, { "Content-Type": "application/json" });
98
- res.end(JSON.stringify({ success: false, error: err.message }));
99
- }
100
- });
101
- return;
102
- }
103
-
104
- res.writeHead(404);
105
- res.end();
106
- });
107
-
108
- httpServer.on("error", (err) => {
109
- if (err.code === "EADDRINUSE") {
110
- log(`Port ${PORT} in use. Running as Relay Client.`);
111
- isMasterServer = false;
112
- } else {
113
- log("Server error:", err.message);
114
- }
115
- });
116
-
117
- wss = new WebSocketServer({ server: httpServer });
118
- wss.on("error", (err) => {
119
- log("WebSocket server error:", err.message);
120
- });
121
-
122
- wss.on("connection", (ws) => {
123
- let attachedProfileId = null;
124
-
125
- ws.on("message", (raw) => {
126
- try {
127
- const data = JSON.parse(raw.toString());
128
-
129
- if (data.type === "register") {
130
- attachedProfileId = data.profileId || `profile_${Date.now()}`;
131
- activeProfiles.set(attachedProfileId, {
132
- id: attachedProfileId,
133
- name: data.profileName || "Browser Profile",
134
- browser: data.browser || "Chromium",
135
- ws,
136
- lastActive: Date.now(),
137
- connectedAt: data.timestamp || Date.now(),
138
- });
139
- log(`Profile Connected: "${data.profileName}" (${attachedProfileId}) [${data.browser}]`);
140
- return;
141
- }
142
-
143
- if (data.type === "heartbeat") {
144
- if (data.profileId && activeProfiles.has(data.profileId)) {
145
- const profile = activeProfiles.get(data.profileId);
146
- profile.lastActive = Date.now();
147
- if (data.profileName) profile.name = data.profileName;
148
- }
149
- return;
150
- }
151
-
152
- if (data.id && pendingRequests.has(data.id)) {
153
- const { resolve, reject, timer } = pendingRequests.get(data.id);
154
- clearTimeout(timer);
155
- pendingRequests.delete(data.id);
156
-
157
- if (attachedProfileId && activeProfiles.has(attachedProfileId)) {
158
- activeProfiles.get(attachedProfileId).lastActive = Date.now();
159
- }
160
-
161
- if (data.error) reject(new Error(data.error));
162
- else resolve(data.result);
163
- }
164
- } catch (err) {
165
- log("Error processing WebSocket message:", err);
166
- }
167
- });
168
-
169
- ws.on("close", () => {
170
- if (attachedProfileId && activeProfiles.has(attachedProfileId)) {
171
- const profile = activeProfiles.get(attachedProfileId);
172
- log(`Profile Disconnected: "${profile.name}" (${attachedProfileId})`);
173
- activeProfiles.delete(attachedProfileId);
174
- }
175
- });
176
-
177
- ws.on("error", (err) => {
178
- log("WebSocket client error:", err.message);
179
- });
180
- });
181
-
182
- httpServer.listen(PORT, "127.0.0.1", () => {
183
- isMasterServer = true;
184
- log(`Master Bridge listening on http://127.0.0.1:${PORT} (WebSocket + HTTP Relay)`);
185
- });
186
- }
187
-
188
- async function ensureMasterOrRelay() {
189
- if (isMasterServer && httpServer?.listening) return true;
190
- const alive = await checkMasterAlive();
191
- if (alive) {
192
- isMasterServer = false;
193
- return false;
194
- }
195
- startMasterServer();
196
- await new Promise((r) => setTimeout(r, 400));
197
- return isMasterServer;
198
- }
199
-
200
- // Initialize Master or Relay
8
+ // 1. Initialize Master Server or Relay Client
201
9
  await ensureMasterOrRelay();
202
-
203
- // Master/Relay Watchdog: If Relay and Master goes down, promote to Master automatically
204
- setInterval(async () => {
205
- if (!isMasterServer) {
206
- const alive = await checkMasterAlive();
207
- if (!alive) {
208
- log("Master server offline. Auto-promoting this instance to Master Bridge...");
209
- await ensureMasterOrRelay();
210
- }
211
- }
212
- }, 3500);
213
-
214
- // Optional standalone browser spawn fallback
215
- function resolveChromeBinary() {
216
- if (process.env.CHROME_BIN) return process.env.CHROME_BIN;
217
- if (process.platform === "win32") {
218
- const candidates = [
219
- "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
220
- "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
221
- path.join(process.env.LOCALAPPDATA || "", "Google\\Chrome\\Application\\chrome.exe"),
222
- ];
223
- return candidates.find((p) => fs.existsSync(p)) || candidates[0];
224
- }
225
- if (process.platform === "darwin") {
226
- return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
227
- }
228
- return "google-chrome";
229
- }
230
-
231
- function spawnIsolatedBrowser() {
232
- if (browserProcess) return;
233
- const profileDir = path.join(os.homedir(), ".agent-browser-profile");
234
- const chromeBin = resolveChromeBinary();
235
- log(`Spawning isolated fallback Chrome instance at ${profileDir}...`);
236
-
237
- browserProcess = spawn(
238
- chromeBin,
239
- [
240
- `--user-data-dir=${profileDir}`,
241
- `--load-extension=${EXTENSION_PATH}`,
242
- "--silent-debugger-extension-api",
243
- "--no-first-run",
244
- "--no-default-browser-check",
245
- "about:blank",
246
- ],
247
- { detached: true, stdio: "ignore" }
248
- );
249
- browserProcess.unref();
250
- }
251
-
252
- async function waitForProfile(timeoutMs = 10000) {
253
- if (activeProfiles.size > 0) return;
254
- if (AUTO_SPAWN && !browserProcess) spawnIsolatedBrowser();
255
-
256
- const start = Date.now();
257
- while (Date.now() - start < timeoutMs) {
258
- if (activeProfiles.size > 0) return;
259
- await new Promise((r) => setTimeout(r, 250));
260
- }
261
- }
262
-
263
- async function getTargetProfile(requestedProfile) {
264
- await waitForProfile();
265
-
266
- if (activeProfiles.size === 0) {
267
- throw new Error(
268
- `No browser profile with "Agent Bridge MCP" extension is currently connected on ws://127.0.0.1:${PORT}.\n` +
269
- `Please open your browser (Chrome/Edge/Brave) where the extension is loaded and ensure the popup status shows "Connected".`
270
- );
271
- }
272
-
273
- if (requestedProfile) {
274
- const req = requestedProfile.toLowerCase().trim();
275
- if (activeProfiles.has(requestedProfile)) {
276
- return activeProfiles.get(requestedProfile);
277
- }
278
- for (const profile of activeProfiles.values()) {
279
- if (profile.name.toLowerCase() === req || profile.id.toLowerCase() === req) {
280
- return profile;
281
- }
282
- }
283
- for (const profile of activeProfiles.values()) {
284
- if (profile.name.toLowerCase().includes(req)) {
285
- return profile;
286
- }
287
- }
288
- const available = Array.from(activeProfiles.values())
289
- .map((p) => `"${p.name}" (ID: ${p.id})`)
290
- .join(", ");
291
- throw new Error(`Profile "${requestedProfile}" not found. Available profiles: ${available}`);
292
- }
293
-
294
- if (activeProfiles.size === 1) {
295
- return activeProfiles.values().next().value;
296
- }
297
-
298
- let mostRecent = null;
299
- for (const profile of activeProfiles.values()) {
300
- if (!mostRecent || profile.lastActive > mostRecent.lastActive) {
301
- mostRecent = profile;
302
- }
303
- }
304
- return mostRecent;
305
- }
306
-
307
- async function callProfileExtensionInternal(profileQuery, action, params = {}, timeoutMs = 25000) {
308
- const profile = await getTargetProfile(profileQuery);
309
-
310
- return new Promise((resolve, reject) => {
311
- if (!profile.ws || profile.ws.readyState !== 1) {
312
- return reject(new Error(`Browser profile "${profile.name}" WebSocket is not open.`));
313
- }
314
-
315
- const id = crypto.randomUUID();
316
- const timer = setTimeout(() => {
317
- pendingRequests.delete(id);
318
- reject(new Error(`Extension request timed out (${action}) on profile "${profile.name}".`));
319
- }, timeoutMs);
320
-
321
- pendingRequests.set(id, { resolve, reject, timer });
322
- profile.ws.send(JSON.stringify({ id, action, params }));
323
- });
324
- }
325
-
326
- async function callProfileExtension(profileQuery, action, params = {}, timeoutMs = 25000) {
327
- // If running as a Relay Client in a secondary Antigravity window/instance,
328
- // forward the request to the Master instance over HTTP RPC!
329
- if (!isMasterServer) {
330
- try {
331
- const res = await fetch(`http://127.0.0.1:${PORT}/rpc`, {
332
- method: "POST",
333
- headers: { "Content-Type": "application/json" },
334
- body: JSON.stringify({ profile: profileQuery, action, params }),
335
- signal: AbortSignal.timeout(timeoutMs),
336
- });
337
- const data = await res.json();
338
- if (!data.success) throw new Error(data.error);
339
- return data.result;
340
- } catch (err) {
341
- log(`Relay call failed (${err.message}). Checking Master health...`);
342
- const masterStillAlive = await checkMasterAlive();
343
- if (!masterStillAlive) {
344
- log("Master server died. Promoting this instance to Master Bridge...");
345
- startMasterServer();
346
- await new Promise((r) => setTimeout(r, 600));
347
- if (isMasterServer) {
348
- return callProfileExtensionInternal(profileQuery, action, params, timeoutMs);
349
- }
350
- }
351
- throw err;
352
- }
353
- }
354
-
355
- return callProfileExtensionInternal(profileQuery, action, params, timeoutMs);
356
- }
357
-
358
- function jsonResult(data) {
359
- return { content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] };
360
- }
10
+ initMasterRelayWatchdog();
361
11
 
362
12
  // 2. Initialize MCP Server
363
13
  const server = new McpServer({
364
14
  name: "custom-browser-bridge",
365
- version: "2.0.0",
15
+ version: "2.2.0",
366
16
  });
367
17
 
368
- // ---------------------------------------------------------------------------
369
- // Profiles & Tabs
370
- // ---------------------------------------------------------------------------
371
- server.registerTool(
372
- "browser_list_profiles",
373
- {
374
- description: "List all currently connected browser profiles (e.g. Work, Personal, Brave, Edge), their IDs, names, and active status.",
375
- },
376
- async () => {
377
- await ensureMasterOrRelay();
378
- if (!isMasterServer) {
379
- try {
380
- const res = await fetch(`http://127.0.0.1:${PORT}/profiles`, {
381
- signal: AbortSignal.timeout(5000),
382
- });
383
- const data = await res.json();
384
- return jsonResult(data);
385
- } catch (err) {
386
- log(`Relay profile fetch failed: ${err.message}`);
387
- await ensureMasterOrRelay();
388
- }
389
- }
390
-
391
- const list = Array.from(activeProfiles.values()).map((p) => ({
392
- id: p.id,
393
- name: p.name,
394
- browser: p.browser,
395
- connectedAt: new Date(p.connectedAt).toISOString(),
396
- lastActive: new Date(p.lastActive).toISOString(),
397
- }));
398
- return jsonResult({
399
- count: list.length,
400
- profiles: list,
401
- hint: list.length > 1 ? "Specify 'profile' (name or ID) in browser commands to target a specific profile." : undefined,
402
- });
403
- }
404
- );
405
-
406
- server.registerTool(
407
- "browser_list_tabs",
408
- {
409
- description: "List all open tabs across browser windows in the profile, including tabId, URL, title, active state, and debugger attachment.",
410
- inputSchema: { profile: z.string().optional().describe("Optional profile name or ID") },
411
- },
412
- async ({ profile }) => jsonResult(await callProfileExtension(profile, "list_tabs"))
413
- );
414
-
415
- server.registerTool(
416
- "browser_switch_tab",
417
- {
418
- description: "Focus and switch to a specific tab by tabId, bringing its browser window to the foreground.",
419
- inputSchema: {
420
- tabId: z.number().describe("The ID of the tab to activate"),
421
- profile: z.string().optional().describe("Optional profile name or ID"),
422
- },
423
- },
424
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "switch_tab", { tabId }))
425
- );
426
-
427
- server.registerTool(
428
- "browser_new_tab",
429
- {
430
- description: "Open a new tab with an optional URL.",
431
- inputSchema: {
432
- url: z.string().optional().describe("URL to open (defaults to 'about:blank')"),
433
- active: z.boolean().optional().describe("Whether to make the new tab active (default true)"),
434
- profile: z.string().optional().describe("Optional profile name or ID"),
435
- },
436
- },
437
- async ({ url, active, profile }) => jsonResult(await callProfileExtension(profile, "new_tab", { url, active }))
438
- );
439
-
440
- server.registerTool(
441
- "browser_close_tab",
442
- {
443
- description: "Close a specific tab by tabId.",
444
- inputSchema: {
445
- tabId: z.number().describe("The ID of the tab to close"),
446
- profile: z.string().optional().describe("Optional profile name or ID"),
447
- },
448
- },
449
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "close_tab", { tabId }))
450
- );
451
-
452
- server.registerTool(
453
- "browser_navigate",
454
- {
455
- description: "Open a new tab or navigate an existing tabId to a specified URL.",
456
- inputSchema: {
457
- url: z.string().describe("The URL to open or navigate to"),
458
- tabId: z.number().optional().describe("Omit to open a new tab, or provide tabId to navigate existing tab"),
459
- active: z.boolean().optional().describe("Whether to make the tab active (default true)"),
460
- profile: z.string().optional().describe("Optional profile name or ID"),
461
- },
462
- },
463
- async ({ url, tabId, active, profile }) => jsonResult(await callProfileExtension(profile, "navigate_or_open", { url, tabId, active }))
464
- );
465
-
466
- server.registerTool(
467
- "browser_reload",
468
- {
469
- description: "Reload a specific tab with optional cache bypass.",
470
- inputSchema: {
471
- tabId: z.number().describe("The ID of the tab to reload"),
472
- ignoreCache: z.boolean().optional().describe("Bypass browser cache (hard reload)"),
473
- profile: z.string().optional().describe("Optional profile name or ID"),
474
- },
475
- },
476
- async ({ tabId, ignoreCache, profile }) => jsonResult(await callProfileExtension(profile, "reload_tab", { tabId, ignoreCache }))
477
- );
478
-
479
- server.registerTool(
480
- "browser_read_all_tabs",
481
- {
482
- description: "Extract visible text and metadata from every open tab in the browser profile at once.",
483
- inputSchema: {
484
- maxLengthPerTab: z.number().optional().describe("Max characters to extract per tab (default 4000)"),
485
- profile: z.string().optional().describe("Optional profile name or ID"),
486
- },
487
- },
488
- async ({ maxLengthPerTab, profile }) => jsonResult(await callProfileExtension(profile, "read_all_tabs", { maxLength: maxLengthPerTab }))
489
- );
490
-
491
- // ---------------------------------------------------------------------------
492
- // DOM, Snapshots, Accessibility & CSS
493
- // ---------------------------------------------------------------------------
494
- server.registerTool(
495
- "browser_get_interactive_snapshot",
496
- {
497
- description: "Assigns data-agent-ref IDs (ref_1, ref_2...) to all visible interactive elements in a tab and returns a structured map of clickable buttons, inputs, links, and coordinates.",
498
- inputSchema: {
499
- tabId: z.number().describe("The tabId to inspect"),
500
- profile: z.string().optional().describe("Optional profile name or ID"),
501
- },
502
- },
503
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "get_interactive_snapshot", { tabId }))
504
- );
505
-
506
- server.registerTool(
507
- "browser_take_snapshot",
508
- {
509
- description: "Take an accessibility (a11y) tree text snapshot of the page listing elements, roles, names, and node IDs.",
510
- inputSchema: {
511
- tabId: z.number().describe("The tabId to snapshot"),
512
- verbose: z.boolean().optional().describe("Include full detailed a11y tree"),
513
- profile: z.string().optional().describe("Optional profile name or ID"),
514
- },
515
- },
516
- async ({ tabId, verbose, profile }) => jsonResult(await callProfileExtension(profile, "take_snapshot", { tabId, verbose }))
517
- );
518
-
519
- server.registerTool(
520
- "browser_get_css_styles",
521
- {
522
- description: "Inspect computed CSS styles for an element specified by ref (e.g. 'ref_4') or CSS selector.",
523
- inputSchema: {
524
- tabId: z.number().describe("The tabId where the element is located"),
525
- target: z.string().describe("Element ref (e.g. 'ref_5') or CSS selector (e.g. 'button.primary')"),
526
- properties: z.array(z.string()).optional().describe("Specific CSS properties to retrieve (defaults to essential visual styles)"),
527
- profile: z.string().optional().describe("Optional profile name or ID"),
528
- },
529
- },
530
- async ({ tabId, target, properties, profile }) => jsonResult(await callProfileExtension(profile, "get_css_styles", { tabId, target, properties }))
531
- );
532
-
533
- server.registerTool(
534
- "browser_wait_for",
535
- {
536
- description: "Wait for specific text or a CSS selector to appear on the page.",
537
- inputSchema: {
538
- tabId: z.number().describe("The tabId to watch"),
539
- text: z.union([z.string(), z.array(z.string())]).optional().describe("One or more texts to wait for"),
540
- selector: z.string().optional().describe("CSS selector to wait for"),
541
- timeout: z.number().optional().describe("Timeout in ms (default 10000ms)"),
542
- profile: z.string().optional().describe("Optional profile name or ID"),
543
- },
544
- },
545
- async ({ tabId, text, selector, timeout, profile }) => jsonResult(await callProfileExtension(profile, "wait_for", { tabId, text, selector, timeout }))
546
- );
547
-
548
- // ---------------------------------------------------------------------------
549
- // Precision Input & Interaction
550
- // ---------------------------------------------------------------------------
551
- server.registerTool(
552
- "browser_act_on_ref",
553
- {
554
- description: "Click or type into a specific data-agent-ref element from browser_get_interactive_snapshot using trusted CDP hardware events and rendering a glowing visual action overlay.",
555
- inputSchema: {
556
- tabId: z.number().describe("The tabId where the element is located"),
557
- ref: z.string().describe("The ref ID, e.g. 'ref_14'"),
558
- actionType: z.enum(["click", "type"]).describe("Action to perform: 'click' or 'type'"),
559
- text: z.string().optional().describe("Text to type if actionType is 'type'"),
560
- clearFirst: z.boolean().optional().describe("Clear existing input value before typing"),
561
- pressEnter: z.boolean().optional().describe("Press Enter key after typing"),
562
- profile: z.string().optional().describe("Optional profile name or ID"),
563
- },
564
- },
565
- async ({ tabId, ref, actionType, text, clearFirst, pressEnter, profile }) =>
566
- jsonResult(await callProfileExtension(profile, "act_on_ref", { tabId, ref, actionType, text, clearFirst, pressEnter }))
567
- );
568
-
569
- server.registerTool(
570
- "browser_click",
571
- {
572
- description: "Click an element (by ref, CSS selector, or coordinates) using trusted hardware CDP events.",
573
- inputSchema: {
574
- tabId: z.number().describe("The tabId"),
575
- target: z.string().optional().describe("Ref (e.g. 'ref_3') or CSS selector (e.g. '#submit')"),
576
- x: z.number().optional().describe("Explicit X coordinate"),
577
- y: z.number().optional().describe("Explicit Y coordinate"),
578
- button: z.enum(["left", "right", "middle"]).optional().describe("Mouse button (default 'left')"),
579
- clickCount: z.number().optional().describe("Click count: 1 for single click, 2 for double click"),
580
- profile: z.string().optional().describe("Optional profile name or ID"),
581
- },
582
- },
583
- async ({ tabId, target, x, y, button, clickCount, profile }) =>
584
- jsonResult(await callProfileExtension(profile, "click", { tabId, target, x, y, button, clickCount }))
585
- );
586
-
587
- server.registerTool(
588
- "browser_hover",
589
- {
590
- description: "Hover over an element (by ref, selector, or coordinates) triggering mouse hover states.",
591
- inputSchema: {
592
- tabId: z.number().describe("The tabId"),
593
- target: z.string().optional().describe("Ref (e.g. 'ref_2') or CSS selector"),
594
- x: z.number().optional().describe("Explicit X coordinate"),
595
- y: z.number().optional().describe("Explicit Y coordinate"),
596
- profile: z.string().optional().describe("Optional profile name or ID"),
597
- },
598
- },
599
- async ({ tabId, target, x, y, profile }) => jsonResult(await callProfileExtension(profile, "hover", { tabId, target, x, y }))
600
- );
601
-
602
- server.registerTool(
603
- "browser_drag",
604
- {
605
- description: "Drag an element or coordinate onto another element or coordinate.",
606
- inputSchema: {
607
- tabId: z.number().describe("The tabId"),
608
- fromTarget: z.string().optional().describe("Source ref or selector"),
609
- toTarget: z.string().optional().describe("Destination ref or selector"),
610
- fromX: z.number().optional().describe("Source X coordinate"),
611
- fromY: z.number().optional().describe("Source Y coordinate"),
612
- toX: z.number().optional().describe("Destination X coordinate"),
613
- toY: z.number().optional().describe("Destination Y coordinate"),
614
- profile: z.string().optional().describe("Optional profile name or ID"),
615
- },
616
- },
617
- async (params) => jsonResult(await callProfileExtension(params.profile, "drag", params))
618
- );
619
-
620
- server.registerTool(
621
- "browser_fill",
622
- {
623
- description: "Fill an input or textarea element with text (focuses, clears existing text, and types value).",
624
- inputSchema: {
625
- tabId: z.number().describe("The tabId"),
626
- target: z.string().describe("Ref (e.g. 'ref_4') or CSS selector (e.g. 'input[name=\"email\"]')"),
627
- value: z.string().describe("Text value to fill into the input"),
628
- profile: z.string().optional().describe("Optional profile name or ID"),
629
- },
630
- },
631
- async ({ tabId, target, value, profile }) => jsonResult(await callProfileExtension(profile, "fill", { tabId, target, value }))
632
- );
633
-
634
- server.registerTool(
635
- "browser_fill_form",
636
- {
637
- description: "Fill multiple form inputs at once given a key-value mapping of targets to values.",
638
- inputSchema: {
639
- tabId: z.number().describe("The tabId"),
640
- fields: z.record(z.string()).describe("Object of { selector_or_ref: value }, e.g. { '#username': 'admin', '#password': 'pass' }"),
641
- profile: z.string().optional().describe("Optional profile name or ID"),
642
- },
643
- },
644
- async ({ tabId, fields, profile }) => jsonResult(await callProfileExtension(profile, "fill_form", { tabId, fields }))
645
- );
646
-
647
- server.registerTool(
648
- "browser_press_key",
649
- {
650
- description: "Press a keyboard key or shortcut (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown').",
651
- inputSchema: {
652
- tabId: z.number().describe("The tabId"),
653
- key: z.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape', 'a'"),
654
- code: z.string().optional().describe("Optional key code, e.g. 'Enter', 'KeyA'"),
655
- profile: z.string().optional().describe("Optional profile name or ID"),
656
- },
657
- },
658
- async ({ tabId, key, code, profile }) => jsonResult(await callProfileExtension(profile, "press_key", { tabId, key, code }))
659
- );
660
-
661
- server.registerTool(
662
- "browser_type_text",
663
- {
664
- description: "Type text into the currently focused element or into a specific target element.",
665
- inputSchema: {
666
- tabId: z.number().describe("The tabId"),
667
- text: z.string().describe("Text string to type"),
668
- target: z.string().optional().describe("Optional ref or selector to focus before typing"),
669
- profile: z.string().optional().describe("Optional profile name or ID"),
670
- },
671
- },
672
- async ({ tabId, text, target, profile }) => jsonResult(await callProfileExtension(profile, "type_text", { tabId, text, target }))
673
- );
674
-
675
- server.registerTool(
676
- "browser_upload_file",
677
- {
678
- description: "Upload local file(s) into a file input element.",
679
- inputSchema: {
680
- tabId: z.number().describe("The tabId"),
681
- target: z.string().describe("Ref (e.g. 'ref_8') or selector for the file input element"),
682
- filePaths: z.union([z.string(), z.array(z.string())]).describe("Absolute local path(s) to the file(s) to upload"),
683
- profile: z.string().optional().describe("Optional profile name or ID"),
684
- },
685
- },
686
- async ({ tabId, target, filePaths, profile }) => jsonResult(await callProfileExtension(profile, "upload_file", { tabId, target, filePaths }))
687
- );
688
-
689
- server.registerTool(
690
- "browser_handle_dialog",
691
- {
692
- description: "Accept or dismiss a JavaScript alert, confirm, prompt, or beforeunload dialog.",
693
- inputSchema: {
694
- tabId: z.number().describe("The tabId"),
695
- action: z.enum(["accept", "dismiss"]).describe("Action to take on dialog: 'accept' or 'dismiss'"),
696
- promptText: z.string().optional().describe("Optional text to enter into a prompt dialog"),
697
- profile: z.string().optional().describe("Optional profile name or ID"),
698
- },
699
- },
700
- async ({ tabId, action, promptText, profile }) => jsonResult(await callProfileExtension(profile, "handle_dialog", { tabId, action, promptText }))
701
- );
702
-
703
- // ---------------------------------------------------------------------------
704
- // Emulation & Viewport
705
- // ---------------------------------------------------------------------------
706
- server.registerTool(
707
- "browser_resize_page",
708
- {
709
- description: "Set viewport width, height, deviceScaleFactor, and mobile emulation.",
710
- inputSchema: {
711
- tabId: z.number().describe("The tabId"),
712
- width: z.number().describe("Viewport width in pixels (e.g. 1280, 390)"),
713
- height: z.number().describe("Viewport height in pixels (e.g. 800, 844)"),
714
- deviceScaleFactor: z.number().optional().describe("Device scale factor (e.g. 1, 2)"),
715
- mobile: z.boolean().optional().describe("Whether to emulate mobile screen"),
716
- profile: z.string().optional().describe("Optional profile name or ID"),
717
- },
718
- },
719
- async ({ tabId, width, height, deviceScaleFactor, mobile, profile }) =>
720
- jsonResult(await callProfileExtension(profile, "resize_page", { tabId, width, height, deviceScaleFactor, mobile }))
721
- );
722
-
723
- server.registerTool(
724
- "browser_emulate",
725
- {
726
- description: "Emulate color scheme, CPU throttling, network conditions, geolocation, or user agent.",
727
- inputSchema: {
728
- tabId: z.number().describe("The tabId"),
729
- colorScheme: z.enum(["dark", "light", "auto"]).optional().describe("Emulate dark or light color scheme"),
730
- cpuThrottlingRate: z.number().optional().describe("CPU slowdown factor (1 to 20, 1 disables)"),
731
- networkConditions: z.enum(["Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G"]).optional().describe("Network throttling profile"),
732
- geolocation: z.string().optional().describe("Geolocation as 'latitude,longitude', e.g. '37.7749,-122.4194'"),
733
- userAgent: z.string().optional().describe("Custom user agent override string"),
734
- profile: z.string().optional().describe("Optional profile name or ID"),
735
- },
736
- },
737
- async ({ tabId, colorScheme, cpuThrottlingRate, networkConditions, geolocation, userAgent, profile }) =>
738
- jsonResult(await callProfileExtension(profile, "emulate", { tabId, colorScheme, cpuThrottlingRate, networkConditions, geolocation, userAgent }))
739
- );
740
-
741
- // ---------------------------------------------------------------------------
742
- // Network, Cookies & Storage
743
- // ---------------------------------------------------------------------------
744
- server.registerTool(
745
- "browser_list_network_requests",
746
- {
747
- description: "List recorded network requests for a tab (status, method, url, resourceType, mimeType, timestamp).",
748
- inputSchema: {
749
- tabId: z.number().describe("The tabId to inspect"),
750
- profile: z.string().optional().describe("Optional profile name or ID"),
751
- },
752
- },
753
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "list_network_requests", { tabId }))
754
- );
755
-
756
- server.registerTool(
757
- "browser_get_network_request",
758
- {
759
- description: "Get full details and response body of a specific network request by requestId.",
760
- inputSchema: {
761
- tabId: z.number().describe("The tabId where the request was made"),
762
- requestId: z.string().describe("The requestId from network request list"),
763
- profile: z.string().optional().describe("Optional profile name or ID"),
764
- },
765
- },
766
- async ({ tabId, requestId, profile }) => jsonResult(await callProfileExtension(profile, "get_network_request", { tabId, requestId }))
767
- );
768
-
769
- server.registerTool(
770
- "browser_get_network_body",
771
- {
772
- description: "Fetch the raw response body of a specific network requestId obtained from browser_get_telemetry or network requests.",
773
- inputSchema: {
774
- tabId: z.number().describe("The tabId"),
775
- requestId: z.string().describe("The requestId"),
776
- profile: z.string().optional().describe("Optional profile name or ID"),
777
- },
778
- },
779
- async ({ tabId, requestId, profile }) => jsonResult(await callProfileExtension(profile, "get_response_body", { tabId, requestId }))
780
- );
781
-
782
- server.registerTool(
783
- "browser_get_cookies",
784
- {
785
- description: "Retrieve all cookies for the current tab's active URL.",
786
- inputSchema: {
787
- tabId: z.number().describe("The tabId"),
788
- profile: z.string().optional().describe("Optional profile name or ID"),
789
- },
790
- },
791
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "get_cookies", { tabId }))
792
- );
793
-
794
- server.registerTool(
795
- "browser_set_cookie",
796
- {
797
- description: "Set a cookie on the active tab's domain.",
798
- inputSchema: {
799
- tabId: z.number().describe("The tabId"),
800
- name: z.string().describe("Cookie name"),
801
- value: z.string().describe("Cookie value"),
802
- url: z.string().optional().describe("Target URL (defaults to tab URL)"),
803
- domain: z.string().optional().describe("Cookie domain"),
804
- path: z.string().optional().describe("Cookie path (defaults to '/')"),
805
- secure: z.boolean().optional().describe("Secure cookie flag (default true)"),
806
- httpOnly: z.boolean().optional().describe("HttpOnly cookie flag (default false)"),
807
- profile: z.string().optional().describe("Optional profile name or ID"),
808
- },
809
- },
810
- async (params) => jsonResult(await callProfileExtension(params.profile, "set_cookie", params))
811
- );
812
-
813
- server.registerTool(
814
- "browser_clear_cookies",
815
- {
816
- description: "Clear all cookies for the browser profile.",
817
- inputSchema: {
818
- tabId: z.number().describe("The tabId"),
819
- profile: z.string().optional().describe("Optional profile name or ID"),
820
- },
821
- },
822
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "clear_cookies", { tabId }))
823
- );
824
-
825
- server.registerTool(
826
- "browser_storage",
827
- {
828
- description: "Inspect or manipulate localStorage or sessionStorage in a tab.",
829
- inputSchema: {
830
- tabId: z.number().describe("The tabId"),
831
- operation: z.enum(["get", "set", "remove", "clear"]).describe("Storage operation to perform"),
832
- key: z.string().optional().describe("Storage key name"),
833
- value: z.string().optional().describe("Value to set (if operation is 'set')"),
834
- storageType: z.enum(["localStorage", "sessionStorage"]).optional().describe("Type of storage (default 'localStorage')"),
835
- profile: z.string().optional().describe("Optional profile name or ID"),
836
- },
837
- },
838
- async ({ tabId, operation, key, value, storageType, profile }) =>
839
- jsonResult(await callProfileExtension(profile, "storage", { tabId, operation, key, value, storageType }))
840
- );
841
-
842
- // ---------------------------------------------------------------------------
843
- // Console, Telemetry & Diagnostics
844
- // ---------------------------------------------------------------------------
845
- server.registerTool(
846
- "browser_list_console_messages",
847
- {
848
- description: "List captured console logs, warnings, errors, and uncaught exceptions with timestamps and stack traces.",
849
- inputSchema: {
850
- tabId: z.number().describe("The tabId to inspect"),
851
- level: z.enum(["log", "info", "warn", "error"]).optional().describe("Filter by message level"),
852
- profile: z.string().optional().describe("Optional profile name or ID"),
853
- },
854
- },
855
- async ({ tabId, level, profile }) => jsonResult(await callProfileExtension(profile, "list_console_messages", { tabId, level }))
856
- );
857
-
858
- server.registerTool(
859
- "browser_get_telemetry",
860
- {
861
- description: "Attach CDP debugger to a tab and retrieve buffered console logs, uncaught exceptions, dialogs, and recorded network requests.",
862
- inputSchema: {
863
- tabId: z.number().describe("The tabId to inspect"),
864
- clearAfterRead: z.boolean().optional().describe("Clear buffered telemetry after reading (default false)"),
865
- profile: z.string().optional().describe("Optional profile name or ID"),
866
- },
867
- },
868
- async ({ tabId, clearAfterRead, profile }) => jsonResult(await callProfileExtension(profile, "get_telemetry", { tabId, clearAfterRead }))
869
- );
870
-
871
- // ---------------------------------------------------------------------------
872
- // Performance, Memory & Screenshots
873
- // ---------------------------------------------------------------------------
874
- server.registerTool(
875
- "browser_screenshot",
876
- {
877
- description: "Capture a PNG screenshot of a specific tab.",
878
- inputSchema: {
879
- tabId: z.number().describe("The tabId to screenshot"),
880
- profile: z.string().optional().describe("Optional profile name or ID"),
881
- },
882
- },
883
- async ({ tabId, profile }) => {
884
- const { base64Png } = await callProfileExtension(profile, "screenshot", { tabId });
885
- return {
886
- content: [{ type: "image", data: base64Png, mimeType: "image/png" }],
887
- };
888
- }
889
- );
890
-
891
- server.registerTool(
892
- "browser_take_heapsnapshot",
893
- {
894
- description: "Take a V8 heap snapshot for memory leak analysis.",
895
- inputSchema: {
896
- tabId: z.number().describe("The tabId"),
897
- profile: z.string().optional().describe("Optional profile name or ID"),
898
- },
899
- },
900
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "take_heapsnapshot", { tabId }))
901
- );
902
-
903
- server.registerTool(
904
- "browser_performance_trace",
905
- {
906
- description: "Start or stop a Chrome performance timeline trace.",
907
- inputSchema: {
908
- tabId: z.number().describe("The tabId"),
909
- action: z.enum(["start", "stop"]).describe("Trace action: 'start' or 'stop'"),
910
- profile: z.string().optional().describe("Optional profile name or ID"),
911
- },
912
- },
913
- async ({ tabId, action, profile }) => {
914
- const rpc = action === "start" ? "performance_start_trace" : "performance_stop_trace";
915
- return jsonResult(await callProfileExtension(profile, rpc, { tabId }));
916
- }
917
- );
918
-
919
- server.registerTool(
920
- "browser_performance_analyze_insight",
921
- {
922
- description: "Extract Core Web Vitals and navigation performance timings (TTFB, FCP, DOMContentLoaded, LoadTime).",
923
- inputSchema: {
924
- tabId: z.number().describe("The tabId to analyze"),
925
- profile: z.string().optional().describe("Optional profile name or ID"),
926
- },
927
- },
928
- async ({ tabId, profile }) => jsonResult(await callProfileExtension(profile, "performance_analyze_insight", { tabId }))
929
- );
930
-
931
- // ---------------------------------------------------------------------------
932
- // Execution & Raw CDP Escape Hatch
933
- // ---------------------------------------------------------------------------
934
- server.registerTool(
935
- "browser_eval_js",
936
- {
937
- description: "Execute arbitrary JavaScript or DOM inspection inside any tab via CDP Runtime.evaluate.",
938
- inputSchema: {
939
- tabId: z.number().describe("The tabId to run code in"),
940
- expression: z.string().describe("JavaScript code or expression to evaluate"),
941
- profile: z.string().optional().describe("Optional profile name or ID"),
942
- },
943
- },
944
- async ({ tabId, expression, profile }) => jsonResult(await callProfileExtension(profile, "eval_js", { tabId, expression }))
945
- );
946
-
947
- server.registerTool(
948
- "browser_raw_cdp",
949
- {
950
- description: "Execute any raw Chrome DevTools Protocol (CDP) method on a tab (e.g. DOM.getDocument, Input.dispatchMouseEvent, Network.getCookies).",
951
- inputSchema: {
952
- tabId: z.number().describe("The tabId to control"),
953
- method: z.string().describe("CDP method name, e.g. 'DOM.getDocument' or 'Network.getCookies'"),
954
- commandParams: z.record(z.any()).optional().describe("Optional parameters object for the CDP command"),
955
- profile: z.string().optional().describe("Optional profile name or ID"),
956
- },
957
- },
958
- async ({ tabId, method, commandParams, profile }) => jsonResult(await callProfileExtension(profile, "raw_cdp", { tabId, method, commandParams }))
959
- );
18
+ // 3. Register All 68 Tools Across Modules
19
+ registerAllTools(server);
960
20
 
961
- // Connect MCP stdio transport
21
+ // 4. Start Transport
962
22
  const transport = new StdioServerTransport();
963
23
  await server.connect(transport);
964
- log("Agent Bridge MCP Server v2.0 connected and ready with full DevTools capabilities!");