mcp-scraper 0.2.20 → 0.2.21

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 (39) hide show
  1. package/README.md +34 -3
  2. package/dist/bin/api-server.cjs +52 -1
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +960 -20
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +3 -1
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +342 -47
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +105 -7
  12. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +976 -36
  14. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -1
  16. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  17. package/dist/bin/paa-harvest.cjs.map +1 -1
  18. package/dist/bin/paa-harvest.js +4 -2
  19. package/dist/bin/paa-harvest.js.map +1 -1
  20. package/dist/chunk-DUEW4EOO.js +214 -0
  21. package/dist/chunk-DUEW4EOO.js.map +1 -0
  22. package/dist/{chunk-L4OWOUGR.js → chunk-HS6OKELW.js} +737 -3
  23. package/dist/chunk-HS6OKELW.js.map +1 -0
  24. package/dist/{chunk-XGUDTDZ2.js → chunk-IPW4LFOT.js} +5 -13
  25. package/dist/chunk-IPW4LFOT.js.map +1 -0
  26. package/dist/chunk-WN7PBKMV.js +28 -0
  27. package/dist/chunk-WN7PBKMV.js.map +1 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/{server-7NE45K2A.js → server-24XIQ6W6.js} +40 -6
  32. package/dist/server-24XIQ6W6.js.map +1 -0
  33. package/dist/{worker-MIYG2B2I.js → worker-SLQ375UG.js} +5 -3
  34. package/dist/{worker-MIYG2B2I.js.map → worker-SLQ375UG.js.map} +1 -1
  35. package/docs/mcp-tool-manifest.generated.json +132 -4
  36. package/package.json +1 -1
  37. package/dist/chunk-L4OWOUGR.js.map +0 -1
  38. package/dist/chunk-XGUDTDZ2.js.map +0 -1
  39. package/dist/server-7NE45K2A.js.map +0 -1
@@ -1,3 +1,15 @@
1
+ import {
2
+ browserServiceProfileName,
3
+ browserServiceProfileSaveChanges
4
+ } from "./chunk-WN7PBKMV.js";
5
+ import {
6
+ defaultChromeExecutablePath,
7
+ importChromeProfile,
8
+ listLocalChromeProfiles,
9
+ loadImportedBrowserProfile,
10
+ localBrowserModeEnabled,
11
+ syncImportedChromeProfile
12
+ } from "./chunk-DUEW4EOO.js";
1
13
  import {
2
14
  PACKAGE_VERSION
3
15
  } from "./chunk-XKUDVN2E.js";
@@ -8,6 +20,361 @@ import { mkdirSync, writeFileSync } from "fs";
8
20
  import { homedir } from "os";
9
21
  import { join as join2 } from "path";
10
22
 
23
+ // src/services/browser-agent/local-browser-agent-service.ts
24
+ import { randomUUID } from "crypto";
25
+ import { existsSync } from "fs";
26
+ import { basename } from "path";
27
+ import { chromium } from "playwright";
28
+ function directProfileDir() {
29
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
30
+ }
31
+ function configuredProfileName(profile) {
32
+ return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? basename(directProfileDir()) : "default");
33
+ }
34
+ function launchExecutablePath(profile) {
35
+ const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
36
+ return configured && existsSync(configured) ? configured : void 0;
37
+ }
38
+ function ignoreDefaultArgsFor(executablePath) {
39
+ if (!executablePath) return void 0;
40
+ if (process.platform !== "darwin") return void 0;
41
+ if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
42
+ return ["--password-store=basic", "--use-mock-keychain"];
43
+ }
44
+ function normalizeKeyCombo(value) {
45
+ return value.split("+").map((part) => {
46
+ const trimmed = part.trim();
47
+ const lower = trimmed.toLowerCase();
48
+ if (lower === "ctrl" || lower === "control") return "Control";
49
+ if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
50
+ if (lower === "option" || lower === "alt") return "Alt";
51
+ if (lower === "return") return "Enter";
52
+ if (lower === "esc") return "Escape";
53
+ if (trimmed.length === 1) return trimmed.toUpperCase();
54
+ return trimmed;
55
+ }).join("+");
56
+ }
57
+ function sessionSummary(session) {
58
+ return {
59
+ session_id: session.sessionId,
60
+ status: session.status,
61
+ label: session.label,
62
+ profile: session.profileName,
63
+ user_data_dir: session.userDataDir,
64
+ created_at: session.createdAt,
65
+ last_action_at: session.lastActionAt,
66
+ closed_at: session.closedAt,
67
+ watch_url: `local://browser-session/${session.sessionId}`,
68
+ url: session.status === "open" ? session.page.url() : null
69
+ };
70
+ }
71
+ var LocalBrowserAgentService = class {
72
+ contexts = /* @__PURE__ */ new Map();
73
+ sessions = /* @__PURE__ */ new Map();
74
+ async open(input) {
75
+ const profile = await this.resolveProfile(input.profile);
76
+ const contextState = await this.ensureContext(profile);
77
+ const pages = contextState.context.pages();
78
+ const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
79
+ const now = (/* @__PURE__ */ new Date()).toISOString();
80
+ const sessionId = `lbs_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
81
+ const session = {
82
+ sessionId,
83
+ label: input.label ?? null,
84
+ page,
85
+ contextKey: contextState.key,
86
+ profileName: profile.name,
87
+ userDataDir: profile.userDataDir,
88
+ createdAt: now,
89
+ lastActionAt: now,
90
+ closedAt: null,
91
+ status: "open"
92
+ };
93
+ const timeoutSeconds = input.timeout_seconds ?? 600;
94
+ if (timeoutSeconds > 0) {
95
+ session.timeout = setTimeout(() => {
96
+ void this.close(sessionId);
97
+ }, timeoutSeconds * 1e3);
98
+ session.timeout.unref();
99
+ }
100
+ this.sessions.set(sessionId, session);
101
+ if (input.url) await this.goto(sessionId, input.url);
102
+ return {
103
+ session_id: sessionId,
104
+ watch_url: `local://browser-session/${sessionId}`,
105
+ live_view_url: null,
106
+ profile: profile.name,
107
+ user_data_dir: profile.userDataDir,
108
+ url: input.url ?? null,
109
+ mode: "local"
110
+ };
111
+ }
112
+ async screenshot(sessionId) {
113
+ const session = this.requireOpenSession(sessionId);
114
+ await this.waitForPage(session.page);
115
+ const image = await session.page.screenshot({ type: "png", fullPage: false });
116
+ const page = await this.read(sessionId);
117
+ return {
118
+ ...page,
119
+ image_base64: image.toString("base64"),
120
+ mime_type: "image/png"
121
+ };
122
+ }
123
+ async read(sessionId) {
124
+ const session = this.requireOpenSession(sessionId);
125
+ await this.waitForPage(session.page);
126
+ this.touch(session);
127
+ const title = await session.page.title().catch(() => "");
128
+ const snapshot = await session.page.evaluate(() => {
129
+ const cssEscape = (value) => {
130
+ const escapeFn = globalThis.CSS?.escape;
131
+ return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
132
+ };
133
+ const textFor = (element) => {
134
+ const html = element;
135
+ return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
136
+ };
137
+ const selectorFor = (element) => {
138
+ const id = element.id;
139
+ if (id) return `#${cssEscape(id)}`;
140
+ const name = element.getAttribute("name");
141
+ const tag = element.tagName.toLowerCase();
142
+ if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
143
+ const parts = [];
144
+ let current = element;
145
+ while (current && current !== document.body && parts.length < 4) {
146
+ const currentTag = current.tagName.toLowerCase();
147
+ const parent = current.parentElement;
148
+ if (!parent) {
149
+ parts.unshift(currentTag);
150
+ break;
151
+ }
152
+ const children = Array.from(parent.children);
153
+ const siblings = children.filter((child) => child.tagName === current.tagName);
154
+ const index = siblings.indexOf(current) + 1;
155
+ parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
156
+ current = parent;
157
+ }
158
+ return parts.join(" > ") || tag;
159
+ };
160
+ const candidates = Array.from(document.querySelectorAll(
161
+ 'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
162
+ ));
163
+ const elements = candidates.map((element, index) => {
164
+ const rect = element.getBoundingClientRect();
165
+ const style = window.getComputedStyle(element);
166
+ const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
167
+ if (!visible) return null;
168
+ const label = textFor(element);
169
+ return {
170
+ index,
171
+ tag: element.tagName.toLowerCase(),
172
+ role: element.getAttribute("role") || null,
173
+ text: label.slice(0, 180),
174
+ selector: selectorFor(element),
175
+ x: Math.round(rect.left + rect.width / 2),
176
+ y: Math.round(rect.top + rect.height / 2),
177
+ left: Math.round(rect.left),
178
+ top: Math.round(rect.top),
179
+ width: Math.round(rect.width),
180
+ height: Math.round(rect.height)
181
+ };
182
+ }).filter(Boolean).slice(0, 120);
183
+ return {
184
+ text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
185
+ elements,
186
+ viewport: {
187
+ width: window.innerWidth,
188
+ height: window.innerHeight,
189
+ devicePixelRatio: window.devicePixelRatio,
190
+ scrollX: window.scrollX,
191
+ scrollY: window.scrollY
192
+ }
193
+ };
194
+ });
195
+ return {
196
+ url: session.page.url(),
197
+ title,
198
+ text: snapshot.text,
199
+ elements: snapshot.elements,
200
+ viewport: snapshot.viewport
201
+ };
202
+ }
203
+ async locate(sessionId, targets) {
204
+ const session = this.requireOpenSession(sessionId);
205
+ this.touch(session);
206
+ const located = [];
207
+ for (const target of targets) {
208
+ try {
209
+ const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
210
+ const selected = locator.nth(target.index ?? 0);
211
+ await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
212
+ const box = await selected.boundingBox();
213
+ const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
214
+ located.push({
215
+ input: target,
216
+ found: Boolean(box),
217
+ element: box ? {
218
+ left: Math.round(box.x),
219
+ top: Math.round(box.y),
220
+ width: Math.round(box.width),
221
+ height: Math.round(box.height),
222
+ x: Math.round(box.x + box.width / 2),
223
+ y: Math.round(box.y + box.height / 2),
224
+ text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
225
+ } : null
226
+ });
227
+ } catch (err) {
228
+ located.push({
229
+ input: target,
230
+ found: false,
231
+ element: null,
232
+ error: err instanceof Error ? err.message : String(err)
233
+ });
234
+ }
235
+ }
236
+ const read = await this.read(sessionId);
237
+ return {
238
+ url: read.url,
239
+ title: read.title,
240
+ viewport: read.viewport,
241
+ replay: null,
242
+ targets: located
243
+ };
244
+ }
245
+ async goto(sessionId, url) {
246
+ const session = this.requireOpenSession(sessionId);
247
+ this.touch(session);
248
+ const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
249
+ throw new Error(err instanceof Error ? err.message : String(err));
250
+ });
251
+ await this.waitForPage(session.page);
252
+ return {
253
+ ok: true,
254
+ url: session.page.url(),
255
+ status: response?.status() ?? null,
256
+ mode: "local"
257
+ };
258
+ }
259
+ async click(sessionId, input) {
260
+ const session = this.requireOpenSession(sessionId);
261
+ this.touch(session);
262
+ await session.page.mouse.click(input.x, input.y, {
263
+ button: input.button ?? "left",
264
+ clickCount: input.num_clicks ?? 1
265
+ });
266
+ await this.waitForPage(session.page);
267
+ return { ok: true, mode: "local", url: session.page.url() };
268
+ }
269
+ async type(sessionId, input) {
270
+ const session = this.requireOpenSession(sessionId);
271
+ this.touch(session);
272
+ await session.page.keyboard.type(input.text, { delay: input.delay });
273
+ return { ok: true, mode: "local", url: session.page.url() };
274
+ }
275
+ async scroll(sessionId, input) {
276
+ const session = this.requireOpenSession(sessionId);
277
+ this.touch(session);
278
+ if (typeof input.x === "number" && typeof input.y === "number") {
279
+ await session.page.mouse.move(input.x, input.y);
280
+ }
281
+ await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
282
+ await session.page.waitForTimeout(150).catch(() => void 0);
283
+ return { ok: true, mode: "local", url: session.page.url() };
284
+ }
285
+ async press(sessionId, input) {
286
+ const session = this.requireOpenSession(sessionId);
287
+ this.touch(session);
288
+ for (const key of input.keys) {
289
+ await session.page.keyboard.press(normalizeKeyCombo(key));
290
+ }
291
+ await this.waitForPage(session.page);
292
+ return { ok: true, mode: "local", url: session.page.url() };
293
+ }
294
+ async close(sessionId) {
295
+ const session = this.sessions.get(sessionId);
296
+ if (!session || session.status === "closed") {
297
+ return { ok: true, closed: true, already_closed: true, mode: "local" };
298
+ }
299
+ if (session.timeout) clearTimeout(session.timeout);
300
+ session.status = "closed";
301
+ session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
302
+ await session.page.close().catch(() => void 0);
303
+ await this.closeContextIfUnused(session.contextKey);
304
+ return { ok: true, closed: true, mode: "local" };
305
+ }
306
+ list(includeClosed = false) {
307
+ return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
308
+ }
309
+ async resolveProfile(profile) {
310
+ const directDir = directProfileDir();
311
+ const requestedName = configuredProfileName(profile);
312
+ if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
313
+ return {
314
+ name: requestedName,
315
+ userDataDir: directDir,
316
+ browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
317
+ manifest: null
318
+ };
319
+ }
320
+ const manifest = await loadImportedBrowserProfile(requestedName);
321
+ return {
322
+ name: manifest.name,
323
+ userDataDir: manifest.userDataDir,
324
+ browserExecutablePath: manifest.browserExecutablePath,
325
+ manifest
326
+ };
327
+ }
328
+ async ensureContext(profile) {
329
+ const existing = this.contexts.get(profile.userDataDir);
330
+ if (existing) return existing;
331
+ const executablePath = launchExecutablePath(profile);
332
+ const context = await chromium.launchPersistentContext(profile.userDataDir, {
333
+ headless: false,
334
+ viewport: { width: 1440, height: 900 },
335
+ acceptDownloads: true,
336
+ ...executablePath ? { executablePath } : {},
337
+ ...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
338
+ });
339
+ const state = { key: profile.userDataDir, context, profile };
340
+ context.on("close", () => {
341
+ this.contexts.delete(profile.userDataDir);
342
+ const closedAt = (/* @__PURE__ */ new Date()).toISOString();
343
+ for (const session of this.sessions.values()) {
344
+ if (session.contextKey === profile.userDataDir && session.status === "open") {
345
+ session.status = "closed";
346
+ session.closedAt = closedAt;
347
+ if (session.timeout) clearTimeout(session.timeout);
348
+ }
349
+ }
350
+ });
351
+ this.contexts.set(profile.userDataDir, state);
352
+ return state;
353
+ }
354
+ requireOpenSession(sessionId) {
355
+ const session = this.sessions.get(sessionId);
356
+ if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
357
+ return session;
358
+ }
359
+ touch(session) {
360
+ session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
361
+ }
362
+ hasOpenSessionForContext(contextKey) {
363
+ return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
364
+ }
365
+ async closeContextIfUnused(contextKey) {
366
+ if (this.hasOpenSessionForContext(contextKey)) return;
367
+ const state = this.contexts.get(contextKey);
368
+ if (!state) return;
369
+ this.contexts.delete(contextKey);
370
+ await state.context.close().catch(() => void 0);
371
+ }
372
+ async waitForPage(page) {
373
+ await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
374
+ await page.waitForTimeout(200).catch(() => void 0);
375
+ }
376
+ };
377
+
11
378
  // src/mcp/browser-agent-tool-schemas.ts
12
379
  import { z } from "zod";
13
380
  var NullableString = z.string().nullable();
@@ -26,8 +393,35 @@ var BrowserOpenInputSchema = {
26
393
  label: z.string().optional().describe("Optional human label for this session, shown in the watch console."),
27
394
  url: z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
28
395
  profile: z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
396
+ save_profile_changes: z.boolean().optional().describe("Persist cookies and local storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
29
397
  timeout_seconds: z.number().int().min(60).max(259200).optional().describe("How long the session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
30
398
  };
399
+ var BrowserProfileListInputSchema = {
400
+ email: z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
401
+ user_data_dir: z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
402
+ };
403
+ var BrowserProfileOnboardInputSchema = {
404
+ email: z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
405
+ profile: z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
406
+ user_data_dir: z.string().optional().describe("Optional Chrome user data directory for email lookup. Defaults to the current OS Chrome profile root."),
407
+ url: z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
408
+ label: z.string().optional().describe("Optional human label for this setup session."),
409
+ timeout_seconds: z.number().int().min(60).max(259200).optional().describe("How long the setup session may live before auto-termination. Defaults to 600.")
410
+ };
411
+ var BrowserProfileImportInputSchema = {
412
+ email: z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
413
+ name: z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
414
+ chrome_profile_directory: z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
415
+ user_data_dir: z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
416
+ output_dir: z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
417
+ browser_executable_path: z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
418
+ overwrite: z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
419
+ };
420
+ var BrowserProfileSyncInputSchema = {
421
+ name: z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
422
+ output_dir: z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
423
+ browser_executable_path: z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
424
+ };
31
425
  var BrowserSessionInputSchema = {
32
426
  session_id: z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
33
427
  };
@@ -132,6 +526,43 @@ var BrowserOpenOutputSchema = {
132
526
  hint: z.string(),
133
527
  raw: BrowserRawObject.optional()
134
528
  };
529
+ var BrowserProfileListOutputSchema = {
530
+ ok: z.boolean(),
531
+ tool: z.literal("browser_profile_list"),
532
+ session_id: z.null(),
533
+ profiles: z.array(BrowserRawObject),
534
+ count: z.number().int().min(0),
535
+ hint: z.string()
536
+ };
537
+ var BrowserProfileOnboardOutputSchema = {
538
+ ok: z.boolean(),
539
+ tool: z.literal("browser_profile_onboard"),
540
+ session_id: z.string(),
541
+ watch_url: z.string(),
542
+ live_view_url: NullableString,
543
+ profile: z.string(),
544
+ setup_url: z.string(),
545
+ chrome_profile: BrowserRawObject.nullable(),
546
+ next_steps: z.array(z.string()),
547
+ raw: BrowserRawObject.optional()
548
+ };
549
+ var BrowserProfileImportOutputSchema = {
550
+ ok: z.boolean(),
551
+ tool: z.literal("browser_profile_import"),
552
+ session_id: z.null(),
553
+ profile: BrowserRawObject,
554
+ source_profile: BrowserRawObject,
555
+ next_steps: z.array(z.string()),
556
+ hint: z.string()
557
+ };
558
+ var BrowserProfileSyncOutputSchema = {
559
+ ok: z.boolean(),
560
+ tool: z.literal("browser_profile_sync"),
561
+ session_id: z.null(),
562
+ profile: BrowserRawObject,
563
+ source_profile: BrowserRawObject,
564
+ hint: z.string()
565
+ };
135
566
  var BrowserScreenshotOutputSchema = {
136
567
  ...BrowserBaseOutput,
137
568
  tool: z.literal("browser_screenshot"),
@@ -572,6 +1003,7 @@ function registerBrowserAgentMcpTools(server, opts) {
572
1003
  const baseUrl = opts.baseUrl.replace(/\/$/, "");
573
1004
  const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
574
1005
  const timeoutMs = opts.timeoutMs ?? 9e4;
1006
+ const localBrowser = new LocalBrowserAgentService();
575
1007
  async function req(method, path, body) {
576
1008
  try {
577
1009
  const res = await fetch(`${baseUrl}${path}`, {
@@ -623,19 +1055,195 @@ function registerBrowserAgentMcpTools(server, opts) {
623
1055
  idempotentHint: false,
624
1056
  openWorldHint: true
625
1057
  });
1058
+ function selectChromeProfile(matches, email) {
1059
+ const wanted = email?.trim().toLowerCase();
1060
+ if (!wanted) return matches[0] ?? null;
1061
+ return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
1062
+ }
1063
+ function localReplayUnsupported(tool, sessionId = null, replayId = null) {
1064
+ return errorResult(tool, {
1065
+ error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
1066
+ mode: "local"
1067
+ }, sessionId, replayId);
1068
+ }
1069
+ server.registerTool(
1070
+ "browser_profile_list",
1071
+ {
1072
+ title: "List Local Chrome Profiles",
1073
+ description: "List local Chrome profile metadata visible to this MCP server: account email, Chrome profile directory, local path, and a suggested managed browser profile name. This reads Chrome Local State only; it does not read cookies, passwords, browsing history, or copy local browser state. Use browser_profile_import when the user wants MCP Scraper to clone a local Chrome profile for local browser mode.",
1074
+ inputSchema: BrowserProfileListInputSchema,
1075
+ outputSchema: BrowserProfileListOutputSchema,
1076
+ annotations: annotations("List Local Chrome Profiles", true)
1077
+ },
1078
+ async (input) => {
1079
+ try {
1080
+ const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
1081
+ return structuredResult({
1082
+ ok: true,
1083
+ tool: "browser_profile_list",
1084
+ session_id: null,
1085
+ profiles,
1086
+ count: profiles.length,
1087
+ hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
1088
+ });
1089
+ } catch (err) {
1090
+ return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
1091
+ }
1092
+ }
1093
+ );
1094
+ server.registerTool(
1095
+ "browser_profile_import",
1096
+ {
1097
+ title: "Import Local Chrome Profile",
1098
+ description: "Clone one local Chrome profile into MCP Scraper managed storage for local browser mode. This copies browser state files such as cookies, history, local storage, session storage, and password database metadata into ~/.mcp-scraper/browser-profiles by default; it skips cache and lock files and never prints cookie or password values. Use this when the user wants browser_open to run locally with their existing logged-in Chrome state.",
1099
+ inputSchema: BrowserProfileImportInputSchema,
1100
+ outputSchema: BrowserProfileImportOutputSchema,
1101
+ annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1102
+ },
1103
+ async (input) => {
1104
+ try {
1105
+ const result = await importChromeProfile({
1106
+ email: input.email,
1107
+ name: input.name,
1108
+ chromeProfileDirectory: input.chrome_profile_directory,
1109
+ sourceUserDataDir: input.user_data_dir,
1110
+ outputDir: input.output_dir,
1111
+ overwrite: input.overwrite,
1112
+ browserExecutablePath: input.browser_executable_path
1113
+ });
1114
+ return structuredResult({
1115
+ ok: true,
1116
+ tool: "browser_profile_import",
1117
+ session_id: null,
1118
+ profile: result.profile,
1119
+ source_profile: result.sourceProfile,
1120
+ next_steps: [
1121
+ `Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
1122
+ `Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
1123
+ "Restart the MCP client so it starts the local browser mode process.",
1124
+ "Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
1125
+ ],
1126
+ hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
1127
+ });
1128
+ } catch (err) {
1129
+ return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
1130
+ }
1131
+ }
1132
+ );
1133
+ server.registerTool(
1134
+ "browser_profile_sync",
1135
+ {
1136
+ title: "Sync Local Chrome Profile",
1137
+ description: "Refresh an existing MCP Scraper managed local browser profile from the source Chrome profile recorded in its manifest. This overwrites the managed clone, copies browser state files again, skips cache and lock files, and never prints cookie or password values.",
1138
+ inputSchema: BrowserProfileSyncInputSchema,
1139
+ outputSchema: BrowserProfileSyncOutputSchema,
1140
+ annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1141
+ },
1142
+ async (input) => {
1143
+ try {
1144
+ const result = await syncImportedChromeProfile(input.name, {
1145
+ outputDir: input.output_dir,
1146
+ browserExecutablePath: input.browser_executable_path
1147
+ });
1148
+ return structuredResult({
1149
+ ok: true,
1150
+ tool: "browser_profile_sync",
1151
+ session_id: null,
1152
+ profile: result.profile,
1153
+ source_profile: result.sourceProfile,
1154
+ hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
1155
+ });
1156
+ } catch (err) {
1157
+ return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
1158
+ }
1159
+ }
1160
+ );
1161
+ server.registerTool(
1162
+ "browser_profile_onboard",
1163
+ {
1164
+ title: "Onboard Hosted Browser Profile",
1165
+ description: "Create or load a saved hosted Kernel browser profile and open a live setup browser with profile saving enabled. Use this only for hosted browser mode. If email is provided, the tool reads local Chrome profile metadata only to suggest the Kernel profile name; it does not import local cookies. For local browser mode with real Chrome state, use browser_profile_import instead.",
1166
+ inputSchema: BrowserProfileOnboardInputSchema,
1167
+ outputSchema: BrowserProfileOnboardOutputSchema,
1168
+ annotations: annotations("Onboard Hosted Browser Profile")
1169
+ },
1170
+ async (input) => {
1171
+ let chromeProfile = null;
1172
+ if (input.email) {
1173
+ try {
1174
+ chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
1175
+ } catch (err) {
1176
+ return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
1177
+ }
1178
+ }
1179
+ const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
1180
+ if (!profile) {
1181
+ return errorResult("browser_profile_onboard", {
1182
+ error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
1183
+ });
1184
+ }
1185
+ const setupUrl = input.url ?? "https://accounts.google.com/";
1186
+ const open = await req("POST", "/agent/sessions", {
1187
+ label: input.label ?? `Profile setup: ${profile}`,
1188
+ profile,
1189
+ save_profile_changes: true,
1190
+ timeout_seconds: input.timeout_seconds
1191
+ });
1192
+ if (!open.ok) return errorResult("browser_profile_onboard", open.data);
1193
+ const session = open.data;
1194
+ const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
1195
+ if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
1196
+ return structuredResult({
1197
+ ok: true,
1198
+ tool: "browser_profile_onboard",
1199
+ session_id: session.session_id,
1200
+ watch_url: `${consoleBase}/console/${session.session_id}`,
1201
+ live_view_url: session.live_view_url ?? null,
1202
+ profile,
1203
+ setup_url: setupUrl,
1204
+ chrome_profile: chromeProfile,
1205
+ next_steps: [
1206
+ "Open the watch_url and complete the login manually.",
1207
+ "After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
1208
+ "Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
1209
+ ],
1210
+ raw: session
1211
+ });
1212
+ }
1213
+ );
626
1214
  server.registerTool(
627
1215
  "browser_open",
628
1216
  {
629
1217
  title: "Open Browser Session",
630
- description: "Open a fresh cloud browser you can drive. Returns a session_id used by all other browser_* tools, and a watch_url where a human can watch live or take over. Anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default: if a Cloudflare or CAPTCHA challenge appears, do NOT click it \u2014 wait a few seconds and call browser_screenshot again; it is solved automatically. Billing: metered per second of active browser work at ~4 credits per minute; idle and standby time are free. Call browser_close when done to stop the meter. After opening, call browser_screenshot to see the page.",
1218
+ description: "Open a browser you can drive. By default this creates a hosted cloud browser with a watch_url; when MCP_SCRAPER_BROWSER_MODE=local it opens a local Google Chrome window against an imported MCP Scraper Chrome profile. Returns a session_id used by all other browser_* tools. In hosted mode, anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default. Call browser_close when done.",
631
1219
  inputSchema: BrowserOpenInputSchema,
632
1220
  outputSchema: BrowserOpenOutputSchema,
633
1221
  annotations: annotations("Open Browser Session")
634
1222
  },
635
1223
  async (input) => {
1224
+ if (localBrowserModeEnabled()) {
1225
+ try {
1226
+ const session2 = await localBrowser.open(input);
1227
+ return structuredResult({
1228
+ ok: true,
1229
+ tool: "browser_open",
1230
+ session_id: session2.session_id,
1231
+ watch_url: session2.watch_url,
1232
+ live_view_url: null,
1233
+ url: input.url ?? null,
1234
+ hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
1235
+ raw: session2
1236
+ });
1237
+ } catch (err) {
1238
+ return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
1239
+ }
1240
+ }
1241
+ const profile = input.profile ?? browserServiceProfileName();
1242
+ const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
636
1243
  const open = await req("POST", "/agent/sessions", {
637
1244
  label: input.label,
638
- profile: input.profile,
1245
+ ...profile ? { profile } : {},
1246
+ ...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
639
1247
  timeout_seconds: input.timeout_seconds
640
1248
  });
641
1249
  if (!open.ok) return errorResult("browser_open", open.data);
@@ -665,6 +1273,27 @@ function registerBrowserAgentMcpTools(server, opts) {
665
1273
  annotations: annotations("See Page", true)
666
1274
  },
667
1275
  async (input) => {
1276
+ if (localBrowserModeEnabled()) {
1277
+ try {
1278
+ const res2 = await localBrowser.screenshot(input.session_id);
1279
+ const content2 = [];
1280
+ if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
1281
+ const structured2 = {
1282
+ ok: true,
1283
+ tool: "browser_screenshot",
1284
+ session_id: input.session_id,
1285
+ url: res2.url ?? null,
1286
+ title: res2.title ?? null,
1287
+ text: res2.text,
1288
+ elements: res2.elements,
1289
+ screenshot: { mime_type: res2.mime_type, inline: true }
1290
+ };
1291
+ content2.push({ type: "text", text: JSON.stringify(structured2) });
1292
+ return { content: content2, structuredContent: structured2 };
1293
+ } catch (err) {
1294
+ return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1295
+ }
1296
+ }
668
1297
  const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
669
1298
  if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
670
1299
  const { image_base64, mime_type, url, title, elements, text } = res.data;
@@ -697,6 +1326,23 @@ function registerBrowserAgentMcpTools(server, opts) {
697
1326
  annotations: annotations("Read Page", true)
698
1327
  },
699
1328
  async (input) => {
1329
+ if (localBrowserModeEnabled()) {
1330
+ try {
1331
+ const res2 = await localBrowser.read(input.session_id);
1332
+ return structuredResult({
1333
+ ok: true,
1334
+ tool: "browser_read",
1335
+ session_id: input.session_id,
1336
+ url: res2.url ?? null,
1337
+ title: res2.title ?? null,
1338
+ text: res2.text,
1339
+ elements: res2.elements,
1340
+ raw: res2
1341
+ });
1342
+ } catch (err) {
1343
+ return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1344
+ }
1345
+ }
700
1346
  const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
701
1347
  if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
702
1348
  return structuredResult({
@@ -721,6 +1367,24 @@ function registerBrowserAgentMcpTools(server, opts) {
721
1367
  annotations: annotations("Locate DOM Targets", true)
722
1368
  },
723
1369
  async (input) => {
1370
+ if (localBrowserModeEnabled()) {
1371
+ try {
1372
+ const res2 = await localBrowser.locate(input.session_id, input.targets);
1373
+ return structuredResult({
1374
+ ok: true,
1375
+ tool: "browser_locate",
1376
+ session_id: input.session_id,
1377
+ url: res2.url ?? null,
1378
+ title: res2.title ?? null,
1379
+ viewport: res2.viewport ?? null,
1380
+ replay: null,
1381
+ targets: Array.isArray(res2.targets) ? res2.targets : [],
1382
+ raw: res2
1383
+ });
1384
+ } catch (err) {
1385
+ return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1386
+ }
1387
+ }
724
1388
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
725
1389
  if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
726
1390
  return structuredResult({
@@ -746,6 +1410,14 @@ function registerBrowserAgentMcpTools(server, opts) {
746
1410
  annotations: annotations("Navigate To URL")
747
1411
  },
748
1412
  async (input) => {
1413
+ if (localBrowserModeEnabled()) {
1414
+ try {
1415
+ const res2 = await localBrowser.goto(input.session_id, input.url);
1416
+ return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
1417
+ } catch (err) {
1418
+ return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1419
+ }
1420
+ }
749
1421
  const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
750
1422
  return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
751
1423
  }
@@ -760,6 +1432,14 @@ function registerBrowserAgentMcpTools(server, opts) {
760
1432
  annotations: annotations("Click")
761
1433
  },
762
1434
  async (input) => {
1435
+ if (localBrowserModeEnabled()) {
1436
+ try {
1437
+ const res2 = await localBrowser.click(input.session_id, input);
1438
+ return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
1439
+ } catch (err) {
1440
+ return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1441
+ }
1442
+ }
763
1443
  const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
764
1444
  x: input.x,
765
1445
  y: input.y,
@@ -779,6 +1459,14 @@ function registerBrowserAgentMcpTools(server, opts) {
779
1459
  annotations: annotations("Type Text")
780
1460
  },
781
1461
  async (input) => {
1462
+ if (localBrowserModeEnabled()) {
1463
+ try {
1464
+ const res2 = await localBrowser.type(input.session_id, input);
1465
+ return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
1466
+ } catch (err) {
1467
+ return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1468
+ }
1469
+ }
782
1470
  const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
783
1471
  return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
784
1472
  }
@@ -793,6 +1481,14 @@ function registerBrowserAgentMcpTools(server, opts) {
793
1481
  annotations: annotations("Scroll")
794
1482
  },
795
1483
  async (input) => {
1484
+ if (localBrowserModeEnabled()) {
1485
+ try {
1486
+ const res2 = await localBrowser.scroll(input.session_id, input);
1487
+ return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
1488
+ } catch (err) {
1489
+ return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1490
+ }
1491
+ }
796
1492
  const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
797
1493
  delta_y: input.delta_y,
798
1494
  delta_x: input.delta_x,
@@ -812,6 +1508,14 @@ function registerBrowserAgentMcpTools(server, opts) {
812
1508
  annotations: annotations("Press Keys")
813
1509
  },
814
1510
  async (input) => {
1511
+ if (localBrowserModeEnabled()) {
1512
+ try {
1513
+ const res2 = await localBrowser.press(input.session_id, input);
1514
+ return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
1515
+ } catch (err) {
1516
+ return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1517
+ }
1518
+ }
815
1519
  const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
816
1520
  return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
817
1521
  }
@@ -826,6 +1530,7 @@ function registerBrowserAgentMcpTools(server, opts) {
826
1530
  annotations: annotations("Start Recording")
827
1531
  },
828
1532
  async (input) => {
1533
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
829
1534
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
830
1535
  if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
831
1536
  return structuredResult({
@@ -849,6 +1554,7 @@ function registerBrowserAgentMcpTools(server, opts) {
849
1554
  annotations: annotations("Stop Recording")
850
1555
  },
851
1556
  async (input) => {
1557
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
852
1558
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
853
1559
  if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
854
1560
  return structuredResult({
@@ -872,6 +1578,7 @@ function registerBrowserAgentMcpTools(server, opts) {
872
1578
  annotations: annotations("List Replay Videos", true)
873
1579
  },
874
1580
  async (input) => {
1581
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
875
1582
  const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
876
1583
  if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
877
1584
  const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
@@ -894,6 +1601,7 @@ function registerBrowserAgentMcpTools(server, opts) {
894
1601
  annotations: annotations("Download Replay MP4")
895
1602
  },
896
1603
  async (input) => {
1604
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
897
1605
  const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
898
1606
  if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
899
1607
  return structuredResult({
@@ -918,6 +1626,7 @@ function registerBrowserAgentMcpTools(server, opts) {
918
1626
  annotations: annotations("Mark Replay Annotation", true)
919
1627
  },
920
1628
  async (input) => {
1629
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
921
1630
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
922
1631
  if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
923
1632
  const target = res.data?.targets?.[0];
@@ -968,6 +1677,7 @@ function registerBrowserAgentMcpTools(server, opts) {
968
1677
  annotations: annotations("Annotate Replay MP4")
969
1678
  },
970
1679
  async (input) => {
1680
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
971
1681
  const sourceName = input.filename ? `${input.filename}-source` : void 0;
972
1682
  const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
973
1683
  if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
@@ -1010,6 +1720,20 @@ function registerBrowserAgentMcpTools(server, opts) {
1010
1720
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1011
1721
  },
1012
1722
  async (input) => {
1723
+ if (localBrowserModeEnabled()) {
1724
+ try {
1725
+ const res2 = await localBrowser.close(input.session_id);
1726
+ return structuredResult({
1727
+ ok: true,
1728
+ tool: "browser_close",
1729
+ session_id: input.session_id,
1730
+ closed: true,
1731
+ raw: res2
1732
+ });
1733
+ } catch (err) {
1734
+ return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1735
+ }
1736
+ }
1013
1737
  const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
1014
1738
  if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
1015
1739
  return structuredResult({
@@ -1031,6 +1755,16 @@ function registerBrowserAgentMcpTools(server, opts) {
1031
1755
  annotations: annotations("List Browser Sessions", true)
1032
1756
  },
1033
1757
  async (input) => {
1758
+ if (localBrowserModeEnabled()) {
1759
+ const sessions2 = localBrowser.list(input.include_closed);
1760
+ return structuredResult({
1761
+ ok: true,
1762
+ tool: "browser_list_sessions",
1763
+ session_id: null,
1764
+ sessions: sessions2,
1765
+ count: sessions2.length
1766
+ });
1767
+ }
1034
1768
  const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
1035
1769
  if (!res.ok) return errorResult("browser_list_sessions", res.data);
1036
1770
  const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
@@ -1049,4 +1783,4 @@ export {
1049
1783
  buildBrowserAgentMcpServer,
1050
1784
  registerBrowserAgentMcpTools
1051
1785
  };
1052
- //# sourceMappingURL=chunk-L4OWOUGR.js.map
1786
+ //# sourceMappingURL=chunk-HS6OKELW.js.map