clay-server 2.26.0-beta.4 → 2.26.0-beta.6

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.
@@ -0,0 +1,496 @@
1
+ // Browser MCP Server for Clay (in-process SDK version)
2
+ // Provides browser automation tools to Claude via createSdkMcpServer.
3
+ // Calls sendExtensionCommand directly instead of HTTP bridge.
4
+ //
5
+ // Usage:
6
+ // var browserMcp = require("./browser-mcp-server");
7
+ // var mcpConfig = browserMcp.create(sendExtensionCommandAny);
8
+ // // Pass mcpConfig to sdk-bridge opts.mcpServers
9
+
10
+ var z;
11
+ try { z = require("zod"); } catch (e) { z = null; }
12
+
13
+ // Build a Zod shape from simple property descriptors
14
+ function buildShape(props, required) {
15
+ if (!z) return {};
16
+ var shape = {};
17
+ var keys = Object.keys(props);
18
+ for (var i = 0; i < keys.length; i++) {
19
+ var k = keys[i];
20
+ var p = props[k];
21
+ var field;
22
+ if (p.type === "number") field = z.number();
23
+ else if (p.type === "boolean") field = z.boolean();
24
+ else if (p.enum) field = z.enum(p.enum);
25
+ else field = z.string();
26
+ if (p.description) field = field.describe(p.description);
27
+ if (!required || required.indexOf(k) === -1) field = field.optional();
28
+ shape[k] = field;
29
+ }
30
+ return shape;
31
+ }
32
+
33
+ function create(sendCommand, getTabList, contextOps) {
34
+ // sendCommand(command, args, timeout) -> Promise<result>
35
+ // getTabList() -> array of { id, url, title, favIconUrl }
36
+ var sdk;
37
+ try { sdk = require("@anthropic-ai/claude-agent-sdk"); } catch (e) {
38
+ console.error("[browser-mcp] Failed to load SDK:", e.message);
39
+ return null;
40
+ }
41
+
42
+ var createSdkMcpServer = sdk.createSdkMcpServer;
43
+ var tool = sdk.tool;
44
+ if (!createSdkMcpServer || !tool) {
45
+ console.error("[browser-mcp] SDK missing createSdkMcpServer or tool helper");
46
+ return null;
47
+ }
48
+
49
+ // Helper: ensure inject.js loaded (best-effort)
50
+ function ensureInjected(tabId) {
51
+ return sendCommand("tab_inject", { tabId: tabId }).catch(function () {});
52
+ }
53
+
54
+ var tools = [];
55
+
56
+ // --- browser_list_tabs ---
57
+ tools.push(tool(
58
+ "browser_list_tabs",
59
+ "List all open browser tabs with their IDs, URLs, and titles",
60
+ buildShape({}, []),
61
+ function () {
62
+ var tabs = getTabList ? getTabList() : [];
63
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(tabs, null, 2) }] });
64
+ }
65
+ ));
66
+
67
+ // --- browser_open ---
68
+ tools.push(tool(
69
+ "browser_open",
70
+ "Open a new browser tab and return its tab ID",
71
+ buildShape({
72
+ url: { type: "string", description: "URL to open" },
73
+ active: { type: "boolean", description: "Activate the tab (default true)" },
74
+ }, ["url"]),
75
+ function (args) {
76
+ return sendCommand("tab_open", { url: args.url, active: args.active !== false }).then(function (result) {
77
+ return { content: [{ type: "text", text: "Opened tab " + (result.id || "unknown") + ": " + args.url }] };
78
+ });
79
+ }
80
+ ));
81
+
82
+ // --- browser_close ---
83
+ tools.push(tool(
84
+ "browser_close",
85
+ "Close a browser tab",
86
+ buildShape({
87
+ tabId: { type: "number", description: "Tab ID to close" },
88
+ }, ["tabId"]),
89
+ function (args) {
90
+ return sendCommand("tab_close", { tabId: args.tabId }).then(function () {
91
+ return { content: [{ type: "text", text: "Closed tab " + args.tabId }] };
92
+ });
93
+ }
94
+ ));
95
+
96
+ // --- browser_navigate ---
97
+ tools.push(tool(
98
+ "browser_navigate",
99
+ "Navigate a tab to a new URL",
100
+ buildShape({
101
+ tabId: { type: "number", description: "Tab ID" },
102
+ url: { type: "string", description: "URL to navigate to" },
103
+ }, ["tabId", "url"]),
104
+ function (args) {
105
+ return sendCommand("tab_navigate", { tabId: args.tabId, url: args.url }).then(function () {
106
+ return { content: [{ type: "text", text: "Navigated tab " + args.tabId + " to " + args.url }] };
107
+ });
108
+ }
109
+ ));
110
+
111
+ // --- browser_screenshot ---
112
+ tools.push(tool(
113
+ "browser_screenshot",
114
+ "Capture a screenshot of a browser tab (full viewport or a specific element)",
115
+ buildShape({
116
+ tabId: { type: "number", description: "Tab ID" },
117
+ selector: { type: "string", description: "CSS selector to capture a specific element (optional)" },
118
+ }, ["tabId"]),
119
+ function (args) {
120
+ var extArgs = { tabId: args.tabId };
121
+ if (args.selector) extArgs.selector = args.selector;
122
+ return sendCommand("tab_screenshot", extArgs, 10000).then(function (result) {
123
+ if (!result || !result.image) throw new Error("Screenshot failed");
124
+ return {
125
+ content: [
126
+ { type: "image", data: result.image, mimeType: "image/png" },
127
+ { type: "text", text: "Screenshot captured" + (args.selector ? " (selector: " + args.selector + ")" : " (full viewport)") },
128
+ ],
129
+ };
130
+ });
131
+ }
132
+ ));
133
+
134
+ // --- browser_console ---
135
+ tools.push(tool(
136
+ "browser_console",
137
+ "Read captured console logs from a tab (log, warn, error, info)",
138
+ buildShape({
139
+ tabId: { type: "number", description: "Tab ID" },
140
+ }, ["tabId"]),
141
+ function (args) {
142
+ return ensureInjected(args.tabId).then(function () {
143
+ return sendCommand("tab_console", { tabId: args.tabId });
144
+ }).then(function (result) {
145
+ var logs = [];
146
+ try { logs = typeof result.logs === "string" ? JSON.parse(result.logs) : (result.logs || []); } catch (e) {}
147
+ if (logs.length === 0) return { content: [{ type: "text", text: "No console output captured" }] };
148
+ var lines = logs.map(function (entry) {
149
+ var ts = entry.ts ? new Date(entry.ts).toTimeString().slice(0, 8) : "";
150
+ return "[" + ts + " " + (entry.level || "log").toUpperCase() + "] " + (entry.text || "");
151
+ });
152
+ return { content: [{ type: "text", text: lines.join("\n") }] };
153
+ });
154
+ }
155
+ ));
156
+
157
+ // --- browser_network ---
158
+ tools.push(tool(
159
+ "browser_network",
160
+ "Read captured network requests (fetch/XHR) from a tab",
161
+ buildShape({
162
+ tabId: { type: "number", description: "Tab ID" },
163
+ }, ["tabId"]),
164
+ function (args) {
165
+ return ensureInjected(args.tabId).then(function () {
166
+ return sendCommand("tab_network", { tabId: args.tabId });
167
+ }).then(function (result) {
168
+ var reqs = [];
169
+ try { reqs = typeof result.network === "string" ? JSON.parse(result.network) : (result.network || []); } catch (e) {}
170
+ if (reqs.length === 0) return { content: [{ type: "text", text: "No network requests captured" }] };
171
+ var lines = reqs.map(function (r) {
172
+ var line = (r.method || "GET") + " " + (r.url || "") + " " + (r.status || 0) + " " + (r.duration || 0) + "ms";
173
+ if (r.error) line += " [" + r.error + "]";
174
+ return line;
175
+ });
176
+ return { content: [{ type: "text", text: lines.join("\n") }] };
177
+ });
178
+ }
179
+ ));
180
+
181
+ // --- browser_read_page ---
182
+ tools.push(tool(
183
+ "browser_read_page",
184
+ "Read page text content (innerText). Optionally read only a specific element.",
185
+ buildShape({
186
+ tabId: { type: "number", description: "Tab ID" },
187
+ selector: { type: "string", description: "CSS selector to read specific element (optional)" },
188
+ }, ["tabId"]),
189
+ function (args) {
190
+ if (args.selector) {
191
+ var script = "(function() { var el = document.querySelector(" + JSON.stringify(args.selector) + "); if (!el) return ''; var t = el.innerText; return t.length > 32768 ? t.substring(0, 32768) : t; })()";
192
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
193
+ var text = result.value || "";
194
+ if (!text) return { content: [{ type: "text", text: "Element not found or empty: " + args.selector }] };
195
+ return { content: [{ type: "text", text: text }] };
196
+ });
197
+ }
198
+ return sendCommand("tab_page_text", { tabId: args.tabId }).then(function (result) {
199
+ var text = result.text || "";
200
+ if (!text) return { content: [{ type: "text", text: "Page has no text content" }] };
201
+ return { content: [{ type: "text", text: text }] };
202
+ });
203
+ }
204
+ ));
205
+
206
+ // --- browser_dom ---
207
+ tools.push(tool(
208
+ "browser_dom",
209
+ "Get a simplified DOM tree (tag, id, class, children) for structural analysis",
210
+ buildShape({
211
+ tabId: { type: "number", description: "Tab ID" },
212
+ selector: { type: "string", description: "CSS selector for root element (default body)" },
213
+ depth: { type: "number", description: "Max tree depth (default 3)" },
214
+ }, ["tabId"]),
215
+ function (args) {
216
+ var selector = args.selector ? JSON.stringify(args.selector) : '"body"';
217
+ var depth = args.depth || 3;
218
+ var script = "(function() {" +
219
+ "function walk(el, d, max) {" +
220
+ " if (!el || d > max) return null;" +
221
+ " var n = { tag: el.tagName.toLowerCase() };" +
222
+ " if (el.id) n.id = el.id;" +
223
+ " if (el.className && typeof el.className === 'string') { var c = el.className.trim(); if (c) n.class = c; }" +
224
+ " if (el.children.length > 0 && d < max) {" +
225
+ " n.children = [];" +
226
+ " for (var i = 0; i < el.children.length; i++) {" +
227
+ " var child = walk(el.children[i], d + 1, max);" +
228
+ " if (child) n.children.push(child);" +
229
+ " }" +
230
+ " } else if (el.children.length > 0) {" +
231
+ " n.childCount = el.children.length;" +
232
+ " }" +
233
+ " if (el.children.length === 0 && el.textContent) {" +
234
+ " var t = el.textContent.trim();" +
235
+ " if (t.length > 100) t = t.substring(0, 100) + '...';" +
236
+ " if (t) n.text = t;" +
237
+ " }" +
238
+ " return n;" +
239
+ "}" +
240
+ "var root = document.querySelector(" + selector + ") || document.body;" +
241
+ "return JSON.stringify(walk(root, 0, " + depth + "), null, 2);" +
242
+ "})()";
243
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
244
+ return { content: [{ type: "text", text: result.value || "null" }] };
245
+ });
246
+ }
247
+ ));
248
+
249
+ // --- browser_styles ---
250
+ tools.push(tool(
251
+ "browser_styles",
252
+ "Get computed styles of an element (display, position, size, colors, etc.)",
253
+ buildShape({
254
+ tabId: { type: "number", description: "Tab ID" },
255
+ selector: { type: "string", description: "CSS selector" },
256
+ }, ["tabId", "selector"]),
257
+ function (args) {
258
+ var script = "(function() {" +
259
+ "var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
260
+ "if (!el) return JSON.stringify({ error: 'Element not found' });" +
261
+ "var cs = window.getComputedStyle(el);" +
262
+ "var props = ['display','visibility','opacity','position','top','right','bottom','left'," +
263
+ "'width','height','minWidth','minHeight','maxWidth','maxHeight'," +
264
+ "'margin','padding','border','borderRadius'," +
265
+ "'color','backgroundColor','fontSize','fontFamily','fontWeight'," +
266
+ "'overflow','zIndex','transform','transition','boxShadow','cursor'];" +
267
+ "var result = {};" +
268
+ "for (var i = 0; i < props.length; i++) { result[props[i]] = cs[props[i]]; }" +
269
+ "result.boundingRect = el.getBoundingClientRect().toJSON();" +
270
+ "return JSON.stringify(result, null, 2);" +
271
+ "})()";
272
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
273
+ return { content: [{ type: "text", text: result.value || "null" }] };
274
+ });
275
+ }
276
+ ));
277
+
278
+ // --- browser_storage ---
279
+ tools.push(tool(
280
+ "browser_storage",
281
+ "Read browser storage (localStorage, sessionStorage, or cookies)",
282
+ buildShape({
283
+ tabId: { type: "number", description: "Tab ID" },
284
+ type: { type: "string", enum: ["local", "session", "cookie"], description: "Storage type (default local)" },
285
+ }, ["tabId"]),
286
+ function (args) {
287
+ var storageType = args.type || "local";
288
+ var script;
289
+ if (storageType === "cookie") {
290
+ script = "JSON.stringify(document.cookie.split('; ').reduce(function(o, c) { var p = c.split('='); o[p[0]] = decodeURIComponent(p.slice(1).join('=')); return o; }, {}), null, 2)";
291
+ } else if (storageType === "session") {
292
+ script = "(function() { var o = {}; for (var i = 0; i < sessionStorage.length; i++) { var k = sessionStorage.key(i); o[k] = sessionStorage.getItem(k); } return JSON.stringify(o, null, 2); })()";
293
+ } else {
294
+ script = "(function() { var o = {}; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); o[k] = localStorage.getItem(k); } return JSON.stringify(o, null, 2); })()";
295
+ }
296
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
297
+ return { content: [{ type: "text", text: result.value || "{}" }] };
298
+ });
299
+ }
300
+ ));
301
+
302
+ // --- browser_evaluate ---
303
+ tools.push(tool(
304
+ "browser_evaluate",
305
+ "Execute arbitrary JavaScript in the page context and return the result",
306
+ buildShape({
307
+ tabId: { type: "number", description: "Tab ID" },
308
+ script: { type: "string", description: "JavaScript expression or IIFE to evaluate" },
309
+ }, ["tabId", "script"]),
310
+ function (args) {
311
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: args.script }).then(function (result) {
312
+ if (result.error) throw new Error(result.error);
313
+ var text = typeof result.value === "string" ? result.value : JSON.stringify(result.value, null, 2);
314
+ return { content: [{ type: "text", text: text || "(undefined)" }] };
315
+ });
316
+ }
317
+ ));
318
+
319
+ // --- browser_click ---
320
+ tools.push(tool(
321
+ "browser_click",
322
+ "Click an element on the page",
323
+ buildShape({
324
+ tabId: { type: "number", description: "Tab ID" },
325
+ selector: { type: "string", description: "CSS selector of the element to click" },
326
+ }, ["tabId", "selector"]),
327
+ function (args) {
328
+ var script = "(function() {" +
329
+ "var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
330
+ "if (!el) return 'Element not found: " + args.selector.replace(/'/g, "\\'") + "';" +
331
+ "el.scrollIntoView({ block: 'center', behavior: 'instant' });" +
332
+ "el.click();" +
333
+ "return 'Clicked: " + args.selector.replace(/'/g, "\\'") + "';" +
334
+ "})()";
335
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
336
+ return { content: [{ type: "text", text: result.value || "Click executed" }] };
337
+ });
338
+ }
339
+ ));
340
+
341
+ // --- browser_type ---
342
+ tools.push(tool(
343
+ "browser_type",
344
+ "Type text into an input element (sets value and dispatches input/change events)",
345
+ buildShape({
346
+ tabId: { type: "number", description: "Tab ID" },
347
+ selector: { type: "string", description: "CSS selector of the input element" },
348
+ text: { type: "string", description: "Text to type" },
349
+ }, ["tabId", "selector", "text"]),
350
+ function (args) {
351
+ var script = "(function() {" +
352
+ "var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
353
+ "if (!el) return 'Element not found: " + args.selector.replace(/'/g, "\\'") + "';" +
354
+ "el.focus();" +
355
+ "var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value') || Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');" +
356
+ "if (nativeSetter && nativeSetter.set) { nativeSetter.set.call(el, " + JSON.stringify(args.text) + "); }" +
357
+ "else { el.value = " + JSON.stringify(args.text) + "; }" +
358
+ "el.dispatchEvent(new Event('input', { bubbles: true }));" +
359
+ "el.dispatchEvent(new Event('change', { bubbles: true }));" +
360
+ "return 'Typed into: " + args.selector.replace(/'/g, "\\'") + "';" +
361
+ "})()";
362
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
363
+ return { content: [{ type: "text", text: result.value || "Type executed" }] };
364
+ });
365
+ }
366
+ ));
367
+
368
+ // --- browser_scroll ---
369
+ tools.push(tool(
370
+ "browser_scroll",
371
+ "Scroll the page or scroll a specific element into view",
372
+ buildShape({
373
+ tabId: { type: "number", description: "Tab ID" },
374
+ selector: { type: "string", description: "CSS selector to scroll into view (optional)" },
375
+ x: { type: "number", description: "Horizontal scroll position (optional)" },
376
+ y: { type: "number", description: "Vertical scroll position (optional)" },
377
+ }, ["tabId"]),
378
+ function (args) {
379
+ var script;
380
+ if (args.selector) {
381
+ script = "(function() {" +
382
+ "var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
383
+ "if (!el) return 'Element not found';" +
384
+ "el.scrollIntoView({ block: 'center', behavior: 'smooth' });" +
385
+ "return 'Scrolled to: " + args.selector.replace(/'/g, "\\'") + "';" +
386
+ "})()";
387
+ } else {
388
+ var x = args.x || 0;
389
+ var y = args.y || 0;
390
+ script = "(function() { window.scrollTo(" + x + ", " + y + "); return 'Scrolled to (" + x + ", " + y + ")'; })()";
391
+ }
392
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
393
+ return { content: [{ type: "text", text: result.value || "Scroll executed" }] };
394
+ });
395
+ }
396
+ ));
397
+
398
+ // --- browser_wait ---
399
+ tools.push(tool(
400
+ "browser_wait",
401
+ "Wait for an element matching a CSS selector to appear in the DOM",
402
+ buildShape({
403
+ tabId: { type: "number", description: "Tab ID" },
404
+ selector: { type: "string", description: "CSS selector to wait for" },
405
+ timeout: { type: "number", description: "Timeout in ms (default 5000)" },
406
+ }, ["tabId", "selector"]),
407
+ function (args) {
408
+ var timeout = args.timeout || 5000;
409
+ var script = "(function() {" +
410
+ "var el = document.querySelector(" + JSON.stringify(args.selector) + ");" +
411
+ "if (el) return JSON.stringify({ found: true, tag: el.tagName.toLowerCase() });" +
412
+ "return JSON.stringify({ found: false });" +
413
+ "})()";
414
+ var startTime = Date.now();
415
+ function poll() {
416
+ return sendCommand("tab_evaluate", { tabId: args.tabId, script: script }).then(function (result) {
417
+ var parsed = {};
418
+ try { parsed = JSON.parse(result.value || "{}"); } catch (e) {}
419
+ if (parsed.found) {
420
+ return { content: [{ type: "text", text: "Element found: " + args.selector + " (" + parsed.tag + ")" }] };
421
+ }
422
+ if (Date.now() - startTime >= timeout) {
423
+ throw new Error("Timeout waiting for element: " + args.selector + " (" + timeout + "ms)");
424
+ }
425
+ return new Promise(function (resolve) {
426
+ setTimeout(function () { resolve(poll()); }, 300);
427
+ });
428
+ });
429
+ }
430
+ return poll();
431
+ }
432
+ ));
433
+
434
+ // --- browser_wait_navigation ---
435
+ tools.push(tool(
436
+ "browser_wait_navigation",
437
+ "Wait for page navigation to complete (URL change + load event)",
438
+ buildShape({
439
+ tabId: { type: "number", description: "Tab ID" },
440
+ timeout: { type: "number", description: "Timeout in ms (default 10000)" },
441
+ }, ["tabId"]),
442
+ function (args) {
443
+ var timeout = args.timeout || 10000;
444
+ return sendCommand("tab_wait_navigation", { tabId: args.tabId, timeout: timeout }, timeout + 3000).then(function (result) {
445
+ if (result.error) throw new Error(result.error);
446
+ return { content: [{ type: "text", text: "Navigation complete: " + (result.url || "unknown URL") }] };
447
+ });
448
+ }
449
+ ));
450
+
451
+ // --- browser_watch_tab ---
452
+ if (contextOps && contextOps.watchTab) {
453
+ tools.push(tool(
454
+ "browser_watch_tab",
455
+ "Add a browser tab as a persistent context source. Its screenshot and text will be automatically included in every subsequent message.",
456
+ buildShape({
457
+ tabId: { type: "number", description: "Tab ID to watch" },
458
+ }, ["tabId"]),
459
+ function (args) {
460
+ var tabs = getTabList ? getTabList() : [];
461
+ var found = null;
462
+ for (var i = 0; i < tabs.length; i++) {
463
+ if (tabs[i].id === args.tabId) { found = tabs[i]; break; }
464
+ }
465
+ if (!found) throw new Error("Tab " + args.tabId + " not found in open tabs");
466
+ var active = contextOps.watchTab(args.tabId);
467
+ return Promise.resolve({
468
+ content: [{ type: "text", text: "Now watching tab " + args.tabId + " (" + (found.title || found.url) + "). Its content will be included as context in every message. Active sources: " + active.join(", ") }],
469
+ });
470
+ }
471
+ ));
472
+
473
+ tools.push(tool(
474
+ "browser_unwatch_tab",
475
+ "Remove a browser tab from persistent context sources. Stops auto-including its content.",
476
+ buildShape({
477
+ tabId: { type: "number", description: "Tab ID to stop watching" },
478
+ }, ["tabId"]),
479
+ function (args) {
480
+ var active = contextOps.unwatchTab(args.tabId);
481
+ return Promise.resolve({
482
+ content: [{ type: "text", text: "Stopped watching tab " + args.tabId + ". Active sources: " + (active.length > 0 ? active.join(", ") : "none") }],
483
+ });
484
+ }
485
+ ));
486
+ }
487
+
488
+ // Create the in-process MCP server
489
+ return createSdkMcpServer({
490
+ name: "clay-browser",
491
+ version: "1.0.0",
492
+ tools: tools,
493
+ });
494
+ }
495
+
496
+ module.exports = { create: create };