claude-browser-bridge 3.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 +64 -0
- package/bin/cli.js +129 -0
- package/gen-token.js +13 -0
- package/index.js +865 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# claude-browser-bridge
|
|
2
|
+
|
|
3
|
+
Connect your **real, logged-in Chrome tabs** to Claude Code for AI-assisted debugging with **57 tools** — DOM snapshots, console/network monitoring, performance tracing, CSS live-injection, visual diffs, device emulation, and more.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Install globally (or use npx)
|
|
9
|
+
npm install -g claude-browser-bridge
|
|
10
|
+
|
|
11
|
+
# 2. Generate auth token
|
|
12
|
+
claude-browser-bridge init
|
|
13
|
+
|
|
14
|
+
# 3. Load the Chrome extension
|
|
15
|
+
# → chrome://extensions → Developer mode → Load unpacked → select extension/ folder
|
|
16
|
+
|
|
17
|
+
# 4. Register with Claude Code
|
|
18
|
+
claude mcp add browser-bridge -- npx claude-browser-bridge
|
|
19
|
+
|
|
20
|
+
# 5. Restart Claude Code and start debugging
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How It Works
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Claude Code ──stdio──► MCP Server ◄──WebSocket──► Chrome Extension ──► Your live tabs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The MCP server runs locally. The Chrome extension connects to it over `ws://localhost:8787`. Claude Code talks to the server over stdio. Everything stays on your machine — no data is sent anywhere.
|
|
30
|
+
|
|
31
|
+
## 57 Tools
|
|
32
|
+
|
|
33
|
+
| Category | Tools |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Core** | `diagnose`, `batch`, `snapshot`, `screenshot`, `full_page_screenshot`, `eval`, `get_page_text`, `get_html` |
|
|
36
|
+
| **Interaction** | `click`, `fill`, `hover`, `scroll`, `press_key`, `select_option`, `upload_file` |
|
|
37
|
+
| **Navigation** | `navigate`, `new_tab`, `close_tab`, `go_back`, `go_forward`, `reload`, `select_tab`, `list_tabs` |
|
|
38
|
+
| **Debugging** | `get_console`, `get_network`, `get_styles`, `get_cookies`, `get_storage`, `get_clipboard`, `watch_dom_changes`, `search_network_bodies` |
|
|
39
|
+
| **Performance** | `performance_trace`, `heap_snapshot_summary`, `get_load_timeline` |
|
|
40
|
+
| **Accessibility** | `get_accessibility_tree`, `check_contrast` |
|
|
41
|
+
| **Emulation** | `emulate_device`, `network_throttle`, `set_geolocation`, `toggle_dark_mode` |
|
|
42
|
+
| **Testing** | `visual_diff`, `inject_css`, `mock_network`, `record_actions`, `replay_actions`, `highlight_element` |
|
|
43
|
+
| **Productivity** | `save_form_profile`, `load_form_profile`, `save_tab_session`, `restore_tab_session`, `generate_selector`, `get_grouped_console` |
|
|
44
|
+
| **Utility** | `get_page_info`, `wait_for`, `handle_dialog`, `export_pdf`, `edit_cookie` |
|
|
45
|
+
|
|
46
|
+
## CLI Commands
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
claude-browser-bridge # Start the MCP server
|
|
50
|
+
claude-browser-bridge init # Generate auth token
|
|
51
|
+
claude-browser-bridge setup # Print Claude Code registration command
|
|
52
|
+
claude-browser-bridge --port 9999 # Use custom WebSocket port
|
|
53
|
+
claude-browser-bridge --help # Show help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
- Node.js 18+
|
|
59
|
+
- Chrome with the Claude Browser Bridge extension loaded
|
|
60
|
+
- Claude Code CLI
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT — Himanshu Kanwar
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Claude Browser Bridge — CLI entry point.
|
|
4
|
+
// Usage:
|
|
5
|
+
// npx claude-browser-bridge → start the MCP server (default)
|
|
6
|
+
// npx claude-browser-bridge init → generate auth token + extension config
|
|
7
|
+
// npx claude-browser-bridge --port 9999 → use a custom WebSocket port
|
|
8
|
+
// npx claude-browser-bridge --help → show usage
|
|
9
|
+
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const SERVER_DIR = resolve(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const hasHelp = args.includes("--help") || args.includes("-h");
|
|
22
|
+
const command = hasHelp ? "help" : (args.find(a => !a.startsWith("-")) || "serve");
|
|
23
|
+
|
|
24
|
+
function printHelp() {
|
|
25
|
+
console.log(`
|
|
26
|
+
Claude Browser Bridge — connect your live Chrome tabs to Claude Code.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
claude-browser-bridge Start the MCP server (stdio mode)
|
|
30
|
+
claude-browser-bridge init [path] Generate auth token + extension/config.js
|
|
31
|
+
claude-browser-bridge setup Print Claude Code registration command
|
|
32
|
+
claude-browser-bridge --help Show this help
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--port <number> WebSocket port (default: 8787, env: BRIDGE_PORT)
|
|
36
|
+
|
|
37
|
+
Quick start:
|
|
38
|
+
1. npx claude-browser-bridge init
|
|
39
|
+
2. Load extension/ folder in chrome://extensions (Developer mode → Load unpacked)
|
|
40
|
+
3. claude mcp add browser-bridge -- npx claude-browser-bridge
|
|
41
|
+
4. Restart Claude Code and start debugging
|
|
42
|
+
|
|
43
|
+
Docs: https://github.com/HimanshuKanwar2001/claude-browser-bridge
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function init() {
|
|
48
|
+
const extPath = args[args.indexOf("init") + 1] || null;
|
|
49
|
+
const tokenPath = join(SERVER_DIR, ".bridge-token");
|
|
50
|
+
const token = randomBytes(24).toString("hex");
|
|
51
|
+
|
|
52
|
+
// Write server token
|
|
53
|
+
writeFileSync(tokenPath, token + "\n");
|
|
54
|
+
console.log(`✓ Token written to ${tokenPath}`);
|
|
55
|
+
|
|
56
|
+
// Write extension config if we can find the extension directory
|
|
57
|
+
const searchPaths = [
|
|
58
|
+
extPath,
|
|
59
|
+
join(SERVER_DIR, "..", "extension"),
|
|
60
|
+
join(process.cwd(), "extension"),
|
|
61
|
+
].filter(Boolean);
|
|
62
|
+
|
|
63
|
+
let written = false;
|
|
64
|
+
for (const dir of searchPaths) {
|
|
65
|
+
if (existsSync(join(dir, "manifest.json"))) {
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(dir, "config.js"),
|
|
68
|
+
`// Generated by claude-browser-bridge init — keep out of version control.\nconst BRIDGE_TOKEN = ${JSON.stringify(token)};\n`
|
|
69
|
+
);
|
|
70
|
+
console.log(`✓ Extension config written to ${join(dir, "config.js")}`);
|
|
71
|
+
written = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!written) {
|
|
77
|
+
console.log(`⚠ Extension directory not found — create extension/config.js manually:`);
|
|
78
|
+
console.log(` const BRIDGE_TOKEN = "${token}";`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`\n✓ Token generated. Reload the extension in chrome://extensions to apply.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setup() {
|
|
85
|
+
// Detect if running from npx or local
|
|
86
|
+
const isGlobal = __dirname.includes("node_modules");
|
|
87
|
+
const cmd = isGlobal
|
|
88
|
+
? "claude mcp add browser-bridge -- npx claude-browser-bridge"
|
|
89
|
+
: `claude mcp add browser-bridge -- node ${join(SERVER_DIR, "index.js")}`;
|
|
90
|
+
|
|
91
|
+
console.log(`\nAdd to Claude Code with:\n`);
|
|
92
|
+
console.log(` ${cmd}\n`);
|
|
93
|
+
console.log(`Or add to ~/.claude.json manually:\n`);
|
|
94
|
+
console.log(` "browser-bridge": {`);
|
|
95
|
+
console.log(` "command": "${isGlobal ? "npx" : "node"}",`);
|
|
96
|
+
console.log(` "args": ${JSON.stringify(isGlobal ? ["claude-browser-bridge"] : [join(SERVER_DIR, "index.js")])}`);
|
|
97
|
+
console.log(` }\n`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Parse --port
|
|
101
|
+
const portIdx = args.indexOf("--port");
|
|
102
|
+
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
103
|
+
process.env.BRIDGE_PORT = args[portIdx + 1];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
switch (command) {
|
|
107
|
+
case "--help":
|
|
108
|
+
case "-h":
|
|
109
|
+
case "help":
|
|
110
|
+
printHelp();
|
|
111
|
+
break;
|
|
112
|
+
case "init":
|
|
113
|
+
init();
|
|
114
|
+
break;
|
|
115
|
+
case "setup":
|
|
116
|
+
setup();
|
|
117
|
+
break;
|
|
118
|
+
case "serve":
|
|
119
|
+
default:
|
|
120
|
+
// Check for token
|
|
121
|
+
const tokenPath = join(SERVER_DIR, ".bridge-token");
|
|
122
|
+
if (!existsSync(tokenPath)) {
|
|
123
|
+
console.error("[bridge] No auth token found. Run 'claude-browser-bridge init' first.");
|
|
124
|
+
console.error("[bridge] Starting anyway — extension connections will be refused until token is generated.");
|
|
125
|
+
}
|
|
126
|
+
// Import and run the server
|
|
127
|
+
await import("../index.js");
|
|
128
|
+
break;
|
|
129
|
+
}
|
package/gen-token.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Generates the shared secret used to authenticate the extension to the
|
|
2
|
+
// bridge: writes server/.bridge-token and extension/config.js.
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { writeFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const token = randomBytes(24).toString("hex");
|
|
7
|
+
writeFileSync(new URL("./.bridge-token", import.meta.url), token + "\n");
|
|
8
|
+
writeFileSync(
|
|
9
|
+
new URL("../extension/config.js", import.meta.url),
|
|
10
|
+
`// Generated by server/gen-token.js — keep out of version control.\nconst BRIDGE_TOKEN = ${JSON.stringify(token)};\n`
|
|
11
|
+
);
|
|
12
|
+
console.log("Wrote server/.bridge-token and extension/config.js");
|
|
13
|
+
console.log("Reload the extension in chrome://extensions to pick up the new token.");
|
package/index.js
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
// Combined MCP stdio server (for Claude Code) + WebSocket bridge (for the
|
|
2
|
+
// Chrome extension). Claude tool calls are proxied to the extension, which
|
|
3
|
+
// executes them on the user's real, logged-in tabs.
|
|
4
|
+
//
|
|
5
|
+
// Multi-session: the first Claude Code session to start binds the port and
|
|
6
|
+
// "owns" the bridge; later sessions detect EADDRINUSE and relay their tool
|
|
7
|
+
// calls through the owner. If the owner exits, a relay takes over the port.
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import {
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
ListToolsRequestSchema,
|
|
15
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
17
|
+
|
|
18
|
+
const PORT = Number(process.env.BRIDGE_PORT) || 8787;
|
|
19
|
+
|
|
20
|
+
let TOKEN = null;
|
|
21
|
+
try {
|
|
22
|
+
TOKEN = readFileSync(new URL("./.bridge-token", import.meta.url), "utf8").trim();
|
|
23
|
+
} catch {}
|
|
24
|
+
if (!TOKEN) {
|
|
25
|
+
console.error(
|
|
26
|
+
"[bridge] WARNING: server/.bridge-token is missing — run `node gen-token.js` " +
|
|
27
|
+
"in the server directory. Refusing all extension connections until then."
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- WebSocket bridge (owner or relay) -------------------------------------
|
|
32
|
+
|
|
33
|
+
const NO_EXT_MSG =
|
|
34
|
+
"Chrome extension is not connected. Make sure Chrome is open with the " +
|
|
35
|
+
"'Claude Code Browser Bridge' extension loaded (chrome://extensions). " +
|
|
36
|
+
"Click the extension icon to check its connection status.";
|
|
37
|
+
|
|
38
|
+
let mode = "starting"; // "owner" | "relay"
|
|
39
|
+
let extension = null; // owner mode: the authenticated extension socket
|
|
40
|
+
let upstream = null; // relay mode: connection to the owning session
|
|
41
|
+
const pending = new Map(); // id -> { resolve, reject, timer }
|
|
42
|
+
let nextId = 1;
|
|
43
|
+
|
|
44
|
+
function addPending(id, resolve, reject, timeoutMs, label) {
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
pending.delete(id);
|
|
47
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
pending.set(id, { resolve, reject, timer });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function settle(id, msg) {
|
|
53
|
+
const p = pending.get(id);
|
|
54
|
+
if (!p) return;
|
|
55
|
+
pending.delete(id);
|
|
56
|
+
clearTimeout(p.timer);
|
|
57
|
+
msg.ok ? p.resolve(msg.result) : p.reject(new Error(msg.error || "unknown bridge error"));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rejectAllPending(reason) {
|
|
61
|
+
for (const [id, p] of pending) {
|
|
62
|
+
clearTimeout(p.timer);
|
|
63
|
+
p.reject(new Error(reason));
|
|
64
|
+
pending.delete(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- owner mode -------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function setupOwner(wss) {
|
|
71
|
+
mode = "owner";
|
|
72
|
+
console.error(`[bridge] owner of ws://127.0.0.1:${PORT}`);
|
|
73
|
+
wss.on("error", (e) => console.error("[bridge] server error:", e.message));
|
|
74
|
+
|
|
75
|
+
wss.on("connection", (ws) => {
|
|
76
|
+
let role = null; // "extension" | "relay"
|
|
77
|
+
const authTimer = setTimeout(() => {
|
|
78
|
+
if (!role) ws.close();
|
|
79
|
+
}, 3000);
|
|
80
|
+
|
|
81
|
+
ws.on("message", (data) => {
|
|
82
|
+
let msg;
|
|
83
|
+
try {
|
|
84
|
+
msg = JSON.parse(data.toString());
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!role) {
|
|
90
|
+
if (msg.type === "auth" && TOKEN && msg.token === TOKEN) {
|
|
91
|
+
role = msg.role === "relay" ? "relay" : "extension";
|
|
92
|
+
clearTimeout(authTimer);
|
|
93
|
+
if (role === "extension") {
|
|
94
|
+
extension = ws;
|
|
95
|
+
console.error("[bridge] extension connected (authenticated)");
|
|
96
|
+
} else {
|
|
97
|
+
console.error("[bridge] relay session connected");
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
console.error("[bridge] rejected connection with bad/missing auth token");
|
|
101
|
+
ws.close();
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (msg.type === "ping") {
|
|
107
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (role === "extension") {
|
|
112
|
+
settle(msg.id, msg);
|
|
113
|
+
} else if (msg.type === "call") {
|
|
114
|
+
callExtensionDirect(msg.method, msg.params || {})
|
|
115
|
+
.then((result) =>
|
|
116
|
+
ws.send(JSON.stringify({ type: "result", id: msg.id, ok: true, result }))
|
|
117
|
+
)
|
|
118
|
+
.catch((e) =>
|
|
119
|
+
ws.send(
|
|
120
|
+
JSON.stringify({ type: "result", id: msg.id, ok: false, error: String(e?.message || e) })
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
ws.on("close", () => {
|
|
127
|
+
clearTimeout(authTimer);
|
|
128
|
+
if (extension === ws) {
|
|
129
|
+
extension = null;
|
|
130
|
+
console.error("[bridge] extension disconnected");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function callExtensionDirect(method, params = {}, timeoutMs = 20000) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
if (!extension || extension.readyState !== 1) {
|
|
139
|
+
return reject(new Error(NO_EXT_MSG));
|
|
140
|
+
}
|
|
141
|
+
const id = nextId++;
|
|
142
|
+
addPending(id, resolve, reject, timeoutMs, `Extension call '${method}'`);
|
|
143
|
+
extension.send(JSON.stringify({ id, method, params }));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- relay mode --------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function connectRelay() {
|
|
150
|
+
mode = "relay";
|
|
151
|
+
const ws = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
|
152
|
+
ws.on("open", () => {
|
|
153
|
+
ws.send(JSON.stringify({ type: "auth", token: TOKEN, role: "relay" }));
|
|
154
|
+
upstream = ws;
|
|
155
|
+
console.error(`[bridge] relay mode: forwarding through the session that owns port ${PORT}`);
|
|
156
|
+
});
|
|
157
|
+
ws.on("message", (data) => {
|
|
158
|
+
let msg;
|
|
159
|
+
try {
|
|
160
|
+
msg = JSON.parse(data.toString());
|
|
161
|
+
} catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (msg.type === "result") settle(msg.id, msg);
|
|
165
|
+
});
|
|
166
|
+
ws.on("close", () => {
|
|
167
|
+
if (upstream === ws) upstream = null;
|
|
168
|
+
rejectAllPending("Bridge connection lost (owner session ended?) — retry the call.");
|
|
169
|
+
setTimeout(establish, 1000); // owner may be gone: try to take over the port
|
|
170
|
+
});
|
|
171
|
+
ws.on("error", () => {
|
|
172
|
+
try {
|
|
173
|
+
ws.close();
|
|
174
|
+
} catch {}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function callViaRelay(method, params = {}, timeoutMs = 20000) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
if (!upstream || upstream.readyState !== 1) {
|
|
181
|
+
return reject(new Error("Bridge is reconnecting — retry in a few seconds."));
|
|
182
|
+
}
|
|
183
|
+
const id = nextId++;
|
|
184
|
+
addPending(id, resolve, reject, timeoutMs, `Bridge call '${method}'`);
|
|
185
|
+
upstream.send(JSON.stringify({ type: "call", id, method, params }));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- entry point --------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function callExtension(method, params = {}) {
|
|
192
|
+
return mode === "owner"
|
|
193
|
+
? callExtensionDirect(method, params)
|
|
194
|
+
: callViaRelay(method, params);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function establish() {
|
|
198
|
+
const wss = new WebSocketServer({ host: "127.0.0.1", port: PORT });
|
|
199
|
+
wss.once("listening", () => setupOwner(wss));
|
|
200
|
+
wss.once("error", (e) => {
|
|
201
|
+
if (e.code === "EADDRINUSE") {
|
|
202
|
+
connectRelay();
|
|
203
|
+
} else {
|
|
204
|
+
console.error("[bridge] failed to start:", e.message);
|
|
205
|
+
setTimeout(establish, 3000);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
establish();
|
|
211
|
+
|
|
212
|
+
// --- MCP server exposed to Claude Code -------------------------------------
|
|
213
|
+
|
|
214
|
+
const BATCH_TOOL = {
|
|
215
|
+
name: "batch",
|
|
216
|
+
description:
|
|
217
|
+
"Execute multiple tool calls in a single round-trip for speed. Pass an array of {name, arguments} objects. All calls run in parallel and results are returned in the same order. Use this when you need to call 2+ tools that don't depend on each other.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
calls: {
|
|
222
|
+
type: "array",
|
|
223
|
+
items: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
name: { type: "string", description: "Tool name" },
|
|
227
|
+
arguments: { type: "object", description: "Tool arguments" },
|
|
228
|
+
},
|
|
229
|
+
required: ["name"],
|
|
230
|
+
},
|
|
231
|
+
description: "Array of tool calls to execute in parallel",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
required: ["calls"],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const TAB_ID = {
|
|
239
|
+
tab_id: {
|
|
240
|
+
type: "number",
|
|
241
|
+
description:
|
|
242
|
+
"Optional tab id from list_tabs. Defaults to the tab pinned with select_tab, else the active tab.",
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const TOOLS = [
|
|
247
|
+
{
|
|
248
|
+
name: "list_tabs",
|
|
249
|
+
description:
|
|
250
|
+
"List all open browser tabs with their ids, titles and URLs. The pinned target (if any) has selected:true.",
|
|
251
|
+
inputSchema: { type: "object", properties: {} },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "select_tab",
|
|
255
|
+
description:
|
|
256
|
+
"Pin a tab as the sticky target for all subsequent tools, and visually mark it (orange border, 'Claude Code' badge, \u{1F916} title prefix in the tab strip) so the user can see exactly which tab is being driven. Without tab_id it pins the currently active tab. Pass clear=true to unpin and remove the marker. Use this first when several tabs show the same site.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
tab_id: { type: "number", description: "Tab to pin (from list_tabs)" },
|
|
261
|
+
clear: { type: "boolean", description: "Unpin and remove the visual marker" },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "snapshot",
|
|
267
|
+
description:
|
|
268
|
+
"Snapshot the visible interactive elements of the page (buttons, links, inputs, etc.) as ref-tagged lines like: ref_12 <button> \"Sign in\". Pass the ref to click/fill — far more reliable than guessing CSS selectors. Refs are invalidated by navigation or a new snapshot.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
max_elements: { type: "number", description: "Cap on elements returned (default 300)" },
|
|
273
|
+
...TAB_ID,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "get_page_text",
|
|
279
|
+
description: "Get URL, title and visible text of the active tab (or a specific tab).",
|
|
280
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "get_html",
|
|
284
|
+
description:
|
|
285
|
+
"Get the outerHTML of an element by CSS selector, or the whole document if no selector is given. For finding things to interact with, prefer the snapshot tool.",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: { selector: { type: "string" }, ...TAB_ID },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "wait_for",
|
|
293
|
+
description:
|
|
294
|
+
"Wait until a CSS selector and/or a text string appears on the page (polls every 200ms). Use after click/navigate on SPAs to know when the page settled. Example: {selector: \".results\", text: \"42 items\"}.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
selector: { type: "string", description: "CSS selector to wait for" },
|
|
299
|
+
text: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "Text that must appear (inside the selector if given, else anywhere on the page)",
|
|
302
|
+
},
|
|
303
|
+
timeout_ms: { type: "number", description: "Default 10000, max 15000" },
|
|
304
|
+
...TAB_ID,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "click",
|
|
310
|
+
description:
|
|
311
|
+
"Click an element identified by a snapshot ref (e.g. \"ref_12\", preferred) or a CSS selector. Automatically waits up to wait_ms (default 5000) for the element to appear, so it is safe right after navigation.",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
ref: { type: "string", description: "Element ref from snapshot, e.g. \"ref_12\" (preferred)" },
|
|
316
|
+
selector: { type: "string", description: "CSS selector (fallback if you have no snapshot)" },
|
|
317
|
+
wait_ms: { type: "number", description: "How long to wait for the element (default 5000)" },
|
|
318
|
+
...TAB_ID,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "fill",
|
|
324
|
+
description:
|
|
325
|
+
"Fill an input/textarea/contenteditable identified by a snapshot ref (preferred) or CSS selector, firing input/change events so frameworks like React notice. Auto-waits for the element like click.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
ref: { type: "string", description: "Element ref from snapshot, e.g. \"ref_12\" (preferred)" },
|
|
330
|
+
selector: { type: "string", description: "CSS selector (fallback if you have no snapshot)" },
|
|
331
|
+
value: { type: "string" },
|
|
332
|
+
wait_ms: { type: "number", description: "How long to wait for the element (default 5000)" },
|
|
333
|
+
...TAB_ID,
|
|
334
|
+
},
|
|
335
|
+
required: ["value"],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "eval",
|
|
340
|
+
description: "Evaluate JavaScript in the page's main world and return the result as a string.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: { code: { type: "string" }, ...TAB_ID },
|
|
344
|
+
required: ["code"],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "get_console",
|
|
349
|
+
description:
|
|
350
|
+
"Get recorded console messages, uncaught errors (with stack traces) and unhandled rejections — recorded continuously since page load. Pass clear=true to reset the buffer (useful before reproducing a bug).",
|
|
351
|
+
inputSchema: {
|
|
352
|
+
type: "object",
|
|
353
|
+
properties: { clear: { type: "boolean" }, ...TAB_ID },
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "get_network",
|
|
358
|
+
description:
|
|
359
|
+
"Get recorded fetch/XHR requests (method, url, status, duration, redacted request headers; response bodies are captured for failed requests). Recorded continuously since page load. Filter with url_contains and only_failures; clear=true resets the buffer.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {
|
|
363
|
+
url_contains: { type: "string", description: "Only requests whose URL contains this substring, e.g. \"/api/\"" },
|
|
364
|
+
only_failures: { type: "boolean", description: "Only network errors and HTTP status >= 400" },
|
|
365
|
+
clear: { type: "boolean" },
|
|
366
|
+
...TAB_ID,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "screenshot",
|
|
372
|
+
description: "Take a PNG screenshot of the visible area of the tab.",
|
|
373
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "navigate",
|
|
377
|
+
description: "Navigate the current tab to a URL (reuses the tab, does not open a new one).",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: { url: { type: "string" }, ...TAB_ID },
|
|
381
|
+
required: ["url"],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "hover",
|
|
386
|
+
description: "Hover over an element by snapshot ref or CSS selector. Fires mouseover/mouseenter events so tooltips and dropdowns appear.",
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {
|
|
390
|
+
ref: { type: "string", description: "Element ref from snapshot" },
|
|
391
|
+
selector: { type: "string", description: "CSS selector" },
|
|
392
|
+
...TAB_ID,
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "select_option",
|
|
398
|
+
description: "Select one or more options in a <select> dropdown by value or visible text.",
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
ref: { type: "string" },
|
|
403
|
+
selector: { type: "string" },
|
|
404
|
+
values: { type: "array", items: { type: "string" }, description: "Option values or text to select" },
|
|
405
|
+
...TAB_ID,
|
|
406
|
+
},
|
|
407
|
+
required: ["values"],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "press_key",
|
|
412
|
+
description: "Press a key or key combination (e.g. 'Enter', 'Escape', 'Tab', 'a'). Modifiers: ['Control','Shift','Alt','Meta'].",
|
|
413
|
+
inputSchema: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
key: { type: "string", description: "Key name (e.g. 'Enter', 'ArrowDown', 'a')" },
|
|
417
|
+
modifiers: { type: "array", items: { type: "string" }, description: "Modifier keys to hold" },
|
|
418
|
+
...TAB_ID,
|
|
419
|
+
},
|
|
420
|
+
required: ["key"],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "scroll",
|
|
425
|
+
description: "Scroll the page or a specific element. Direction: up/down/left/right. Default 400px.",
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
direction: { type: "string", enum: ["up", "down", "left", "right"], description: "Scroll direction (default 'down')" },
|
|
430
|
+
amount: { type: "number", description: "Pixels to scroll (default 400)" },
|
|
431
|
+
ref: { type: "string", description: "Scroll inside this element (from snapshot)" },
|
|
432
|
+
selector: { type: "string", description: "CSS selector of scrollable container" },
|
|
433
|
+
...TAB_ID,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "go_back",
|
|
439
|
+
description: "Navigate the tab back in history (like clicking the browser back button).",
|
|
440
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "go_forward",
|
|
444
|
+
description: "Navigate the tab forward in history.",
|
|
445
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "reload",
|
|
449
|
+
description: "Reload the current tab. Pass bypass_cache=true for a hard reload.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: "object",
|
|
452
|
+
properties: {
|
|
453
|
+
bypass_cache: { type: "boolean", description: "Bypass browser cache (hard reload)" },
|
|
454
|
+
...TAB_ID,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "new_tab",
|
|
460
|
+
description: "Open a new browser tab, optionally with a URL.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: { url: { type: "string" } },
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: "close_tab",
|
|
468
|
+
description: "Close the specified tab (or the active/pinned tab).",
|
|
469
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "get_cookies",
|
|
473
|
+
description: "Get cookies for the current tab's URL. Sensitive cookie values (session/auth/token) are redacted.",
|
|
474
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "get_storage",
|
|
478
|
+
description: "Read localStorage or sessionStorage from the tab. Pass key_filter to search for specific keys.",
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
storage_type: { type: "string", enum: ["local", "session"], description: "Which storage (default 'local')" },
|
|
483
|
+
key_filter: { type: "string", description: "Only return keys containing this substring" },
|
|
484
|
+
...TAB_ID,
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "upload_file",
|
|
490
|
+
description: "Upload a file to an <input type='file'> element by ref or CSS selector. Uses chrome.debugger to set the file path.",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
ref: { type: "string", description: "Element ref from snapshot" },
|
|
495
|
+
selector: { type: "string", description: "CSS selector for the file input" },
|
|
496
|
+
file_path: { type: "string", description: "Absolute path to the file to upload" },
|
|
497
|
+
...TAB_ID,
|
|
498
|
+
},
|
|
499
|
+
required: ["file_path"],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: "diagnose",
|
|
504
|
+
description:
|
|
505
|
+
"PREFERRED first tool — returns snapshot + console errors + failed network requests + recent API responses + CAPTCHA/iframe detection + localStorage keys, ALL in a single call. Use this instead of calling snapshot + get_console + get_network separately. Replaces 4 round-trips with 1.",
|
|
506
|
+
inputSchema: {
|
|
507
|
+
type: "object",
|
|
508
|
+
properties: {
|
|
509
|
+
max_elements: { type: "number", description: "Max snapshot elements (default 200)" },
|
|
510
|
+
...TAB_ID,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "get_page_info",
|
|
516
|
+
description:
|
|
517
|
+
"Quick diagnostic: returns URL, title, recent console errors, recent failed network requests, and CAPTCHA detection — all in a single call. Use this first when investigating a page issue.",
|
|
518
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: "get_styles",
|
|
522
|
+
description: "Get computed CSS styles (fonts, colors, spacing, layout) for any element by ref or selector. Essential for frontend/design debugging.",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: "object",
|
|
525
|
+
properties: {
|
|
526
|
+
ref: { type: "string" }, selector: { type: "string" },
|
|
527
|
+
properties: { type: "array", items: { type: "string" }, description: "Specific CSS properties to return (default: common layout/typography/color properties)" },
|
|
528
|
+
...TAB_ID,
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "get_accessibility_tree",
|
|
534
|
+
description: "Get the full accessibility (a11y) tree from Chrome — ARIA roles, names, states. Use for accessibility auditing and WCAG compliance checks.",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: {
|
|
538
|
+
max_depth: { type: "number", description: "Tree depth (default 5)" },
|
|
539
|
+
max_nodes: { type: "number", description: "Max nodes to return (default 300)" },
|
|
540
|
+
...TAB_ID,
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "performance_trace",
|
|
546
|
+
description: "Capture Core Web Vitals (LCP, FCP, CLS) and performance metrics — DOM load time, resource count, transfer size, long task count. One-call performance audit.",
|
|
547
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "heap_snapshot_summary",
|
|
551
|
+
description: "Get JS heap memory usage (used/total/limit MB) and DOM node count. Quick memory health check without a full heap snapshot.",
|
|
552
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: "emulate_device",
|
|
556
|
+
description: "Emulate a device viewport — 'mobile' (iPhone 375x812), 'tablet' (iPad 768x1024), 'desktop' (1440x900), or custom width/height. Pass clear=true or device='reset' to restore.",
|
|
557
|
+
inputSchema: {
|
|
558
|
+
type: "object",
|
|
559
|
+
properties: {
|
|
560
|
+
device: { type: "string", enum: ["mobile", "tablet", "desktop", "reset"], description: "Preset device" },
|
|
561
|
+
width: { type: "number" }, height: { type: "number" },
|
|
562
|
+
device_scale: { type: "number" }, mobile: { type: "boolean" },
|
|
563
|
+
user_agent: { type: "string" }, clear: { type: "boolean" },
|
|
564
|
+
...TAB_ID,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: "network_throttle",
|
|
570
|
+
description: "Throttle network speed — 'slow-3g', 'fast-3g', '4g', 'offline', or 'none' to disable. Use for testing loading states and slow connections.",
|
|
571
|
+
inputSchema: {
|
|
572
|
+
type: "object",
|
|
573
|
+
properties: {
|
|
574
|
+
preset: { type: "string", enum: ["slow-3g", "fast-3g", "4g", "offline", "none"] },
|
|
575
|
+
...TAB_ID,
|
|
576
|
+
},
|
|
577
|
+
required: ["preset"],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "full_page_screenshot",
|
|
582
|
+
description: "Capture the ENTIRE scrollable page as a PNG, not just the visible viewport. Uses Chrome DevTools Protocol.",
|
|
583
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "export_pdf",
|
|
587
|
+
description: "Export the current page as a PDF document.",
|
|
588
|
+
inputSchema: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: {
|
|
591
|
+
format: { type: "string", enum: ["A4", "Letter", "Legal"], description: "Page format (default A4)" },
|
|
592
|
+
...TAB_ID,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: "watch_dom_changes",
|
|
598
|
+
description: "Watch for DOM mutations (added/removed nodes, attribute changes) for a specified duration. Use to see what changes when an action is performed.",
|
|
599
|
+
inputSchema: {
|
|
600
|
+
type: "object",
|
|
601
|
+
properties: {
|
|
602
|
+
selector: { type: "string", description: "CSS selector of the subtree to watch (default: body)" },
|
|
603
|
+
duration_ms: { type: "number", description: "How long to watch in ms (default 5000, max 15000)" },
|
|
604
|
+
...TAB_ID,
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "check_contrast",
|
|
610
|
+
description: "Check WCAG color contrast ratio for a text element — returns fg/bg colors, contrast ratio, and AA/AAA pass/fail. Essential for accessibility.",
|
|
611
|
+
inputSchema: {
|
|
612
|
+
type: "object",
|
|
613
|
+
properties: {
|
|
614
|
+
ref: { type: "string" }, selector: { type: "string" }, ...TAB_ID,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "handle_dialog",
|
|
620
|
+
description: "Set up an auto-handler for the next JavaScript dialog (alert/confirm/prompt). Use before triggering an action that shows a dialog.",
|
|
621
|
+
inputSchema: {
|
|
622
|
+
type: "object",
|
|
623
|
+
properties: {
|
|
624
|
+
accept: { type: "boolean", description: "Accept (true) or dismiss (false) the dialog (default true)" },
|
|
625
|
+
prompt_text: { type: "string", description: "Text to enter for prompt() dialogs" },
|
|
626
|
+
...TAB_ID,
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "get_clipboard",
|
|
632
|
+
description: "Read the current clipboard text content.",
|
|
633
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
634
|
+
},
|
|
635
|
+
// ====================== TIER 1 ======================
|
|
636
|
+
{
|
|
637
|
+
name: "visual_diff",
|
|
638
|
+
description: "Compare a 'before' screenshot with the current page state. Returns diff percentage, pixel count, and a diff image with changes highlighted in red. Use: take a screenshot, make changes, then call visual_diff with before_dataUrl set to the first screenshot.",
|
|
639
|
+
inputSchema: {
|
|
640
|
+
type: "object",
|
|
641
|
+
properties: {
|
|
642
|
+
before_dataUrl: { type: "string", description: "data:image/png;base64,... from a previous screenshot call" },
|
|
643
|
+
...TAB_ID,
|
|
644
|
+
},
|
|
645
|
+
required: ["before_dataUrl"],
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "inject_css",
|
|
650
|
+
description: "Inject CSS directly into the live page WITHOUT rebuilding. Instant visual feedback for CSS fixes. Pass css=null with the same id to remove it. Use this to test CSS changes before writing them to the actual file.",
|
|
651
|
+
inputSchema: {
|
|
652
|
+
type: "object",
|
|
653
|
+
properties: {
|
|
654
|
+
css: { type: "string", description: "CSS to inject (null to remove)" },
|
|
655
|
+
id: { type: "string", description: "Style element ID (default '__claude_inject_css__'). Use different IDs for multiple injections." },
|
|
656
|
+
...TAB_ID,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "record_actions",
|
|
662
|
+
description: "Start or stop recording user interactions (clicks, input changes). Call with no params to start recording. Call with stop=true to get the recorded action list. Replay the list with replay_actions.",
|
|
663
|
+
inputSchema: {
|
|
664
|
+
type: "object",
|
|
665
|
+
properties: {
|
|
666
|
+
stop: { type: "boolean", description: "Stop recording and return actions" },
|
|
667
|
+
...TAB_ID,
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "replay_actions",
|
|
673
|
+
description: "Replay a recorded sequence of actions (from record_actions). Useful for regression testing — record the steps to reproduce a bug, fix the code, replay to verify.",
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
actions: { type: "array", items: { type: "object" }, description: "Array of actions from record_actions" },
|
|
678
|
+
delay_ms: { type: "number", description: "Delay between actions in ms (default: none)" },
|
|
679
|
+
...TAB_ID,
|
|
680
|
+
},
|
|
681
|
+
required: ["actions"],
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "mock_network",
|
|
686
|
+
description: "Intercept network requests matching a URL pattern and return a custom response. Use for testing error states, empty states, slow responses. The mock stays active until the debugger is detached.",
|
|
687
|
+
inputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
properties: {
|
|
690
|
+
url_pattern: { type: "string", description: "URL substring to match (e.g. '/api/cart')" },
|
|
691
|
+
status_code: { type: "number", description: "HTTP status to return (default 200)" },
|
|
692
|
+
response_body: { description: "Response body (string or object)" },
|
|
693
|
+
content_type: { type: "string", description: "Content-Type header (default 'application/json')" },
|
|
694
|
+
...TAB_ID,
|
|
695
|
+
},
|
|
696
|
+
required: ["url_pattern"],
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
name: "highlight_element",
|
|
701
|
+
description: "Visually highlight an element in the browser with a pulsing colored outline. The user sees exactly which element you're referring to. Highlight disappears after duration_ms.",
|
|
702
|
+
inputSchema: {
|
|
703
|
+
type: "object",
|
|
704
|
+
properties: {
|
|
705
|
+
ref: { type: "string" }, selector: { type: "string" },
|
|
706
|
+
color: { type: "string", description: "Border color (default '#D97757' orange)" },
|
|
707
|
+
duration_ms: { type: "number", description: "How long to show highlight (default 3000)" },
|
|
708
|
+
...TAB_ID,
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
// ====================== TIER 2 ======================
|
|
713
|
+
{
|
|
714
|
+
name: "save_form_profile",
|
|
715
|
+
description: "Save all current form field values on the page as a named profile. Reuse with load_form_profile on similar forms.",
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: "object",
|
|
718
|
+
properties: { name: { type: "string", description: "Profile name (default 'default')" }, ...TAB_ID },
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "load_form_profile",
|
|
723
|
+
description: "Load a saved form profile and fill all matching fields. Useful for job applications, login forms, or any repeated form filling.",
|
|
724
|
+
inputSchema: {
|
|
725
|
+
type: "object",
|
|
726
|
+
properties: { name: { type: "string", description: "Profile name to load" }, ...TAB_ID },
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: "get_load_timeline",
|
|
731
|
+
description: "Get the full page load timeline: DNS, TCP, request, response, DOM processing phases + resource waterfall (top 30 by load order) + milestones (FP, FCP, LCP, DCL, Load).",
|
|
732
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "get_grouped_console",
|
|
736
|
+
description: "Get console messages grouped and counted — e.g. '27x: Font resolve mismatch' instead of 27 individual entries. Sorted by frequency.",
|
|
737
|
+
inputSchema: { type: "object", properties: { ...TAB_ID } },
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: "generate_selector",
|
|
741
|
+
description: "Generate multiple stable CSS selectors for an element (by id, data-testid, aria-label, path, text). Returns options ranked by reliability.",
|
|
742
|
+
inputSchema: {
|
|
743
|
+
type: "object",
|
|
744
|
+
properties: { ref: { type: "string" }, selector: { type: "string" }, ...TAB_ID },
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
name: "save_tab_session",
|
|
749
|
+
description: "Save all current tab URLs as a named session. Restore later with restore_tab_session.",
|
|
750
|
+
inputSchema: {
|
|
751
|
+
type: "object",
|
|
752
|
+
properties: { name: { type: "string", description: "Session name (default 'default')" } },
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "restore_tab_session",
|
|
757
|
+
description: "Restore a previously saved tab session — reopens all tabs from the saved session.",
|
|
758
|
+
inputSchema: {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties: { name: { type: "string", description: "Session name to restore" } },
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
// ====================== TIER 3 ======================
|
|
764
|
+
{
|
|
765
|
+
name: "set_geolocation",
|
|
766
|
+
description: "Spoof GPS geolocation for location-based features. Pass clear=true to restore real location.",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: {
|
|
770
|
+
latitude: { type: "number" }, longitude: { type: "number" }, accuracy: { type: "number" },
|
|
771
|
+
clear: { type: "boolean" }, ...TAB_ID,
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: "toggle_dark_mode",
|
|
777
|
+
description: "Toggle prefers-color-scheme between dark and light. Tests dark mode without changing OS settings.",
|
|
778
|
+
inputSchema: {
|
|
779
|
+
type: "object",
|
|
780
|
+
properties: { dark: { type: "boolean", description: "true for dark, false for light (default true)" }, ...TAB_ID },
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "edit_cookie",
|
|
785
|
+
description: "Set, modify, or delete a cookie. Pass delete=true to remove. Useful for testing auth states.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {
|
|
789
|
+
name: { type: "string" }, value: { type: "string" }, domain: { type: "string" },
|
|
790
|
+
path: { type: "string" }, secure: { type: "boolean" }, httpOnly: { type: "boolean" },
|
|
791
|
+
expirationDate: { type: "number" }, delete: { type: "boolean" }, ...TAB_ID,
|
|
792
|
+
},
|
|
793
|
+
required: ["name"],
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "search_network_bodies",
|
|
798
|
+
description: "Search across all recorded request/response bodies for a string. Finds which API call contains a specific value, field name, or error message.",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: { query: { type: "string", description: "String to search for in request/response bodies" }, ...TAB_ID },
|
|
802
|
+
required: ["query"],
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
];
|
|
806
|
+
|
|
807
|
+
const server = new Server(
|
|
808
|
+
{ name: "browser-bridge", version: "3.0.0" },
|
|
809
|
+
{ capabilities: { tools: {} } }
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [BATCH_TOOL, ...TOOLS] }));
|
|
813
|
+
|
|
814
|
+
function formatResult(name, result) {
|
|
815
|
+
if ((name === "screenshot" || name === "full_page_screenshot") && result?.dataUrl) {
|
|
816
|
+
const base64 = result.dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
817
|
+
return {
|
|
818
|
+
content: [
|
|
819
|
+
{ type: "image", data: base64, mimeType: "image/png" },
|
|
820
|
+
{ type: "text", text: `Screenshot of ${result.url}` },
|
|
821
|
+
],
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (name === "visual_diff" && result?.diffImage) {
|
|
825
|
+
const base64 = result.diffImage.replace(/^data:image\/png;base64,/, "");
|
|
826
|
+
const { diffImage, ...rest } = result;
|
|
827
|
+
return {
|
|
828
|
+
content: [
|
|
829
|
+
{ type: "image", data: base64, mimeType: "image/png" },
|
|
830
|
+
{ type: "text", text: JSON.stringify(rest, null, 2) },
|
|
831
|
+
],
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
838
|
+
const { name, arguments: args } = req.params;
|
|
839
|
+
try {
|
|
840
|
+
// Batch: run multiple calls in one round-trip through the extension.
|
|
841
|
+
if (name === "batch") {
|
|
842
|
+
const calls = args?.calls;
|
|
843
|
+
if (!Array.isArray(calls) || !calls.length) {
|
|
844
|
+
return { isError: true, content: [{ type: "text", text: "'calls' array is required" }] };
|
|
845
|
+
}
|
|
846
|
+
const results = await callExtension("batch", { calls: calls.map(c => ({ method: c.name, params: c.arguments || {} })) });
|
|
847
|
+
const formatted = results.map((r, i) => {
|
|
848
|
+
const label = `[${i}] ${calls[i].name}`;
|
|
849
|
+
if (!r.ok) return `${label}: ERROR — ${r.error}`;
|
|
850
|
+
if (calls[i].name === "screenshot" && r.result?.dataUrl) return `${label}: (screenshot captured)`;
|
|
851
|
+
return `${label}: ${JSON.stringify(r.result)}`;
|
|
852
|
+
});
|
|
853
|
+
return { content: [{ type: "text", text: formatted.join("\n\n") }] };
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const result = await callExtension(name, args || {});
|
|
857
|
+
return formatResult(name, result);
|
|
858
|
+
} catch (e) {
|
|
859
|
+
return { isError: true, content: [{ type: "text", text: String(e?.message || e) }] };
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const transport = new StdioServerTransport();
|
|
864
|
+
await server.connect(transport);
|
|
865
|
+
console.error("[mcp] ready on stdio");
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-browser-bridge",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Connect your live Chrome tabs to Claude Code — 57 tools for debugging, performance, accessibility, device emulation, and browser automation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-browser-bridge": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"index.js",
|
|
13
|
+
"gen-token.js",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude-code",
|
|
18
|
+
"mcp",
|
|
19
|
+
"browser",
|
|
20
|
+
"chrome",
|
|
21
|
+
"devtools",
|
|
22
|
+
"debugging",
|
|
23
|
+
"automation",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"anthropic",
|
|
26
|
+
"claude"
|
|
27
|
+
],
|
|
28
|
+
"author": "Himanshu Kanwar <himanshukanwar2001@gmail.com>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/HimanshuKanwar2001/claude-browser-bridge"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/HimanshuKanwar2001/claude-browser-bridge#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
+
"ws": "^8.18.0"
|
|
41
|
+
}
|
|
42
|
+
}
|