browserhand-mcp 0.1.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/dist/index.d.ts +2 -0
- package/dist/index.js +385 -0
- package/package.json +25 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
// ── Config ──────────────────────────────────────────────────────
|
|
9
|
+
const RELAY_URL = process.env.BROWSERHAND_RELAY_URL || "http://127.0.0.1:29981";
|
|
10
|
+
const RELAY_COMMAND_URL = `${RELAY_URL}/command`;
|
|
11
|
+
const RELAY_STATUS_URL = `${RELAY_URL}/status`;
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const RELAY_INDEX = resolve(__dirname, "..", "..", "relay", "index.ts");
|
|
14
|
+
// ── Relay helper ────────────────────────────────────────────────
|
|
15
|
+
async function relay(action, params = {}) {
|
|
16
|
+
const res = await fetch(RELAY_COMMAND_URL, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify({ action, params }),
|
|
20
|
+
});
|
|
21
|
+
return (await res.json());
|
|
22
|
+
}
|
|
23
|
+
function textResult(data) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function screenshotResult(data) {
|
|
29
|
+
const content = [];
|
|
30
|
+
// Extract base64 from data URL (data:image/jpeg;base64,...)
|
|
31
|
+
const screenshot = data.screenshot;
|
|
32
|
+
if (screenshot) {
|
|
33
|
+
const base64 = screenshot.includes(",")
|
|
34
|
+
? screenshot.split(",")[1]
|
|
35
|
+
: screenshot;
|
|
36
|
+
const mimeType = screenshot.startsWith("data:image/png")
|
|
37
|
+
? "image/png"
|
|
38
|
+
: "image/jpeg";
|
|
39
|
+
content.push({ type: "image", data: base64, mimeType });
|
|
40
|
+
}
|
|
41
|
+
// Also include text metadata
|
|
42
|
+
const { screenshot: _, ...meta } = data;
|
|
43
|
+
if (Object.keys(meta).length > 0) {
|
|
44
|
+
content.push({ type: "text", text: JSON.stringify(meta, null, 2) });
|
|
45
|
+
}
|
|
46
|
+
return { content };
|
|
47
|
+
}
|
|
48
|
+
// ── Auto-start relay ────────────────────────────────────────────
|
|
49
|
+
async function ensureRelay() {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(RELAY_STATUS_URL);
|
|
52
|
+
if (res.ok)
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Not running, start it
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const child = spawn("npx", ["tsx", RELAY_INDEX], {
|
|
60
|
+
detached: true,
|
|
61
|
+
stdio: "ignore",
|
|
62
|
+
env: { ...process.env },
|
|
63
|
+
});
|
|
64
|
+
child.unref();
|
|
65
|
+
// Wait for relay to be ready (up to 5s)
|
|
66
|
+
for (let i = 0; i < 25; i++) {
|
|
67
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(RELAY_STATUS_URL);
|
|
70
|
+
if (res.ok)
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// keep waiting
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// If auto-start fails, tools will return relay connection errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ── MCP Server ──────────────────────────────────────────────────
|
|
83
|
+
const server = new McpServer({
|
|
84
|
+
name: "browserhand",
|
|
85
|
+
version: "0.1.0",
|
|
86
|
+
});
|
|
87
|
+
// ── Core actions ────────────────────────────────────────────────
|
|
88
|
+
server.registerTool("navigate", {
|
|
89
|
+
title: "Navigate",
|
|
90
|
+
description: "Open a URL in the user's real browser. Preserves login state and cookies.",
|
|
91
|
+
inputSchema: z.object({
|
|
92
|
+
url: z.string().describe("The URL to navigate to"),
|
|
93
|
+
groupLabel: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Tab group label for agent-operated tabs"),
|
|
97
|
+
}),
|
|
98
|
+
}, async (params) => {
|
|
99
|
+
const result = await relay("navigate", params);
|
|
100
|
+
return textResult(result);
|
|
101
|
+
});
|
|
102
|
+
server.registerTool("extract", {
|
|
103
|
+
title: "Extract Elements",
|
|
104
|
+
description: "Get all interactive elements on the current page. Returns an array of elements with index, tag, text, type, and position. Use the index to click, type, or interact with elements.",
|
|
105
|
+
inputSchema: z.object({}),
|
|
106
|
+
}, async () => {
|
|
107
|
+
const result = await relay("extract");
|
|
108
|
+
return textResult(result);
|
|
109
|
+
});
|
|
110
|
+
server.registerTool("click", {
|
|
111
|
+
title: "Click",
|
|
112
|
+
description: "Click an interactive element by its index (from extract). Returns updated page elements after click.",
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
index: z.number().describe("Element index from extract results"),
|
|
115
|
+
}),
|
|
116
|
+
}, async (params) => {
|
|
117
|
+
const result = await relay("click", params);
|
|
118
|
+
return textResult(result);
|
|
119
|
+
});
|
|
120
|
+
server.registerTool("type", {
|
|
121
|
+
title: "Type Text",
|
|
122
|
+
description: "Type text into an input element. Clicks the element first to focus it, then types each character.",
|
|
123
|
+
inputSchema: z.object({
|
|
124
|
+
index: z.number().describe("Element index from extract results"),
|
|
125
|
+
text: z.string().describe("Text to type"),
|
|
126
|
+
}),
|
|
127
|
+
}, async (params) => {
|
|
128
|
+
const result = await relay("type", params);
|
|
129
|
+
return textResult(result);
|
|
130
|
+
});
|
|
131
|
+
server.registerTool("scroll", {
|
|
132
|
+
title: "Scroll",
|
|
133
|
+
description: "Scroll the page up or down.",
|
|
134
|
+
inputSchema: z.object({
|
|
135
|
+
direction: z.enum(["up", "down"]).describe("Scroll direction"),
|
|
136
|
+
}),
|
|
137
|
+
}, async (params) => {
|
|
138
|
+
const result = await relay("scroll", params);
|
|
139
|
+
return textResult(result);
|
|
140
|
+
});
|
|
141
|
+
server.registerTool("screenshot", {
|
|
142
|
+
title: "Screenshot",
|
|
143
|
+
description: "Capture a screenshot of the visible viewport. Returns the image directly.",
|
|
144
|
+
inputSchema: z.object({}),
|
|
145
|
+
}, async () => {
|
|
146
|
+
const result = await relay("screenshot");
|
|
147
|
+
if (result.success && result.data) {
|
|
148
|
+
return screenshotResult(result.data);
|
|
149
|
+
}
|
|
150
|
+
return textResult(result);
|
|
151
|
+
});
|
|
152
|
+
server.registerTool("wait", {
|
|
153
|
+
title: "Wait",
|
|
154
|
+
description: "Wait for a specified number of seconds.",
|
|
155
|
+
inputSchema: z.object({
|
|
156
|
+
seconds: z.number().optional().describe("Seconds to wait (default: 1)"),
|
|
157
|
+
}),
|
|
158
|
+
}, async (params) => {
|
|
159
|
+
const result = await relay("wait", params);
|
|
160
|
+
return textResult(result);
|
|
161
|
+
});
|
|
162
|
+
// ── Navigation actions ──────────────────────────────────────────
|
|
163
|
+
server.registerTool("goback", {
|
|
164
|
+
title: "Go Back",
|
|
165
|
+
description: "Navigate back in browser history.",
|
|
166
|
+
inputSchema: z.object({}),
|
|
167
|
+
}, async () => {
|
|
168
|
+
const result = await relay("goback");
|
|
169
|
+
return textResult(result);
|
|
170
|
+
});
|
|
171
|
+
server.registerTool("goforward", {
|
|
172
|
+
title: "Go Forward",
|
|
173
|
+
description: "Navigate forward in browser history.",
|
|
174
|
+
inputSchema: z.object({}),
|
|
175
|
+
}, async () => {
|
|
176
|
+
const result = await relay("goforward");
|
|
177
|
+
return textResult(result);
|
|
178
|
+
});
|
|
179
|
+
server.registerTool("refresh", {
|
|
180
|
+
title: "Refresh",
|
|
181
|
+
description: "Reload the current page.",
|
|
182
|
+
inputSchema: z.object({}),
|
|
183
|
+
}, async () => {
|
|
184
|
+
const result = await relay("refresh");
|
|
185
|
+
return textResult(result);
|
|
186
|
+
});
|
|
187
|
+
server.registerTool("tab", {
|
|
188
|
+
title: "Manage Tabs",
|
|
189
|
+
description: "List, switch, close, or create browser tabs. Use tabAction to specify the operation.",
|
|
190
|
+
inputSchema: z.object({
|
|
191
|
+
tabAction: z
|
|
192
|
+
.enum(["list", "switch", "close", "create"])
|
|
193
|
+
.describe("Tab operation"),
|
|
194
|
+
tabId: z.number().optional().describe("Tab ID (for switch/close)"),
|
|
195
|
+
url: z
|
|
196
|
+
.string()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe("URL to open (for create, default: about:blank)"),
|
|
199
|
+
groupLabel: z.string().optional().describe("Tab group label (for create)"),
|
|
200
|
+
}),
|
|
201
|
+
}, async (params) => {
|
|
202
|
+
const result = await relay("tab", params);
|
|
203
|
+
return textResult(result);
|
|
204
|
+
});
|
|
205
|
+
// ── Interaction actions ─────────────────────────────────────────
|
|
206
|
+
server.registerTool("select", {
|
|
207
|
+
title: "Select Option",
|
|
208
|
+
description: "Choose an option in a dropdown/select element.",
|
|
209
|
+
inputSchema: z.object({
|
|
210
|
+
index: z.number().describe("Element index from extract results"),
|
|
211
|
+
value: z.string().describe("Value to select"),
|
|
212
|
+
}),
|
|
213
|
+
}, async (params) => {
|
|
214
|
+
const result = await relay("select", params);
|
|
215
|
+
return textResult(result);
|
|
216
|
+
});
|
|
217
|
+
server.registerTool("hover", {
|
|
218
|
+
title: "Hover",
|
|
219
|
+
description: "Move the mouse over an element (triggers hover effects, tooltips, etc).",
|
|
220
|
+
inputSchema: z.object({
|
|
221
|
+
index: z.number().describe("Element index from extract results"),
|
|
222
|
+
}),
|
|
223
|
+
}, async (params) => {
|
|
224
|
+
const result = await relay("hover", params);
|
|
225
|
+
return textResult(result);
|
|
226
|
+
});
|
|
227
|
+
server.registerTool("keypress", {
|
|
228
|
+
title: "Key Press",
|
|
229
|
+
description: "Press a key or key combination. Supports special keys (Enter, Tab, Escape, ArrowDown, etc) and modifiers.",
|
|
230
|
+
inputSchema: z.object({
|
|
231
|
+
key: z.string().describe("Key to press (e.g. 'Enter', 'Tab', 'a')"),
|
|
232
|
+
modifiers: z
|
|
233
|
+
.array(z.enum(["alt", "ctrl", "meta", "cmd", "shift"]))
|
|
234
|
+
.optional()
|
|
235
|
+
.describe("Modifier keys to hold"),
|
|
236
|
+
}),
|
|
237
|
+
}, async (params) => {
|
|
238
|
+
const result = await relay("keypress", params);
|
|
239
|
+
return textResult(result);
|
|
240
|
+
});
|
|
241
|
+
server.registerTool("clear", {
|
|
242
|
+
title: "Clear Input",
|
|
243
|
+
description: "Clear the text content of an input or textarea element.",
|
|
244
|
+
inputSchema: z.object({
|
|
245
|
+
index: z.number().describe("Element index from extract results"),
|
|
246
|
+
}),
|
|
247
|
+
}, async (params) => {
|
|
248
|
+
const result = await relay("clear", params);
|
|
249
|
+
return textResult(result);
|
|
250
|
+
});
|
|
251
|
+
server.registerTool("upload", {
|
|
252
|
+
title: "Upload File",
|
|
253
|
+
description: "Upload a file to a file input element.",
|
|
254
|
+
inputSchema: z.object({
|
|
255
|
+
index: z.number().describe("Element index of the file input"),
|
|
256
|
+
filePath: z.string().describe("Absolute path to the file to upload"),
|
|
257
|
+
}),
|
|
258
|
+
}, async (params) => {
|
|
259
|
+
const result = await relay("upload", params);
|
|
260
|
+
return textResult(result);
|
|
261
|
+
});
|
|
262
|
+
server.registerTool("dialog", {
|
|
263
|
+
title: "Handle Dialog",
|
|
264
|
+
description: "Accept or dismiss a browser dialog (alert, confirm, prompt).",
|
|
265
|
+
inputSchema: z.object({
|
|
266
|
+
dialogAction: z
|
|
267
|
+
.enum(["accept", "dismiss"])
|
|
268
|
+
.describe("Accept or dismiss the dialog"),
|
|
269
|
+
dialogText: z
|
|
270
|
+
.string()
|
|
271
|
+
.optional()
|
|
272
|
+
.describe("Text to enter in a prompt dialog"),
|
|
273
|
+
}),
|
|
274
|
+
}, async (params) => {
|
|
275
|
+
const result = await relay("dialog", params);
|
|
276
|
+
return textResult(result);
|
|
277
|
+
});
|
|
278
|
+
// ── Data actions ────────────────────────────────────────────────
|
|
279
|
+
server.registerTool("gettext", {
|
|
280
|
+
title: "Get Page Text",
|
|
281
|
+
description: "Extract the text content of the page (scripts and styles removed, max 10000 chars).",
|
|
282
|
+
inputSchema: z.object({}),
|
|
283
|
+
}, async () => {
|
|
284
|
+
const result = await relay("gettext");
|
|
285
|
+
return textResult(result);
|
|
286
|
+
});
|
|
287
|
+
server.registerTool("exec", {
|
|
288
|
+
title: "Execute JavaScript",
|
|
289
|
+
description: "Run JavaScript code on the current page and return the result. Blocked on sensitive pages.",
|
|
290
|
+
inputSchema: z.object({
|
|
291
|
+
script: z.string().describe("JavaScript code to execute"),
|
|
292
|
+
}),
|
|
293
|
+
}, async (params) => {
|
|
294
|
+
const result = await relay("exec", params);
|
|
295
|
+
return textResult(result);
|
|
296
|
+
});
|
|
297
|
+
server.registerTool("cookie", {
|
|
298
|
+
title: "Manage Cookies",
|
|
299
|
+
description: "Get, set, or delete cookies for the current page.",
|
|
300
|
+
inputSchema: z.object({
|
|
301
|
+
cookieAction: z
|
|
302
|
+
.enum(["get", "set", "delete"])
|
|
303
|
+
.describe("Cookie operation"),
|
|
304
|
+
cookieName: z.string().describe("Cookie name"),
|
|
305
|
+
cookieValue: z
|
|
306
|
+
.string()
|
|
307
|
+
.optional()
|
|
308
|
+
.describe("Cookie value (required for set)"),
|
|
309
|
+
}),
|
|
310
|
+
}, async (params) => {
|
|
311
|
+
const result = await relay("cookie", params);
|
|
312
|
+
return textResult(result);
|
|
313
|
+
});
|
|
314
|
+
server.registerTool("network", {
|
|
315
|
+
title: "Network Monitor",
|
|
316
|
+
description: "Start, stop, or get captured network requests. Use 'start' to begin monitoring, 'get' to retrieve entries, 'stop' to finish.",
|
|
317
|
+
inputSchema: z.object({
|
|
318
|
+
networkAction: z
|
|
319
|
+
.enum(["start", "stop", "get"])
|
|
320
|
+
.describe("Network monitoring operation"),
|
|
321
|
+
}),
|
|
322
|
+
}, async (params) => {
|
|
323
|
+
const result = await relay("network", params);
|
|
324
|
+
return textResult(result);
|
|
325
|
+
});
|
|
326
|
+
// ── Capture actions ─────────────────────────────────────────────
|
|
327
|
+
server.registerTool("fullscreenshot", {
|
|
328
|
+
title: "Full Page Screenshot",
|
|
329
|
+
description: "Capture a screenshot of the entire page (including content below the fold).",
|
|
330
|
+
inputSchema: z.object({
|
|
331
|
+
format: z
|
|
332
|
+
.enum(["png", "jpeg"])
|
|
333
|
+
.optional()
|
|
334
|
+
.describe("Image format (default: jpeg)"),
|
|
335
|
+
}),
|
|
336
|
+
}, async (params) => {
|
|
337
|
+
const result = await relay("fullscreenshot", params);
|
|
338
|
+
if (result.success && result.data) {
|
|
339
|
+
return screenshotResult(result.data);
|
|
340
|
+
}
|
|
341
|
+
return textResult(result);
|
|
342
|
+
});
|
|
343
|
+
server.registerTool("pdf", {
|
|
344
|
+
title: "Export PDF",
|
|
345
|
+
description: "Export the current page as a PDF document.",
|
|
346
|
+
inputSchema: z.object({}),
|
|
347
|
+
}, async () => {
|
|
348
|
+
const result = await relay("pdf");
|
|
349
|
+
return textResult(result);
|
|
350
|
+
});
|
|
351
|
+
// ── Wait actions ────────────────────────────────────────────────
|
|
352
|
+
server.registerTool("scrollto", {
|
|
353
|
+
title: "Scroll To Element",
|
|
354
|
+
description: "Scroll the page to bring a specific element into view.",
|
|
355
|
+
inputSchema: z.object({
|
|
356
|
+
index: z.number().describe("Element index from extract results"),
|
|
357
|
+
}),
|
|
358
|
+
}, async (params) => {
|
|
359
|
+
const result = await relay("scrollto", params);
|
|
360
|
+
return textResult(result);
|
|
361
|
+
});
|
|
362
|
+
server.registerTool("waitfor", {
|
|
363
|
+
title: "Wait For Element",
|
|
364
|
+
description: "Wait for an element matching a CSS selector to appear on the page.",
|
|
365
|
+
inputSchema: z.object({
|
|
366
|
+
selector: z.string().describe("CSS selector to wait for"),
|
|
367
|
+
timeout: z
|
|
368
|
+
.number()
|
|
369
|
+
.optional()
|
|
370
|
+
.describe("Max wait time in ms (default: 10000)"),
|
|
371
|
+
}),
|
|
372
|
+
}, async (params) => {
|
|
373
|
+
const result = await relay("waitfor", params);
|
|
374
|
+
return textResult(result);
|
|
375
|
+
});
|
|
376
|
+
// ── Start ───────────────────────────────────────────────────────
|
|
377
|
+
async function main() {
|
|
378
|
+
await ensureRelay();
|
|
379
|
+
const transport = new StdioServerTransport();
|
|
380
|
+
await server.connect(transport);
|
|
381
|
+
}
|
|
382
|
+
main().catch((err) => {
|
|
383
|
+
console.error("BrowserHand MCP server failed to start:", err);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browserhand-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for BrowserHand — control your real browser from Claude Desktop, Cursor, and other MCP clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"browserhand-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsx index.ts"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
18
|
+
"zod": "^3.23.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.7.0",
|
|
22
|
+
"tsx": "^4.21.0",
|
|
23
|
+
"@types/node": "^22.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|