agent-browser-bridge 0.0.0-stage → 2.0.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 +124 -2
- package/bin/cli.js +8 -0
- package/index.js +964 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
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 (39 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_navigate`: Navigate an existing tab or open a new URL.
|
|
65
|
+
- `browser_reload`: Reload tab with optional cache bypass.
|
|
66
|
+
- `browser_read_all_tabs`: Extract visible text and metadata from every open tab at once.
|
|
67
|
+
|
|
68
|
+
### DOM, Visual Refs & CSS Styling
|
|
69
|
+
- `browser_get_interactive_snapshot`: Map interactive elements with deterministic `data-agent-ref` IDs (`ref_1`, `ref_2`...) and coordinates.
|
|
70
|
+
- `browser_take_snapshot`: Accessibility (a11y) tree text snapshot matching `chrome-devtools-mcp`.
|
|
71
|
+
- `browser_get_css_styles`: Inspect computed CSS styles for any element by ref or selector.
|
|
72
|
+
- `browser_wait_for`: Wait for specific text or CSS selector to appear on the page.
|
|
73
|
+
|
|
74
|
+
### Precision Hardware-Level Input
|
|
75
|
+
- `browser_act_on_ref`: Visual highlight overlay + click or text input on a `data-agent-ref`.
|
|
76
|
+
- `browser_click`: Hardware CDP mouse click (by ref, selector, or x/y coordinates).
|
|
77
|
+
- `browser_hover`: Trigger mouse hover states.
|
|
78
|
+
- `browser_drag`: Drag and drop from source element/coords to destination element/coords.
|
|
79
|
+
- `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`).
|
|
82
|
+
- `browser_type_text`: Type text into current focus or target element.
|
|
83
|
+
- `browser_upload_file`: Upload local file(s) into file input elements (`DOM.setFileInputFiles`).
|
|
84
|
+
- `browser_handle_dialog`: Accept or dismiss JavaScript `alert()`, `confirm()`, or `prompt()` dialogs.
|
|
85
|
+
|
|
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
|
|
91
|
+
- `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.
|
|
94
|
+
- `browser_get_cookies`: Retrieve cookies for a tab's URL.
|
|
95
|
+
- `browser_set_cookie`: Set a cookie (name, value, domain, path, secure, httpOnly).
|
|
96
|
+
- `browser_clear_cookies`: Clear all cookies for the profile.
|
|
97
|
+
- `browser_storage`: Read, write, or clear `localStorage` / `sessionStorage`.
|
|
98
|
+
|
|
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.
|
|
105
|
+
- `browser_take_heapsnapshot`: V8 heap snapshot for memory leak analysis.
|
|
106
|
+
- `browser_performance_trace`: Start or stop Chrome performance timeline trace.
|
|
107
|
+
- `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.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🔒 Security & Privacy
|
|
116
|
+
|
|
117
|
+
- **Local-Only**: Only binds to `127.0.0.1:18922` by default. Never exposes ports to external networks.
|
|
118
|
+
- **Instant Killswitch**: Toggle "Allow Agent Control" off anytime in the extension popup to instantly freeze agent access.
|
|
119
|
+
- **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.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📄 License
|
|
124
|
+
|
|
125
|
+
Apache-2.0
|
package/bin/cli.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
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";
|
|
12
|
+
|
|
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
|
|
201
|
+
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
|
+
}
|
|
361
|
+
|
|
362
|
+
// 2. Initialize MCP Server
|
|
363
|
+
const server = new McpServer({
|
|
364
|
+
name: "custom-browser-bridge",
|
|
365
|
+
version: "2.0.0",
|
|
366
|
+
});
|
|
367
|
+
|
|
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
|
+
);
|
|
960
|
+
|
|
961
|
+
// Connect MCP stdio transport
|
|
962
|
+
const transport = new StdioServerTransport();
|
|
963
|
+
await server.connect(transport);
|
|
964
|
+
log("Agent Bridge MCP Server v2.0 connected and ready with full DevTools capabilities!");
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-browser-bridge",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.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
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"mcp-server",
|
|
23
|
+
"chrome-extension",
|
|
24
|
+
"browser-control",
|
|
25
|
+
"devtools",
|
|
26
|
+
"antigravity",
|
|
27
|
+
"claude",
|
|
28
|
+
"cursor"
|
|
29
|
+
],
|
|
30
|
+
"license": "Apache-2.0",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.6.0",
|
|
33
|
+
"ws": "^8.18.0",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
}
|
|
36
|
+
}
|