@_nazmiforreal/agent-browser-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.mcp.json +12 -0
  2. package/AGENTS.md +100 -0
  3. package/README.md +502 -0
  4. package/dist/browsers/discover.d.ts +13 -0
  5. package/dist/browsers/discover.d.ts.map +1 -0
  6. package/dist/browsers/discover.js +54 -0
  7. package/dist/browsers/discover.js.map +1 -0
  8. package/dist/browsers/env.d.ts +10 -0
  9. package/dist/browsers/env.d.ts.map +1 -0
  10. package/dist/browsers/env.js +72 -0
  11. package/dist/browsers/env.js.map +1 -0
  12. package/dist/browsers/index.d.ts +5 -0
  13. package/dist/browsers/index.d.ts.map +1 -0
  14. package/dist/browsers/index.js +5 -0
  15. package/dist/browsers/index.js.map +1 -0
  16. package/dist/browsers/installer.d.ts +13 -0
  17. package/dist/browsers/installer.d.ts.map +1 -0
  18. package/dist/browsers/installer.js +59 -0
  19. package/dist/browsers/installer.js.map +1 -0
  20. package/dist/browsers/registry.d.ts +14 -0
  21. package/dist/browsers/registry.d.ts.map +1 -0
  22. package/dist/browsers/registry.js +119 -0
  23. package/dist/browsers/registry.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +153 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/tools/browser.d.ts +3 -0
  33. package/dist/tools/browser.d.ts.map +1 -0
  34. package/dist/tools/browser.js +885 -0
  35. package/dist/tools/browser.js.map +1 -0
  36. package/dist/tools/executor.d.ts +9 -0
  37. package/dist/tools/executor.d.ts.map +1 -0
  38. package/dist/tools/executor.js +1067 -0
  39. package/dist/tools/executor.js.map +1 -0
  40. package/dist/tools/index.d.ts +3 -0
  41. package/dist/tools/index.d.ts.map +1 -0
  42. package/dist/tools/index.js +3 -0
  43. package/dist/tools/index.js.map +1 -0
  44. package/package.json +60 -0
  45. package/tests/browser-tools.test.ts +329 -0
  46. package/tests/executor.test.ts +356 -0
  47. package/tests/integration.test.ts +467 -0
  48. package/tests/server.test.ts +224 -0
  49. package/vitest.config.ts +15 -0
@@ -0,0 +1,885 @@
1
+ import { z } from "zod";
2
+ import { execBrowser } from "./executor.js";
3
+ import { listBrowsers, installBrowser } from "../browsers/installer.js";
4
+ import { discoverBrowsers, discoverActiveBrowser, selectBrowser } from "../browsers/discover.js";
5
+ export function registerBrowserTools(server) {
6
+ // ============================================================
7
+ // Navigation (5 tools)
8
+ // ============================================================
9
+ server.tool("browser_navigate", "Navigate to a URL (or launch browser if no URL)", {
10
+ url: z.string().url().describe("The URL to navigate to"),
11
+ sessionId: z.string().optional().describe("Browser session ID"),
12
+ }, async ({ url, sessionId }) => {
13
+ const result = await execBrowser("navigate", { url }, sessionId);
14
+ return { content: [{ type: "text", text: result }] };
15
+ });
16
+ server.tool("browser_go_back", "Go back in browser history", { sessionId: z.string().optional().describe("Browser session ID") }, async ({ sessionId }) => {
17
+ const result = await execBrowser("go_back", {}, sessionId);
18
+ return { content: [{ type: "text", text: result }] };
19
+ });
20
+ server.tool("browser_go_forward", "Go forward in browser history", { sessionId: z.string().optional().describe("Browser session ID") }, async ({ sessionId }) => {
21
+ const result = await execBrowser("go_forward", {}, sessionId);
22
+ return { content: [{ type: "text", text: result }] };
23
+ });
24
+ server.tool("browser_reload", "Reload the current page", { sessionId: z.string().optional().describe("Browser session ID") }, async ({ sessionId }) => {
25
+ const result = await execBrowser("reload", {}, sessionId);
26
+ return { content: [{ type: "text", text: result }] };
27
+ });
28
+ server.tool("browser_pushstate", "SPA client-side navigation via history.pushState", {
29
+ url: z.string().describe("URL to push onto the history stack"),
30
+ sessionId: z.string().optional().describe("Browser session ID"),
31
+ }, async ({ url, sessionId }) => {
32
+ const result = await execBrowser("pushstate", { url }, sessionId);
33
+ return { content: [{ type: "text", text: result }] };
34
+ });
35
+ // ============================================================
36
+ // Core Interaction (13 tools)
37
+ // ============================================================
38
+ server.tool("browser_click", "Click an element by CSS selector", {
39
+ selector: z.string().describe("CSS selector of element to click"),
40
+ newTab: z.boolean().optional().describe("Open link in new tab"),
41
+ sessionId: z.string().optional().describe("Browser session ID"),
42
+ }, async ({ selector, newTab, sessionId }) => {
43
+ const result = await execBrowser("click", { selector, newTab }, sessionId);
44
+ return { content: [{ type: "text", text: result }] };
45
+ });
46
+ server.tool("browser_dblclick", "Double-click an element", {
47
+ selector: z.string().describe("CSS selector of element to double-click"),
48
+ sessionId: z.string().optional().describe("Browser session ID"),
49
+ }, async ({ selector, sessionId }) => {
50
+ const result = await execBrowser("dblclick", { selector }, sessionId);
51
+ return { content: [{ type: "text", text: result }] };
52
+ });
53
+ server.tool("browser_fill", "Clear and fill an input field", {
54
+ selector: z.string().describe("CSS selector of the input element"),
55
+ value: z.string().describe("Text value to fill in"),
56
+ sessionId: z.string().optional().describe("Browser session ID"),
57
+ }, async ({ selector, value, sessionId }) => {
58
+ const result = await execBrowser("fill", { selector, value }, sessionId);
59
+ return { content: [{ type: "text", text: result }] };
60
+ });
61
+ server.tool("browser_type", "Type text into an element (does not clear first)", {
62
+ selector: z.string().describe("CSS selector of the input element"),
63
+ text: z.string().describe("Text to type"),
64
+ sessionId: z.string().optional().describe("Browser session ID"),
65
+ }, async ({ selector, text, sessionId }) => {
66
+ const result = await execBrowser("type", { selector, text }, sessionId);
67
+ return { content: [{ type: "text", text: result }] };
68
+ });
69
+ server.tool("browser_hover", "Hover over an element", {
70
+ selector: z.string().describe("CSS selector of element to hover"),
71
+ sessionId: z.string().optional().describe("Browser session ID"),
72
+ }, async ({ selector, sessionId }) => {
73
+ const result = await execBrowser("hover", { selector }, sessionId);
74
+ return { content: [{ type: "text", text: result }] };
75
+ });
76
+ server.tool("browser_focus", "Focus an element", {
77
+ selector: z.string().describe("CSS selector of element to focus"),
78
+ sessionId: z.string().optional().describe("Browser session ID"),
79
+ }, async ({ selector, sessionId }) => {
80
+ const result = await execBrowser("focus", { selector }, sessionId);
81
+ return { content: [{ type: "text", text: result }] };
82
+ });
83
+ server.tool("browser_check", "Check a checkbox or radio element", {
84
+ selector: z.string().describe("CSS selector of the checkbox/radio element"),
85
+ sessionId: z.string().optional().describe("Browser session ID"),
86
+ }, async ({ selector, sessionId }) => {
87
+ const result = await execBrowser("check", { selector }, sessionId);
88
+ return { content: [{ type: "text", text: result }] };
89
+ });
90
+ server.tool("browser_uncheck", "Uncheck a checkbox element", {
91
+ selector: z.string().describe("CSS selector of the checkbox element"),
92
+ sessionId: z.string().optional().describe("Browser session ID"),
93
+ }, async ({ selector, sessionId }) => {
94
+ const result = await execBrowser("uncheck", { selector }, sessionId);
95
+ return { content: [{ type: "text", text: result }] };
96
+ });
97
+ server.tool("browser_select", "Select option(s) from a dropdown", {
98
+ selector: z.string().describe("CSS selector of the select element"),
99
+ values: z.union([z.string(), z.array(z.string())]).describe("Value(s) to select"),
100
+ sessionId: z.string().optional().describe("Browser session ID"),
101
+ }, async ({ selector, values, sessionId }) => {
102
+ const result = await execBrowser("select", { selector, values }, sessionId);
103
+ return { content: [{ type: "text", text: result }] };
104
+ });
105
+ server.tool("browser_drag", "Drag and drop from source to target", {
106
+ source: z.string().describe("CSS selector of the source element"),
107
+ target: z.string().describe("CSS selector of the target element"),
108
+ sessionId: z.string().optional().describe("Browser session ID"),
109
+ }, async ({ source, target, sessionId }) => {
110
+ const result = await execBrowser("drag", { source, target }, sessionId);
111
+ return { content: [{ type: "text", text: result }] };
112
+ });
113
+ server.tool("browser_upload", "Upload file(s) via a file input element", {
114
+ selector: z.string().describe("CSS selector of the file input element"),
115
+ files: z.array(z.string()).describe("File path(s) to upload"),
116
+ sessionId: z.string().optional().describe("Browser session ID"),
117
+ }, async ({ selector, files, sessionId }) => {
118
+ const result = await execBrowser("upload", { selector, files }, sessionId);
119
+ return { content: [{ type: "text", text: result }] };
120
+ });
121
+ server.tool("browser_download", "Download a file by clicking a download link/button", {
122
+ selector: z.string().describe("CSS selector of the element that triggers download"),
123
+ path: z.string().describe("File path to save the download to"),
124
+ sessionId: z.string().optional().describe("Browser session ID"),
125
+ }, async ({ selector, path, sessionId }) => {
126
+ const result = await execBrowser("download", { selector, path }, sessionId);
127
+ return { content: [{ type: "text", text: result }] };
128
+ });
129
+ // ============================================================
130
+ // Keyboard (1 tool)
131
+ // ============================================================
132
+ server.tool("browser_keyboard", "Keyboard actions: press key, key down/up, type text, insert text", {
133
+ action: z.enum(["press", "keydown", "keyup", "type", "inserttext"])
134
+ .describe("Keyboard action to perform"),
135
+ key: z.string().optional().describe("Key name (for press/keydown/keyup, e.g. 'Enter', 'Tab', 'Control+a')"),
136
+ text: z.string().optional().describe("Text to type or insert (for type/inserttext)"),
137
+ sessionId: z.string().optional().describe("Browser session ID"),
138
+ }, async ({ action, key, text, sessionId }) => {
139
+ let command;
140
+ let options;
141
+ if (action === "press" || action === "keydown" || action === "keyup") {
142
+ command = action;
143
+ options = { key };
144
+ }
145
+ else if (action === "type") {
146
+ command = "keyboard_type";
147
+ options = { text };
148
+ }
149
+ else {
150
+ command = "keyboard_inserttext";
151
+ options = { text };
152
+ }
153
+ const result = await execBrowser(command, options, sessionId);
154
+ return { content: [{ type: "text", text: result }] };
155
+ });
156
+ // ============================================================
157
+ // Mouse (1 tool)
158
+ // ============================================================
159
+ server.tool("browser_mouse", "Mouse actions: move, down, up, wheel", {
160
+ action: z.enum(["move", "down", "up", "wheel"]).describe("Mouse action to perform"),
161
+ x: z.number().optional().describe("X coordinate (for move)"),
162
+ y: z.number().optional().describe("Y coordinate (for move)"),
163
+ button: z.enum(["left", "right", "middle"]).optional().describe("Mouse button (for down/up)"),
164
+ deltaY: z.number().optional().describe("Vertical scroll amount (for wheel)"),
165
+ deltaX: z.number().optional().describe("Horizontal scroll amount (for wheel, defaults to 0)"),
166
+ sessionId: z.string().optional().describe("Browser session ID"),
167
+ }, async ({ action, x, y, button, deltaY, deltaX, sessionId }) => {
168
+ const command = `mouse_${action}`;
169
+ const result = await execBrowser(command, { x, y, button, deltaY, deltaX }, sessionId);
170
+ return { content: [{ type: "text", text: result }] };
171
+ });
172
+ // ============================================================
173
+ // Scroll (2 tools)
174
+ // ============================================================
175
+ server.tool("browser_scroll", "Scroll the page in a direction", {
176
+ direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
177
+ amount: z.number().optional().describe("Scroll amount in pixels"),
178
+ selector: z.string().optional().describe("CSS selector to scroll within"),
179
+ sessionId: z.string().optional().describe("Browser session ID"),
180
+ }, async ({ direction, amount, selector, sessionId }) => {
181
+ const result = await execBrowser("scroll", { direction, amount, selector }, sessionId);
182
+ return { content: [{ type: "text", text: result }] };
183
+ });
184
+ server.tool("browser_scroll_into_view", "Scroll an element into view", {
185
+ selector: z.string().describe("CSS selector of the element to scroll into view"),
186
+ sessionId: z.string().optional().describe("Browser session ID"),
187
+ }, async ({ selector, sessionId }) => {
188
+ const result = await execBrowser("scroll_into_view", { selector }, sessionId);
189
+ return { content: [{ type: "text", text: result }] };
190
+ });
191
+ // ============================================================
192
+ // Get Info (1 tool)
193
+ // ============================================================
194
+ const GET_COMMANDS = {
195
+ text: "get_text",
196
+ html: "get_html",
197
+ value: "get_value",
198
+ attr: "get_attribute",
199
+ url: "get_url",
200
+ title: "get_title",
201
+ "cdp-url": "get_cdp_url",
202
+ count: "get_count",
203
+ box: "get_bounding_box",
204
+ styles: "get_styles",
205
+ };
206
+ server.tool("browser_get", "Get page information (text, HTML, attribute, URL, title, etc.)", {
207
+ property: z.enum(["text", "html", "value", "attr", "url", "title", "cdp-url", "count", "box", "styles"])
208
+ .describe("What information to retrieve"),
209
+ selector: z.string().optional().describe("CSS selector (required for text/html/value/attr/count/box/styles)"),
210
+ attribute: z.string().optional().describe("Attribute name (only for property='attr')"),
211
+ sessionId: z.string().optional().describe("Browser session ID"),
212
+ }, async ({ property, selector, attribute, sessionId }) => {
213
+ const command = GET_COMMANDS[property];
214
+ const result = await execBrowser(command, { selector, attribute }, sessionId);
215
+ return { content: [{ type: "text", text: result }] };
216
+ });
217
+ // ============================================================
218
+ // State Checks (1 tool)
219
+ // ============================================================
220
+ server.tool("browser_is", "Check element state: visible, enabled, checked", {
221
+ state: z.enum(["visible", "enabled", "checked"]).describe("State to check"),
222
+ selector: z.string().describe("CSS selector of the element"),
223
+ sessionId: z.string().optional().describe("Browser session ID"),
224
+ }, async ({ state, selector, sessionId }) => {
225
+ const command = `is_${state}`;
226
+ const result = await execBrowser(command, { selector }, sessionId);
227
+ return { content: [{ type: "text", text: result }] };
228
+ });
229
+ // ============================================================
230
+ // Find / Locators (1 tool)
231
+ // ============================================================
232
+ server.tool("browser_find", "Find and optionally interact with elements by various locator strategies", {
233
+ by: z.enum(["role", "text", "label", "placeholder", "alt", "title", "testid", "first", "last", "nth"])
234
+ .describe("Locator strategy"),
235
+ value: z.string().describe("The value to search for (for 'nth', this is the CSS selector)"),
236
+ subaction: z.string().optional().describe("Action to perform on the located element (default: click)"),
237
+ name: z.string().optional().describe("Accessible name filter (for by='role')"),
238
+ exact: z.boolean().optional().describe("Exact match"),
239
+ index: z.number().optional().describe("Index (only for by='nth')"),
240
+ sessionId: z.string().optional().describe("Browser session ID"),
241
+ }, async ({ by, value, subaction, name, exact, index, sessionId }) => {
242
+ const command = by === "nth" ? "find_nth" : `find_${by}`;
243
+ const options = by === "nth"
244
+ ? { index, selector: value, subaction }
245
+ : { value, subaction, name, exact };
246
+ const result = await execBrowser(command, options, sessionId);
247
+ return { content: [{ type: "text", text: result }] };
248
+ });
249
+ // ============================================================
250
+ // Wait (1 tool)
251
+ // ============================================================
252
+ server.tool("browser_wait", "Wait for various conditions: selector visibility, URL, load state, JS function, text, download, or timeout", {
253
+ mode: z.enum(["selector", "timeout", "url", "load-state", "function", "text", "download"])
254
+ .describe("What to wait for"),
255
+ target: z.string().optional().describe("CSS selector or timeout in ms (for selector/timeout mode)"),
256
+ value: z.string().optional().describe("URL pattern (url mode), load state (load-state mode), JS expression (function mode), or text (text mode)"),
257
+ path: z.string().optional().describe("File path (download mode)"),
258
+ timeout: z.number().optional().describe("Maximum time to wait in milliseconds"),
259
+ sessionId: z.string().optional().describe("Browser session ID"),
260
+ }, async ({ mode, target, value, path, timeout, sessionId }) => {
261
+ let command;
262
+ let options;
263
+ switch (mode) {
264
+ case "selector":
265
+ command = "wait_for_selector";
266
+ options = { selector: target, timeout };
267
+ break;
268
+ case "timeout":
269
+ command = "wait";
270
+ options = { target: String(target || timeout), timeout };
271
+ break;
272
+ case "url":
273
+ command = "wait_for_url";
274
+ options = { url: value, timeout };
275
+ break;
276
+ case "load-state":
277
+ command = "wait_for_load_state";
278
+ options = { state: value, timeout };
279
+ break;
280
+ case "function":
281
+ command = "wait_for_function";
282
+ options = { expression: value, timeout };
283
+ break;
284
+ case "text":
285
+ command = "wait_for_text";
286
+ options = { text: value, timeout };
287
+ break;
288
+ case "download":
289
+ command = "wait_for_download";
290
+ options = { path, timeout };
291
+ break;
292
+ default:
293
+ command = "wait";
294
+ options = { target };
295
+ }
296
+ const result = await execBrowser(command, options, sessionId);
297
+ return { content: [{ type: "text", text: result }] };
298
+ });
299
+ // ============================================================
300
+ // Screenshot / PDF (2 tools)
301
+ // ============================================================
302
+ server.tool("browser_screenshot", "Take a screenshot of the page", {
303
+ path: z.string().optional().describe("File path to save the screenshot"),
304
+ fullPage: z.boolean().optional().describe("Capture full scrollable page"),
305
+ selector: z.string().optional().describe("CSS selector to capture specific element"),
306
+ annotate: z.boolean().optional().describe("Annotate screenshot with element labels"),
307
+ format: z.enum(["png", "jpeg"]).optional().describe("Image format"),
308
+ quality: z.number().min(0).max(100).optional().describe("Image quality (0-100)"),
309
+ sessionId: z.string().optional().describe("Browser session ID"),
310
+ }, async ({ path, fullPage, selector, annotate, format, quality, sessionId }) => {
311
+ const result = await execBrowser("screenshot", { path, fullPage, selector, annotate, format, quality }, sessionId);
312
+ return { content: [{ type: "text", text: result }] };
313
+ });
314
+ server.tool("browser_pdf", "Save the page as a PDF", {
315
+ path: z.string().describe("File path to save the PDF"),
316
+ sessionId: z.string().optional().describe("Browser session ID"),
317
+ }, async ({ path, sessionId }) => {
318
+ const result = await execBrowser("pdf", { path }, sessionId);
319
+ return { content: [{ type: "text", text: result }] };
320
+ });
321
+ // ============================================================
322
+ // Snapshot (1 tool)
323
+ // ============================================================
324
+ server.tool("browser_snapshot", "Get the accessibility tree snapshot with element references (for AI)", {
325
+ interactive: z.boolean().optional().describe("Only show interactive elements"),
326
+ compact: z.boolean().optional().describe("Remove empty structural elements"),
327
+ cursor: z.boolean().optional().describe("Show cursor position indicator"),
328
+ urls: z.boolean().optional().describe("Show URLs of links"),
329
+ depth: z.number().optional().describe("Limit tree depth"),
330
+ selector: z.string().optional().describe("Scope snapshot to a CSS selector"),
331
+ sessionId: z.string().optional().describe("Browser session ID"),
332
+ }, async ({ interactive, compact, cursor, urls, depth, selector, sessionId }) => {
333
+ const result = await execBrowser("snapshot", { interactive, compact, cursor, urls, depth, selector }, sessionId);
334
+ return { content: [{ type: "text", text: result }] };
335
+ });
336
+ // ============================================================
337
+ // Eval (2 tools)
338
+ // ============================================================
339
+ server.tool("browser_evaluate", "Execute JavaScript code in the browser context", {
340
+ script: z.string().describe("JavaScript code to execute"),
341
+ sessionId: z.string().optional().describe("Browser session ID"),
342
+ }, async ({ script, sessionId }) => {
343
+ const result = await execBrowser("evaluate", { script }, sessionId);
344
+ return { content: [{ type: "text", text: result }] };
345
+ });
346
+ server.tool("browser_evaluate_base64", "Execute base64-encoded JavaScript in the browser context", {
347
+ script: z.string().describe("Base64-encoded JavaScript code to execute"),
348
+ sessionId: z.string().optional().describe("Browser session ID"),
349
+ }, async ({ script, sessionId }) => {
350
+ const result = await execBrowser("evaluate_base64", { script }, sessionId);
351
+ return { content: [{ type: "text", text: result }] };
352
+ });
353
+ // ============================================================
354
+ // Batch (1 tool)
355
+ // ============================================================
356
+ server.tool("browser_batch", "Execute multiple agent-browser commands sequentially", {
357
+ commands: z.array(z.string()).describe("Array of commands to execute"),
358
+ bail: z.boolean().optional().describe("Stop execution on first error"),
359
+ sessionId: z.string().optional().describe("Browser session ID"),
360
+ }, async ({ commands, bail, sessionId }) => {
361
+ const result = await execBrowser("batch", { commands, bail }, sessionId);
362
+ return { content: [{ type: "text", text: result }] };
363
+ });
364
+ // ============================================================
365
+ // Clipboard (1 tool)
366
+ // ============================================================
367
+ server.tool("browser_clipboard", "Clipboard operations: read, write, copy, paste", {
368
+ action: z.enum(["read", "write", "copy", "paste"]).describe("Clipboard action"),
369
+ text: z.string().optional().describe("Text to write (for write action)"),
370
+ sessionId: z.string().optional().describe("Browser session ID"),
371
+ }, async ({ action, text, sessionId }) => {
372
+ const command = `clipboard_${action}`;
373
+ const result = await execBrowser(command, { text }, sessionId);
374
+ return { content: [{ type: "text", text: result }] };
375
+ });
376
+ // ============================================================
377
+ // Settings (1 tool)
378
+ // ============================================================
379
+ server.tool("browser_settings", "Browser settings: viewport, device emulation, geolocation, offline, headers, credentials, media", {
380
+ setting: z.enum(["viewport", "device", "geo", "offline", "headers", "credentials", "media"])
381
+ .describe("Setting to change"),
382
+ width: z.number().optional().describe("Viewport width (for viewport)"),
383
+ height: z.number().optional().describe("Viewport height (for viewport)"),
384
+ scale: z.number().optional().describe("Device scale factor (for viewport)"),
385
+ device: z.string().optional().describe("Device name (for device)"),
386
+ latitude: z.number().optional().describe("Latitude (for geo)"),
387
+ longitude: z.number().optional().describe("Longitude (for geo)"),
388
+ offline: z.boolean().optional().describe("Enable/disable offline mode (for offline)"),
389
+ headers: z.string().optional().describe("Headers as JSON string (for headers)"),
390
+ username: z.string().optional().describe("Username (for credentials)"),
391
+ password: z.string().optional().describe("Password (for credentials)"),
392
+ colorScheme: z.enum(["dark", "light", "no-preference"]).optional().describe("Color scheme (for media)"),
393
+ reducedMotion: z.boolean().optional().describe("Reduced motion (for media)"),
394
+ sessionId: z.string().optional().describe("Browser session ID"),
395
+ }, async ({ setting, width, height, scale, device, latitude, longitude, offline, headers, username, password, colorScheme, reducedMotion, sessionId }) => {
396
+ const command = `set_${setting === "geo" ? "geolocation" : setting}`;
397
+ const result = await execBrowser(command, { width, height, scale, device, latitude, longitude, offline, headers, username, password, colorScheme, reducedMotion }, sessionId);
398
+ return { content: [{ type: "text", text: result }] };
399
+ });
400
+ // ============================================================
401
+ // Cookies (1 tool)
402
+ // ============================================================
403
+ server.tool("browser_cookies", "Cookie operations: get, set individual cookie, set from file, clear all", {
404
+ action: z.enum(["get", "set", "set-file", "clear"]).describe("Cookie action"),
405
+ name: z.string().optional().describe("Cookie name (for set)"),
406
+ value: z.string().optional().describe("Cookie value (for set)"),
407
+ url: z.string().optional().describe("Cookie URL (for set/set-file)"),
408
+ domain: z.string().optional().describe("Cookie domain (for set/set-file)"),
409
+ path: z.string().optional().describe("Cookie path (for set)"),
410
+ httpOnly: z.boolean().optional().describe("HTTP-only flag (for set)"),
411
+ secure: z.boolean().optional().describe("Secure flag (for set)"),
412
+ sameSite: z.enum(["Strict", "Lax", "None"]).optional().describe("SameSite attribute (for set)"),
413
+ expires: z.number().optional().describe("Expiration timestamp in seconds (for set)"),
414
+ file: z.string().optional().describe("Path to cookie file (for set-file)"),
415
+ sessionId: z.string().optional().describe("Browser session ID"),
416
+ }, async ({ action, name, value, url, domain, path, httpOnly, secure, sameSite, expires, file, sessionId }) => {
417
+ let command;
418
+ let options;
419
+ switch (action) {
420
+ case "get":
421
+ command = "get_cookies";
422
+ options = {};
423
+ break;
424
+ case "set":
425
+ command = "set_cookie";
426
+ options = { name, value, url, domain, path, httpOnly, secure, sameSite, expires };
427
+ break;
428
+ case "set-file":
429
+ command = "set_cookies_from_file";
430
+ options = { file, domain, url };
431
+ break;
432
+ case "clear":
433
+ command = "clear_cookies";
434
+ options = {};
435
+ break;
436
+ default:
437
+ command = "get_cookies";
438
+ options = {};
439
+ }
440
+ const result = await execBrowser(command, options, sessionId);
441
+ return { content: [{ type: "text", text: result }] };
442
+ });
443
+ // ============================================================
444
+ // Storage (1 tool)
445
+ // ============================================================
446
+ server.tool("browser_storage", "Web storage operations: local/session storage get, set, clear", {
447
+ area: z.enum(["local", "session"]).describe("Storage area"),
448
+ action: z.enum(["get", "set", "clear"]).describe("Storage action"),
449
+ key: z.string().optional().describe("Storage key (for get/set)"),
450
+ value: z.string().optional().describe("Storage value (for set)"),
451
+ sessionId: z.string().optional().describe("Browser session ID"),
452
+ }, async ({ area, action, key, value, sessionId }) => {
453
+ const command = `storage_${action}_${area}`;
454
+ const options = action === "get" ? { key } : action === "set" ? { key, value } : {};
455
+ const result = await execBrowser(command, options, sessionId);
456
+ return { content: [{ type: "text", text: result }] };
457
+ });
458
+ // ============================================================
459
+ // Network (1 tool)
460
+ // ============================================================
461
+ server.tool("browser_network", "Network operations: route, unroute, list requests, request details, HAR recording", {
462
+ action: z.enum(["route", "unroute", "requests", "request-detail", "har-start", "har-stop"])
463
+ .describe("Network action"),
464
+ url: z.string().optional().describe("URL pattern (for route/unroute)"),
465
+ abort: z.boolean().optional().describe("Abort the request (for route)"),
466
+ body: z.string().optional().describe("Response body to return (for route)"),
467
+ resourceType: z.string().optional().describe("Filter by resource type (for route)"),
468
+ clear: z.boolean().optional().describe("Clear log after reading (for requests)"),
469
+ filter: z.string().optional().describe("Filter by URL pattern (for requests)"),
470
+ type: z.string().optional().describe("Filter by resource type (for requests)"),
471
+ method: z.string().optional().describe("Filter by HTTP method (for requests)"),
472
+ status: z.string().optional().describe("Filter by HTTP status (for requests)"),
473
+ requestId: z.string().optional().describe("Request ID to inspect (for request-detail)"),
474
+ path: z.string().optional().describe("File path to save HAR (for har-stop)"),
475
+ sessionId: z.string().optional().describe("Browser session ID"),
476
+ }, async ({ action, url, abort, body, resourceType, clear, filter, type, method, status, requestId, path, sessionId }) => {
477
+ let command;
478
+ let options;
479
+ switch (action) {
480
+ case "route":
481
+ command = "network_route";
482
+ options = { url, abort, body, resourceType };
483
+ break;
484
+ case "unroute":
485
+ command = "network_unroute";
486
+ options = { url };
487
+ break;
488
+ case "requests":
489
+ command = "network_requests";
490
+ options = { clear, filter, type, method, status };
491
+ break;
492
+ case "request-detail":
493
+ command = "network_request_detail";
494
+ options = { requestId };
495
+ break;
496
+ case "har-start":
497
+ command = "network_har_start";
498
+ options = {};
499
+ break;
500
+ case "har-stop":
501
+ command = "network_har_stop";
502
+ options = { path };
503
+ break;
504
+ default:
505
+ command = "network_requests";
506
+ options = {};
507
+ }
508
+ const result = await execBrowser(command, options, sessionId);
509
+ return { content: [{ type: "text", text: result }] };
510
+ });
511
+ // ============================================================
512
+ // Tabs (1 tool)
513
+ // ============================================================
514
+ server.tool("browser_tab", "Tab and window operations: list, new, close, switch, new window", {
515
+ action: z.enum(["list", "new", "close", "switch", "new-window"]).describe("Tab action"),
516
+ tabId: z.string().optional().describe("Tab ID (for close/switch)"),
517
+ url: z.string().optional().describe("URL to open (for new)"),
518
+ label: z.string().optional().describe("Tab label (for new)"),
519
+ sessionId: z.string().optional().describe("Browser session ID"),
520
+ }, async ({ action, tabId, url, label, sessionId }) => {
521
+ let command;
522
+ let options;
523
+ switch (action) {
524
+ case "list":
525
+ command = "tab_list";
526
+ options = {};
527
+ break;
528
+ case "new":
529
+ command = "tab_new";
530
+ options = { url, label };
531
+ break;
532
+ case "close":
533
+ command = "tab_close";
534
+ options = { tabId };
535
+ break;
536
+ case "switch":
537
+ command = "tab_switch";
538
+ options = { tabId };
539
+ break;
540
+ case "new-window":
541
+ command = "window_new";
542
+ options = {};
543
+ break;
544
+ default:
545
+ command = "tab_list";
546
+ options = {};
547
+ }
548
+ const result = await execBrowser(command, options, sessionId);
549
+ return { content: [{ type: "text", text: result }] };
550
+ });
551
+ // ============================================================
552
+ // Frames (1 tool)
553
+ // ============================================================
554
+ server.tool("browser_frame", "Frame operations: switch to an iframe by selector, or switch to main frame", {
555
+ action: z.enum(["switch", "main"]).describe("Frame action"),
556
+ selector: z.string().optional().describe("CSS selector of the iframe (for switch)"),
557
+ sessionId: z.string().optional().describe("Browser session ID"),
558
+ }, async ({ action, selector, sessionId }) => {
559
+ const command = action === "main" ? "frame_main" : "frame_switch";
560
+ const result = await execBrowser(command, { selector }, sessionId);
561
+ return { content: [{ type: "text", text: result }] };
562
+ });
563
+ // ============================================================
564
+ // Dialogs (1 tool)
565
+ // ============================================================
566
+ server.tool("browser_dialog", "Dialog operations: accept, dismiss, or check status of browser dialogs (alert/confirm/prompt)", {
567
+ action: z.enum(["accept", "dismiss", "status"]).describe("Dialog action"),
568
+ promptText: z.string().optional().describe("Text to enter for prompt dialogs (for accept/dismiss)"),
569
+ sessionId: z.string().optional().describe("Browser session ID"),
570
+ }, async ({ action, promptText, sessionId }) => {
571
+ const command = `dialog_${action}`;
572
+ const result = await execBrowser(command, { promptText }, sessionId);
573
+ return { content: [{ type: "text", text: result }] };
574
+ });
575
+ // ============================================================
576
+ // Debug (5 tools)
577
+ // ============================================================
578
+ server.tool("browser_trace", "Tracing operations: start/stop Chrome DevTools trace recording", {
579
+ action: z.enum(["start", "stop"]).describe("Trace action"),
580
+ path: z.string().optional().describe("File path to save the trace (for stop)"),
581
+ sessionId: z.string().optional().describe("Browser session ID"),
582
+ }, async ({ action, path, sessionId }) => {
583
+ const command = `trace_${action}`;
584
+ const result = await execBrowser(command, { path }, sessionId);
585
+ return { content: [{ type: "text", text: result }] };
586
+ });
587
+ server.tool("browser_profiler", "Profiler operations: start/stop Chrome DevTools profiler recording", {
588
+ action: z.enum(["start", "stop"]).describe("Profiler action"),
589
+ categories: z.string().optional().describe("Comma-separated profiling categories (for start)"),
590
+ path: z.string().optional().describe("File path to save the profile (for stop)"),
591
+ sessionId: z.string().optional().describe("Browser session ID"),
592
+ }, async ({ action, categories, path, sessionId }) => {
593
+ const command = `profiler_${action}`;
594
+ const result = await execBrowser(command, { categories, path }, sessionId);
595
+ return { content: [{ type: "text", text: result }] };
596
+ });
597
+ server.tool("browser_console", "View page console logs", {
598
+ clear: z.boolean().optional().describe("Clear console log after reading"),
599
+ sessionId: z.string().optional().describe("Browser session ID"),
600
+ }, async ({ clear, sessionId }) => {
601
+ const result = await execBrowser("get_console", { clear }, sessionId);
602
+ return { content: [{ type: "text", text: result }] };
603
+ });
604
+ server.tool("browser_errors", "View page JavaScript errors", {
605
+ clear: z.boolean().optional().describe("Clear error log after reading"),
606
+ sessionId: z.string().optional().describe("Browser session ID"),
607
+ }, async ({ clear, sessionId }) => {
608
+ const result = await execBrowser("get_errors", { clear }, sessionId);
609
+ return { content: [{ type: "text", text: result }] };
610
+ });
611
+ server.tool("browser_highlight", "Highlight an element on the page", {
612
+ selector: z.string().describe("CSS selector of the element to highlight"),
613
+ sessionId: z.string().optional().describe("Browser session ID"),
614
+ }, async ({ selector, sessionId }) => {
615
+ const result = await execBrowser("highlight", { selector }, sessionId);
616
+ return { content: [{ type: "text", text: result }] };
617
+ });
618
+ server.tool("browser_inspect", "Open Chrome DevTools for the active page", { sessionId: z.string().optional().describe("Browser session ID") }, async ({ sessionId }) => {
619
+ const result = await execBrowser("inspect", {}, sessionId);
620
+ return { content: [{ type: "text", text: result }] };
621
+ });
622
+ // ============================================================
623
+ // State Management (1 tool)
624
+ // ============================================================
625
+ server.tool("browser_state", "State management: save, load, list, show, rename, clear, clean browser states", {
626
+ action: z.enum(["save", "load", "list", "show", "rename", "clear", "clean"])
627
+ .describe("State action"),
628
+ path: z.string().optional().describe("State name or path (for save/load/show)"),
629
+ oldName: z.string().optional().describe("Current state name (for rename)"),
630
+ newName: z.string().optional().describe("New state name (for rename)"),
631
+ sessionName: z.string().optional().describe("Session name to clear (for clear)"),
632
+ all: z.boolean().optional().describe("Clear all states (for clear)"),
633
+ days: z.number().optional().describe("Remove states older than N days (for clean)"),
634
+ sessionId: z.string().optional().describe("Browser session ID"),
635
+ }, async ({ action, path, oldName, newName, sessionName, all, days, sessionId }) => {
636
+ const command = `state_${action}`;
637
+ const result = await execBrowser(command, { path, oldName, newName, sessionName, all, days }, sessionId);
638
+ return { content: [{ type: "text", text: result }] };
639
+ });
640
+ // ============================================================
641
+ // Recording (1 tool)
642
+ // ============================================================
643
+ server.tool("browser_record", "Video recording operations: start, stop, restart screen recording", {
644
+ action: z.enum(["start", "stop", "restart"]).describe("Recording action"),
645
+ path: z.string().optional().describe("Output WebM file path (for start/restart)"),
646
+ url: z.string().optional().describe("URL to navigate to (for start/restart)"),
647
+ sessionId: z.string().optional().describe("Browser session ID"),
648
+ }, async ({ action, path, url, sessionId }) => {
649
+ const command = `record_${action}`;
650
+ const result = await execBrowser(command, { path, url }, sessionId);
651
+ return { content: [{ type: "text", text: result }] };
652
+ });
653
+ // ============================================================
654
+ // Auth Vault (1 tool)
655
+ // ============================================================
656
+ server.tool("browser_auth", "Auth vault operations: save, login, list, delete, show saved credentials", {
657
+ action: z.enum(["save", "login", "list", "delete", "show"]).describe("Auth action"),
658
+ name: z.string().optional().describe("Credential name (for save/login/delete/show)"),
659
+ url: z.string().optional().describe("Login URL (for save)"),
660
+ username: z.string().optional().describe("Username (for save)"),
661
+ password: z.string().optional().describe("Password (for save)"),
662
+ usernameSelector: z.string().optional().describe("CSS selector for username field (for save)"),
663
+ passwordSelector: z.string().optional().describe("CSS selector for password field (for save)"),
664
+ submitSelector: z.string().optional().describe("CSS selector for submit button (for save)"),
665
+ sessionId: z.string().optional().describe("Browser session ID"),
666
+ }, async ({ action, name, url, username, password, usernameSelector, passwordSelector, submitSelector, sessionId }) => {
667
+ const command = `auth_${action}`;
668
+ const result = await execBrowser(command, { name, url, username, password, usernameSelector, passwordSelector, submitSelector }, sessionId);
669
+ return { content: [{ type: "text", text: result }] };
670
+ });
671
+ // ============================================================
672
+ // Diff (1 tool)
673
+ // ============================================================
674
+ server.tool("browser_diff", "Diff operations: compare snapshots, screenshots, or URLs", {
675
+ mode: z.enum(["snapshot", "screenshot", "url"]).describe("Diff mode"),
676
+ baseline: z.string().optional().describe("Baseline path (for snapshot/screenshot)"),
677
+ selector: z.string().optional().describe("CSS selector to scope the diff"),
678
+ compact: z.boolean().optional().describe("Use compact format (for snapshot/url)"),
679
+ depth: z.number().optional().describe("Limit tree depth (for snapshot/url)"),
680
+ output: z.string().optional().describe("Output path for diff image (for screenshot)"),
681
+ threshold: z.number().min(0).max(1).optional().describe("Mismatch threshold 0-1 (for screenshot)"),
682
+ fullPage: z.boolean().optional().describe("Compare full page (for screenshot/url)"),
683
+ url1: z.string().optional().describe("First URL (for url mode)"),
684
+ url2: z.string().optional().describe("Second URL (for url mode)"),
685
+ screenshot: z.boolean().optional().describe("Take screenshot comparison (for url mode)"),
686
+ waitUntil: z.string().optional().describe("Wait state before capture (for url mode)"),
687
+ sessionId: z.string().optional().describe("Browser session ID"),
688
+ }, async ({ mode, baseline, selector, compact, depth, output, threshold, fullPage, url1, url2, screenshot, waitUntil, sessionId }) => {
689
+ const command = `diff_${mode}`;
690
+ const result = await execBrowser(command, { baseline, selector, compact, depth, output, threshold, fullPage, url1, url2, screenshot, waitUntil }, sessionId);
691
+ return { content: [{ type: "text", text: result }] };
692
+ });
693
+ // ============================================================
694
+ // React DevTools (1 tool)
695
+ // ============================================================
696
+ server.tool("browser_react", "React DevTools operations: component tree, fiber inspection, render tracking, suspense boundaries", {
697
+ action: z.enum(["tree", "inspect", "renders-start", "renders-stop", "suspense"])
698
+ .describe("React action"),
699
+ fiberId: z.number().optional().describe("Numeric fiber ID (for inspect)"),
700
+ json: z.boolean().optional().describe("Output as JSON"),
701
+ onlyDynamic: z.boolean().optional().describe("Show only dynamic suspense boundaries (for suspense)"),
702
+ sessionId: z.string().optional().describe("Browser session ID"),
703
+ }, async ({ action, fiberId, json, onlyDynamic, sessionId }) => {
704
+ const command = action === "renders-start" ? "react_renders_start"
705
+ : action === "renders-stop" ? "react_renders_stop"
706
+ : `react_${action}`;
707
+ const result = await execBrowser(command, { fiberId, json, onlyDynamic }, sessionId);
708
+ return { content: [{ type: "text", text: result }] };
709
+ });
710
+ // ============================================================
711
+ // Vitals (1 tool)
712
+ // ============================================================
713
+ server.tool("browser_vitals", "Get Core Web Vitals measurements (LCP, CLS, TTFB, FCP, INP)", {
714
+ url: z.string().optional().describe("URL to measure vitals on"),
715
+ json: z.boolean().optional().describe("Output as JSON"),
716
+ sessionId: z.string().optional().describe("Browser session ID"),
717
+ }, async ({ url, json, sessionId }) => {
718
+ const result = await execBrowser("vitals", { url, json }, sessionId);
719
+ return { content: [{ type: "text", text: result }] };
720
+ });
721
+ // ============================================================
722
+ // Init Scripts (1 tool)
723
+ // ============================================================
724
+ server.tool("browser_remove_init_script", "Remove a previously registered initialization script", {
725
+ identifier: z.string().describe("Identifier of the init script to remove"),
726
+ sessionId: z.string().optional().describe("Browser session ID"),
727
+ }, async ({ identifier, sessionId }) => {
728
+ const result = await execBrowser("remove_init_script", { identifier }, sessionId);
729
+ return { content: [{ type: "text", text: result }] };
730
+ });
731
+ // ============================================================
732
+ // Session (1 tool)
733
+ // ============================================================
734
+ server.tool("browser_session", "Session operations: show current session, list active sessions", {
735
+ action: z.enum(["show", "list"]).describe("Session action"),
736
+ sessionId: z.string().optional().describe("Browser session ID"),
737
+ }, async ({ action, sessionId }) => {
738
+ const command = action === "list" ? "session_list" : "session_show";
739
+ const result = await execBrowser(command, {}, sessionId);
740
+ return { content: [{ type: "text", text: result }] };
741
+ });
742
+ // ============================================================
743
+ // Streaming (1 tool)
744
+ // ============================================================
745
+ server.tool("browser_stream", "WebSocket streaming operations for real-time page observation", {
746
+ action: z.enum(["enable", "disable", "status"]).describe("Stream action"),
747
+ port: z.number().optional().describe("Port for the stream (for enable)"),
748
+ sessionId: z.string().optional().describe("Browser session ID"),
749
+ }, async ({ action, port, sessionId }) => {
750
+ const command = `stream_${action}`;
751
+ const result = await execBrowser(command, { port }, sessionId);
752
+ return { content: [{ type: "text", text: result }] };
753
+ });
754
+ // ============================================================
755
+ // Dashboard (1 tool)
756
+ // ============================================================
757
+ server.tool("browser_dashboard", "Dashboard operations: start/stop the observability dashboard server", {
758
+ action: z.enum(["start", "stop"]).describe("Dashboard action"),
759
+ port: z.number().optional().describe("Port for the dashboard (for start)"),
760
+ sessionId: z.string().optional().describe("Browser session ID"),
761
+ }, async ({ action, port, sessionId }) => {
762
+ const command = `dashboard_${action}`;
763
+ const result = await execBrowser(command, { port }, sessionId);
764
+ return { content: [{ type: "text", text: result }] };
765
+ });
766
+ // ============================================================
767
+ // iOS (3 tools)
768
+ // ============================================================
769
+ server.tool("browser_device_list", "List available iOS simulators", { sessionId: z.string().optional().describe("Browser session ID") }, async ({ sessionId }) => {
770
+ const result = await execBrowser("device_list", {}, sessionId);
771
+ return { content: [{ type: "text", text: result }] };
772
+ });
773
+ server.tool("browser_tap", "Tap an element on a touch/iOS device", {
774
+ selector: z.string().describe("CSS selector of the element to tap"),
775
+ sessionId: z.string().optional().describe("Browser session ID"),
776
+ }, async ({ selector, sessionId }) => {
777
+ const result = await execBrowser("tap", { selector }, sessionId);
778
+ return { content: [{ type: "text", text: result }] };
779
+ });
780
+ server.tool("browser_swipe", "Swipe gesture on a touch/iOS device", {
781
+ direction: z.enum(["up", "down", "left", "right"]).describe("Swipe direction"),
782
+ distance: z.number().optional().describe("Swipe distance in pixels"),
783
+ sessionId: z.string().optional().describe("Browser session ID"),
784
+ }, async ({ direction, distance, sessionId }) => {
785
+ const result = await execBrowser("swipe", { direction, distance }, sessionId);
786
+ return { content: [{ type: "text", text: result }] };
787
+ });
788
+ // ============================================================
789
+ // Confirm / Deny (2 tools)
790
+ // ============================================================
791
+ server.tool("browser_confirm", "Approve a pending action/confirmation", {
792
+ confirmationId: z.string().describe("ID of the confirmation to accept"),
793
+ sessionId: z.string().optional().describe("Browser session ID"),
794
+ }, async ({ confirmationId, sessionId }) => {
795
+ const result = await execBrowser("confirm_action", { confirmationId }, sessionId);
796
+ return { content: [{ type: "text", text: result }] };
797
+ });
798
+ server.tool("browser_deny", "Reject a pending action/confirmation", {
799
+ confirmationId: z.string().describe("ID of the confirmation to reject"),
800
+ sessionId: z.string().optional().describe("Browser session ID"),
801
+ }, async ({ confirmationId, sessionId }) => {
802
+ const result = await execBrowser("deny_action", { confirmationId }, sessionId);
803
+ return { content: [{ type: "text", text: result }] };
804
+ });
805
+ // ============================================================
806
+ // Connect (1 tool)
807
+ // ============================================================
808
+ server.tool("browser_connect", "Connect to a browser via CDP (Chrome DevTools Protocol)", {
809
+ endpoint: z.string().describe("CDP endpoint URL or port number"),
810
+ sessionId: z.string().optional().describe("Browser session ID"),
811
+ }, async ({ endpoint, sessionId }) => {
812
+ const result = await execBrowser("connect", { endpoint }, sessionId);
813
+ return { content: [{ type: "text", text: result }] };
814
+ });
815
+ // ============================================================
816
+ // Session Management (MCP-specific, 1 tool)
817
+ // ============================================================
818
+ server.tool("browser_close", "Close the current browser session (the daemon creates sessions automatically)", { sessionId: z.string().optional().describe("Browser session ID to close") }, async ({ sessionId }) => {
819
+ const result = await execBrowser("close_session", {}, sessionId);
820
+ return { content: [{ type: "text", text: result }] };
821
+ });
822
+ // ============================================================
823
+ // Browser Manager (1 tool)
824
+ // ============================================================
825
+ server.tool("browser_manager", "Browser manager: list available browsers, discover installed browsers, install a browser, or select the active browser", {
826
+ action: z.enum(["list", "discover", "install", "select"])
827
+ .describe("Manager action: list (all browsers), discover (installed only), install (by name), select (set active)"),
828
+ browser: z.enum(["chromium", "cloakbrowser", "chrome", "firefox", "brave", "edge"]).optional()
829
+ .describe("Browser name (required for install/select actions)"),
830
+ }, async ({ action, browser }) => {
831
+ switch (action) {
832
+ case "list": {
833
+ const browsers = listBrowsers();
834
+ const active = discoverActiveBrowser();
835
+ let output = "Available browsers:\n\n";
836
+ for (const b of browsers) {
837
+ const isActive = active?.name === b.name;
838
+ const status = b.installed ? "installed" : "not installed";
839
+ const marker = isActive ? " [ACTIVE]" : "";
840
+ const pathInfo = b.path ? ` → ${b.path}` : "";
841
+ output += ` ${b.name.padEnd(16)} ${status}${marker}${pathInfo}\n`;
842
+ output += ` ${"".padEnd(16)} ${b.description}\n\n`;
843
+ }
844
+ output += active ? `Active: ${active.name}` : `No active browser. Use action=select.`;
845
+ return { content: [{ type: "text", text: output }] };
846
+ }
847
+ case "discover": {
848
+ const browsers = discoverBrowsers();
849
+ const active = discoverActiveBrowser();
850
+ let output = "Discovered browsers:\n\n";
851
+ if (browsers.length === 0) {
852
+ output += " None found. Use action=install to add one.";
853
+ }
854
+ else {
855
+ for (const b of browsers) {
856
+ const isActive = active?.name === b.name;
857
+ const marker = isActive ? " [ACTIVE]" : "";
858
+ const source = b.source === "env" ? "(.env)" : "(auto)";
859
+ output += ` ${b.name.padEnd(16)} ${b.path} ${source}${marker}\n`;
860
+ }
861
+ }
862
+ return { content: [{ type: "text", text: output }] };
863
+ }
864
+ case "install": {
865
+ if (!browser)
866
+ return { content: [{ type: "text", text: "Error: browser name required for install" }], isError: true };
867
+ const result = installBrowser(browser);
868
+ if (result.success) {
869
+ return { content: [{ type: "text", text: `${browser} installed → ${result.path}` }] };
870
+ }
871
+ return { content: [{ type: "text", text: `Failed: ${result.error}` }], isError: true };
872
+ }
873
+ case "select": {
874
+ if (!browser)
875
+ return { content: [{ type: "text", text: "Error: browser name required for select" }], isError: true };
876
+ const result = selectBrowser(browser);
877
+ if (result.success) {
878
+ return { content: [{ type: "text", text: `Active: ${browser} → ${result.path}` }] };
879
+ }
880
+ return { content: [{ type: "text", text: result.error || "Failed" }], isError: true };
881
+ }
882
+ }
883
+ });
884
+ }
885
+ //# sourceMappingURL=browser.js.map