mcp-scraper 0.2.19 → 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 (77) hide show
  1. package/README.md +36 -5
  2. package/dist/bin/api-server.cjs +1072 -349
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/browser-agent-stdio-server.cjs +1274 -75
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +4 -2
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +659 -210
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +108 -9
  12. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +2104 -566
  14. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js +7 -4
  16. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  17. package/dist/bin/mcp-scraper-install.cjs +3 -3
  18. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  19. package/dist/bin/mcp-scraper-install.js +2 -2
  20. package/dist/bin/mcp-stdio-server.cjs +726 -387
  21. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  22. package/dist/bin/mcp-stdio-server.js +3 -2
  23. package/dist/bin/mcp-stdio-server.js.map +1 -1
  24. package/dist/bin/paa-harvest.cjs +5 -3
  25. package/dist/bin/paa-harvest.cjs.map +1 -1
  26. package/dist/bin/paa-harvest.js +5 -2
  27. package/dist/bin/paa-harvest.js.map +1 -1
  28. package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
  29. package/dist/chunk-DBQDG7EH.js.map +1 -0
  30. package/dist/chunk-DUEW4EOO.js +214 -0
  31. package/dist/chunk-DUEW4EOO.js.map +1 -0
  32. package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
  33. package/dist/chunk-H2R232HK.js.map +1 -0
  34. package/dist/chunk-HS6OKELW.js +1786 -0
  35. package/dist/chunk-HS6OKELW.js.map +1 -0
  36. package/dist/{chunk-CQTAKXBN.js → chunk-IPW4LFOT.js} +11 -116
  37. package/dist/chunk-IPW4LFOT.js.map +1 -0
  38. package/dist/chunk-LFATOGDF.js +111 -0
  39. package/dist/chunk-LFATOGDF.js.map +1 -0
  40. package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
  41. package/dist/chunk-MLNCKQ2B.js.map +1 -0
  42. package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
  43. package/dist/chunk-O5J7E4VX.js.map +1 -0
  44. package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
  45. package/dist/chunk-WN7PBKMV.js +28 -0
  46. package/dist/chunk-WN7PBKMV.js.map +1 -0
  47. package/dist/chunk-XKUDVN2E.js +7 -0
  48. package/dist/chunk-XKUDVN2E.js.map +1 -0
  49. package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
  50. package/dist/index.cjs +5 -3
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +2 -2
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +3 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/{server-CRUSZA2Q.js → server-24XIQ6W6.js} +248 -40
  57. package/dist/server-24XIQ6W6.js.map +1 -0
  58. package/dist/{worker-OZSWIS3F.js → worker-SLQ375UG.js} +8 -5
  59. package/dist/{worker-OZSWIS3F.js.map → worker-SLQ375UG.js.map} +1 -1
  60. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
  61. package/docs/mcp-tool-craft-lint.generated.md +51 -0
  62. package/docs/mcp-tool-manifest.generated.json +1534 -0
  63. package/docs/specs/local-competitive-audit-spec.md +1 -2
  64. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
  65. package/package.json +4 -1
  66. package/dist/chunk-4UEVYYDW.js.map +0 -1
  67. package/dist/chunk-76P4DKWR.js +0 -793
  68. package/dist/chunk-76P4DKWR.js.map +0 -1
  69. package/dist/chunk-CQTAKXBN.js.map +0 -1
  70. package/dist/chunk-F44RBOJ5.js.map +0 -1
  71. package/dist/chunk-MCBOZ2JF.js.map +0 -1
  72. package/dist/chunk-RU2HZD55.js +0 -7
  73. package/dist/chunk-RU2HZD55.js.map +0 -1
  74. package/dist/chunk-TL7YTFLH.js.map +0 -1
  75. package/dist/server-CRUSZA2Q.js.map +0 -1
  76. /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
  77. /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
@@ -2,9 +2,9 @@
2
2
  "use strict";
3
3
 
4
4
  // bin/mcp-scraper-combined-stdio-server.ts
5
- var import_node_fs4 = require("fs");
6
- var import_node_os4 = require("os");
7
- var import_node_path5 = require("path");
5
+ var import_node_fs6 = require("fs");
6
+ var import_node_os6 = require("os");
7
+ var import_node_path8 = require("path");
8
8
  var import_mcp3 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
9
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
10
10
 
@@ -23,6 +23,27 @@ function harvestTimeoutBudget(maxQuestions, serpOnly = false) {
23
23
  }
24
24
 
25
25
  // src/mcp/http-mcp-tool-executor.ts
26
+ function youtubeVideoIdFromUrl(url) {
27
+ if (!url) return null;
28
+ try {
29
+ const parsed = new URL(url);
30
+ const host = parsed.hostname.replace(/^www\./, "").replace(/^m\./, "");
31
+ if (host === "youtu.be") {
32
+ const id = parsed.pathname.split("/").filter(Boolean)[0];
33
+ return id || null;
34
+ }
35
+ if (host === "youtube.com" || host === "music.youtube.com") {
36
+ const watchId = parsed.searchParams.get("v");
37
+ if (watchId) return watchId;
38
+ const parts = parsed.pathname.split("/").filter(Boolean);
39
+ const markerIndex = parts.findIndex((part) => ["shorts", "embed", "live"].includes(part));
40
+ if (markerIndex >= 0 && parts[markerIndex + 1]) return parts[markerIndex + 1];
41
+ }
42
+ } catch {
43
+ return null;
44
+ }
45
+ return null;
46
+ }
26
47
  var HttpMcpToolExecutor = class {
27
48
  baseUrl;
28
49
  apiKey;
@@ -148,7 +169,21 @@ var HttpMcpToolExecutor = class {
148
169
  return this.call("/youtube/harvest", input);
149
170
  }
150
171
  youtubeTranscribe(input) {
151
- return this.call("/youtube/transcribe", input);
172
+ const videoId = input.videoId?.trim() || youtubeVideoIdFromUrl(input.url);
173
+ if (!videoId) {
174
+ return Promise.resolve({
175
+ content: [{
176
+ type: "text",
177
+ text: JSON.stringify({
178
+ error_code: "youtube_video_id_required",
179
+ error: "Pass videoId from youtube_harvest or a YouTube url that contains a video id.",
180
+ retryable: false
181
+ })
182
+ }],
183
+ isError: true
184
+ });
185
+ }
186
+ return this.call("/youtube/transcribe", { videoId });
152
187
  }
153
188
  facebookPageIntel(input) {
154
189
  return this.call("/facebook/page-intel", input);
@@ -185,6 +220,10 @@ var HttpMcpToolExecutor = class {
185
220
  webhookUrl: input.webhookUrl
186
221
  }, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
187
222
  }
223
+ workflowStep(input) {
224
+ const timeoutMs = this.httpTimeoutOverrideMs ?? Number(process.env.MCP_SCRAPER_WORKFLOW_TIMEOUT_MS ?? 9e5);
225
+ return this.call(`/workflows/runs/${encodeURIComponent(input.runId)}/step`, {}, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
226
+ }
188
227
  workflowStatus(input) {
189
228
  return this.getJson(`/workflows/runs/${encodeURIComponent(input.runId)}`);
190
229
  }
@@ -207,23 +246,635 @@ var HttpMcpToolExecutor = class {
207
246
 
208
247
  // src/mcp/browser-agent-mcp-server.ts
209
248
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
249
+ var import_node_fs3 = require("fs");
250
+ var import_node_os4 = require("os");
251
+ var import_node_path5 = require("path");
252
+
253
+ // src/lib/chrome-profiles.ts
254
+ var import_promises = require("fs/promises");
255
+ var import_node_os = require("os");
256
+ var import_node_path = require("path");
257
+ function defaultChromeUserDataDir() {
258
+ if (process.platform === "darwin") return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Google", "Chrome");
259
+ if (process.platform === "win32") {
260
+ return (0, import_node_path.join)(process.env.LOCALAPPDATA ?? (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local"), "Google", "Chrome", "User Data");
261
+ }
262
+ return (0, import_node_path.join)(process.env.XDG_CONFIG_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"), "google-chrome");
263
+ }
264
+ function recommendedKernelProfileName(value) {
265
+ return value.trim().toLowerCase().replace(/@/g, "-").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "browser-profile";
266
+ }
267
+ async function listLocalChromeProfiles(userDataDir = defaultChromeUserDataDir(), emailFilter) {
268
+ const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, "Local State"), "utf8");
269
+ const localState = JSON.parse(raw);
270
+ const wanted = emailFilter?.trim().toLowerCase();
271
+ return Object.entries(localState.profile?.info_cache ?? {}).map(([directory, info]) => {
272
+ const email = (info.user_name ?? "").trim();
273
+ const displayName = (info.name ?? info.shortcut_name ?? "").trim();
274
+ const labelSource = email || displayName || directory;
275
+ return {
276
+ email,
277
+ displayName,
278
+ chromeProfileDirectory: directory,
279
+ chromeProfilePath: (0, import_node_path.join)(userDataDir, directory),
280
+ recommendedKernelProfileName: recommendedKernelProfileName(labelSource)
281
+ };
282
+ }).filter((row) => !wanted || row.email.toLowerCase() === wanted || row.displayName.toLowerCase().includes(wanted));
283
+ }
284
+
285
+ // src/lib/browser-service-env.ts
286
+ function browserServiceProfileName() {
287
+ const value = (process.env.BROWSER_AGENT_PROFILE_NAME ?? process.env.BROWSER_SERVICE_PROFILE_NAME ?? process.env.KERNEL_BROWSER_PROFILE_NAME ?? process.env.KERNEL_PROFILE_NAME)?.trim();
288
+ return value || void 0;
289
+ }
290
+ function browserServiceProfileSaveChanges() {
291
+ const value = (process.env.BROWSER_AGENT_PROFILE_SAVE_CHANGES ?? process.env.BROWSER_SERVICE_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_BROWSER_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_PROFILE_SAVE_CHANGES)?.trim().toLowerCase();
292
+ if (!value) return void 0;
293
+ if (["1", "true", "yes", "on"].includes(value)) return true;
294
+ if (["0", "false", "no", "off"].includes(value)) return false;
295
+ return void 0;
296
+ }
297
+
298
+ // src/lib/local-browser-profiles.ts
210
299
  var import_node_fs = require("fs");
300
+ var import_promises2 = require("fs/promises");
211
301
  var import_node_os2 = require("os");
212
302
  var import_node_path2 = require("path");
303
+ var EXCLUDED_PROFILE_NAMES = [
304
+ "Cache",
305
+ "Code Cache",
306
+ "Crashpad",
307
+ "DawnCache",
308
+ "DawnGraphiteCache",
309
+ "DawnWebGPUCache",
310
+ "GPUCache",
311
+ "GrShaderCache",
312
+ "ShaderCache",
313
+ "Shared Dictionary"
314
+ ];
315
+ function defaultImportedBrowserProfilesDir() {
316
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_BASE_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".mcp-scraper", "browser-profiles");
317
+ }
318
+ function defaultChromeExecutablePath() {
319
+ const envPath = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || process.env.CHROME_PATH?.trim();
320
+ if (envPath) return envPath;
321
+ const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
322
+ (0, import_node_path2.join)(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
323
+ (0, import_node_path2.join)(process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
324
+ ] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
325
+ return candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
326
+ }
327
+ function localBrowserModeEnabled() {
328
+ return process.env.MCP_SCRAPER_BROWSER_MODE?.trim().toLowerCase() === "local";
329
+ }
330
+ function importedProfileName(value) {
331
+ return recommendedKernelProfileName(value);
332
+ }
333
+ function importedProfileRoot(name, outputDir = defaultImportedBrowserProfilesDir()) {
334
+ return (0, import_node_path2.join)(outputDir, importedProfileName(name));
335
+ }
336
+ function importedProfileManifestPath(name, outputDir = defaultImportedBrowserProfilesDir()) {
337
+ return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "manifest.json");
338
+ }
339
+ function importedProfileUserDataDir(name, outputDir = defaultImportedBrowserProfilesDir()) {
340
+ return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "user-data");
341
+ }
342
+ async function readExistingManifest(path) {
343
+ try {
344
+ return JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
345
+ } catch {
346
+ return null;
347
+ }
348
+ }
349
+ function isExcludedProfilePath(sourceRoot, sourcePath) {
350
+ const rel = (0, import_node_path2.relative)(sourceRoot, sourcePath);
351
+ if (!rel) return false;
352
+ const parts = rel.split(import_node_path2.sep).filter(Boolean);
353
+ const last = parts[parts.length - 1] ?? "";
354
+ if (last.startsWith("Singleton")) return true;
355
+ if (last === "LOCK" || last.endsWith(".lock")) return true;
356
+ if (parts.some((part) => EXCLUDED_PROFILE_NAMES.includes(part))) return true;
357
+ if (parts.includes("Service Worker") && parts.includes("CacheStorage")) return true;
358
+ return false;
359
+ }
360
+ async function copyProfileTree(sourceProfilePath, destinationProfilePath) {
361
+ await (0, import_promises2.cp)(sourceProfilePath, destinationProfilePath, {
362
+ recursive: true,
363
+ force: true,
364
+ errorOnExist: false,
365
+ dereference: false,
366
+ filter: (source) => !isExcludedProfilePath(sourceProfilePath, source)
367
+ });
368
+ }
369
+ function selectProfile(profiles, options) {
370
+ const requestedDirectory = options.chromeProfileDirectory?.trim();
371
+ const requestedEmail = options.email?.trim().toLowerCase();
372
+ const exactDirectory = requestedDirectory ? profiles.find((profile) => profile.chromeProfileDirectory === requestedDirectory) : void 0;
373
+ if (exactDirectory) return exactDirectory;
374
+ const exactEmail = requestedEmail ? profiles.find((profile) => profile.email.toLowerCase() === requestedEmail) : void 0;
375
+ if (exactEmail) return exactEmail;
376
+ const first = profiles[0];
377
+ if (first) return first;
378
+ const hint = requestedDirectory ? `Chrome profile directory "${requestedDirectory}"` : requestedEmail ? `Chrome account "${requestedEmail}"` : "Chrome profile";
379
+ throw new Error(`${hint} was not found in ${options.sourceUserDataDir ?? defaultChromeUserDataDir()}`);
380
+ }
381
+ async function ensureProfilePathExists(profile) {
382
+ const details = await (0, import_promises2.stat)(profile.chromeProfilePath).catch(() => null);
383
+ if (!details?.isDirectory()) {
384
+ throw new Error(`Chrome profile path does not exist or is not a directory: ${profile.chromeProfilePath}`);
385
+ }
386
+ }
387
+ async function writeNormalizedLocalState(sourceLocalStatePath, destinationLocalStatePath, sourceProfileDirectory) {
388
+ const raw = await (0, import_promises2.readFile)(sourceLocalStatePath, "utf8");
389
+ const localState = JSON.parse(raw);
390
+ const selectedInfo = localState.profile?.info_cache?.[sourceProfileDirectory] ?? localState.profile?.info_cache?.Default ?? {};
391
+ localState.profile = {
392
+ ...localState.profile ?? {},
393
+ info_cache: { Default: selectedInfo },
394
+ last_used: "Default",
395
+ last_active_profiles: ["Default"]
396
+ };
397
+ await (0, import_promises2.writeFile)(destinationLocalStatePath, `${JSON.stringify(localState, null, 2)}
398
+ `);
399
+ }
400
+ async function importChromeProfile(options = {}) {
401
+ const sourceUserDataDir = options.sourceUserDataDir?.trim() || defaultChromeUserDataDir();
402
+ const profiles = await listLocalChromeProfiles(sourceUserDataDir, options.email);
403
+ const sourceProfile = selectProfile(profiles, { ...options, sourceUserDataDir });
404
+ await ensureProfilePathExists(sourceProfile);
405
+ const name = importedProfileName(options.name?.trim() || sourceProfile.recommendedKernelProfileName);
406
+ const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
407
+ const profileRoot = importedProfileRoot(name, outputDir);
408
+ const manifestPath = importedProfileManifestPath(name, outputDir);
409
+ const userDataDir = importedProfileUserDataDir(name, outputDir);
410
+ const existingManifest = await readExistingManifest(manifestPath);
411
+ const existing = (0, import_node_fs.existsSync)(profileRoot);
412
+ if (existing && !options.overwrite) {
413
+ throw new Error(`Imported browser profile "${name}" already exists at ${profileRoot}. Re-run with --overwrite to refresh it.`);
414
+ }
415
+ if (existing) await (0, import_promises2.rm)(profileRoot, { recursive: true, force: true });
416
+ await (0, import_promises2.mkdir)((0, import_node_path2.join)(userDataDir, "Default"), { recursive: true });
417
+ await writeNormalizedLocalState(
418
+ (0, import_node_path2.join)(sourceUserDataDir, "Local State"),
419
+ (0, import_node_path2.join)(userDataDir, "Local State"),
420
+ sourceProfile.chromeProfileDirectory
421
+ );
422
+ await copyProfileTree(sourceProfile.chromeProfilePath, (0, import_node_path2.join)(userDataDir, "Default"));
423
+ const now = (/* @__PURE__ */ new Date()).toISOString();
424
+ const manifest = {
425
+ layoutVersion: 1,
426
+ name,
427
+ profileRoot,
428
+ userDataDir,
429
+ browserExecutablePath: options.browserExecutablePath?.trim() || defaultChromeExecutablePath() || null,
430
+ sourceUserDataDir,
431
+ sourceProfileDirectory: sourceProfile.chromeProfileDirectory,
432
+ sourceProfilePath: sourceProfile.chromeProfilePath,
433
+ email: sourceProfile.email,
434
+ displayName: sourceProfile.displayName,
435
+ createdAt: existingManifest?.createdAt ?? now,
436
+ updatedAt: now,
437
+ copyMode: "chrome-profile-clone"
438
+ };
439
+ await (0, import_promises2.writeFile)(manifestPath, `${JSON.stringify(manifest, null, 2)}
440
+ `);
441
+ return {
442
+ profile: manifest,
443
+ sourceProfile,
444
+ excludedNames: EXCLUDED_PROFILE_NAMES
445
+ };
446
+ }
447
+ async function loadImportedBrowserProfile(name = process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || "default", outputDir = defaultImportedBrowserProfilesDir()) {
448
+ const profileName = importedProfileName(name);
449
+ const manifestPath = importedProfileManifestPath(profileName, outputDir);
450
+ const manifest = await readExistingManifest(manifestPath);
451
+ if (!manifest) {
452
+ throw new Error(
453
+ `Imported browser profile "${profileName}" was not found. Run: mcp-scraper-cli browser import-chrome --name ${profileName}`
454
+ );
455
+ }
456
+ return manifest;
457
+ }
458
+ async function syncImportedChromeProfile(name, options = {}) {
459
+ const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
460
+ const manifest = await loadImportedBrowserProfile(name, outputDir);
461
+ return importChromeProfile({
462
+ name: manifest.name,
463
+ sourceUserDataDir: manifest.sourceUserDataDir,
464
+ chromeProfileDirectory: manifest.sourceProfileDirectory,
465
+ outputDir,
466
+ overwrite: true,
467
+ browserExecutablePath: options.browserExecutablePath ?? manifest.browserExecutablePath ?? void 0
468
+ });
469
+ }
470
+
471
+ // src/services/browser-agent/local-browser-agent-service.ts
472
+ var import_node_crypto = require("crypto");
473
+ var import_node_fs2 = require("fs");
474
+ var import_node_path3 = require("path");
475
+ var import_playwright = require("playwright");
476
+ function directProfileDir() {
477
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
478
+ }
479
+ function configuredProfileName(profile) {
480
+ return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path3.basename)(directProfileDir()) : "default");
481
+ }
482
+ function launchExecutablePath(profile) {
483
+ const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
484
+ return configured && (0, import_node_fs2.existsSync)(configured) ? configured : void 0;
485
+ }
486
+ function ignoreDefaultArgsFor(executablePath) {
487
+ if (!executablePath) return void 0;
488
+ if (process.platform !== "darwin") return void 0;
489
+ if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
490
+ return ["--password-store=basic", "--use-mock-keychain"];
491
+ }
492
+ function normalizeKeyCombo(value) {
493
+ return value.split("+").map((part) => {
494
+ const trimmed = part.trim();
495
+ const lower = trimmed.toLowerCase();
496
+ if (lower === "ctrl" || lower === "control") return "Control";
497
+ if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
498
+ if (lower === "option" || lower === "alt") return "Alt";
499
+ if (lower === "return") return "Enter";
500
+ if (lower === "esc") return "Escape";
501
+ if (trimmed.length === 1) return trimmed.toUpperCase();
502
+ return trimmed;
503
+ }).join("+");
504
+ }
505
+ function sessionSummary(session) {
506
+ return {
507
+ session_id: session.sessionId,
508
+ status: session.status,
509
+ label: session.label,
510
+ profile: session.profileName,
511
+ user_data_dir: session.userDataDir,
512
+ created_at: session.createdAt,
513
+ last_action_at: session.lastActionAt,
514
+ closed_at: session.closedAt,
515
+ watch_url: `local://browser-session/${session.sessionId}`,
516
+ url: session.status === "open" ? session.page.url() : null
517
+ };
518
+ }
519
+ var LocalBrowserAgentService = class {
520
+ contexts = /* @__PURE__ */ new Map();
521
+ sessions = /* @__PURE__ */ new Map();
522
+ async open(input) {
523
+ const profile = await this.resolveProfile(input.profile);
524
+ const contextState = await this.ensureContext(profile);
525
+ const pages = contextState.context.pages();
526
+ const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
527
+ const now = (/* @__PURE__ */ new Date()).toISOString();
528
+ const sessionId = `lbs_${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
529
+ const session = {
530
+ sessionId,
531
+ label: input.label ?? null,
532
+ page,
533
+ contextKey: contextState.key,
534
+ profileName: profile.name,
535
+ userDataDir: profile.userDataDir,
536
+ createdAt: now,
537
+ lastActionAt: now,
538
+ closedAt: null,
539
+ status: "open"
540
+ };
541
+ const timeoutSeconds = input.timeout_seconds ?? 600;
542
+ if (timeoutSeconds > 0) {
543
+ session.timeout = setTimeout(() => {
544
+ void this.close(sessionId);
545
+ }, timeoutSeconds * 1e3);
546
+ session.timeout.unref();
547
+ }
548
+ this.sessions.set(sessionId, session);
549
+ if (input.url) await this.goto(sessionId, input.url);
550
+ return {
551
+ session_id: sessionId,
552
+ watch_url: `local://browser-session/${sessionId}`,
553
+ live_view_url: null,
554
+ profile: profile.name,
555
+ user_data_dir: profile.userDataDir,
556
+ url: input.url ?? null,
557
+ mode: "local"
558
+ };
559
+ }
560
+ async screenshot(sessionId) {
561
+ const session = this.requireOpenSession(sessionId);
562
+ await this.waitForPage(session.page);
563
+ const image = await session.page.screenshot({ type: "png", fullPage: false });
564
+ const page = await this.read(sessionId);
565
+ return {
566
+ ...page,
567
+ image_base64: image.toString("base64"),
568
+ mime_type: "image/png"
569
+ };
570
+ }
571
+ async read(sessionId) {
572
+ const session = this.requireOpenSession(sessionId);
573
+ await this.waitForPage(session.page);
574
+ this.touch(session);
575
+ const title = await session.page.title().catch(() => "");
576
+ const snapshot = await session.page.evaluate(() => {
577
+ const cssEscape = (value) => {
578
+ const escapeFn = globalThis.CSS?.escape;
579
+ return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
580
+ };
581
+ const textFor = (element) => {
582
+ const html = element;
583
+ return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
584
+ };
585
+ const selectorFor = (element) => {
586
+ const id = element.id;
587
+ if (id) return `#${cssEscape(id)}`;
588
+ const name = element.getAttribute("name");
589
+ const tag = element.tagName.toLowerCase();
590
+ if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
591
+ const parts = [];
592
+ let current = element;
593
+ while (current && current !== document.body && parts.length < 4) {
594
+ const currentTag = current.tagName.toLowerCase();
595
+ const parent = current.parentElement;
596
+ if (!parent) {
597
+ parts.unshift(currentTag);
598
+ break;
599
+ }
600
+ const children = Array.from(parent.children);
601
+ const siblings = children.filter((child) => child.tagName === current.tagName);
602
+ const index = siblings.indexOf(current) + 1;
603
+ parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
604
+ current = parent;
605
+ }
606
+ return parts.join(" > ") || tag;
607
+ };
608
+ const candidates = Array.from(document.querySelectorAll(
609
+ 'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
610
+ ));
611
+ const elements = candidates.map((element, index) => {
612
+ const rect = element.getBoundingClientRect();
613
+ const style = window.getComputedStyle(element);
614
+ const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
615
+ if (!visible) return null;
616
+ const label = textFor(element);
617
+ return {
618
+ index,
619
+ tag: element.tagName.toLowerCase(),
620
+ role: element.getAttribute("role") || null,
621
+ text: label.slice(0, 180),
622
+ selector: selectorFor(element),
623
+ x: Math.round(rect.left + rect.width / 2),
624
+ y: Math.round(rect.top + rect.height / 2),
625
+ left: Math.round(rect.left),
626
+ top: Math.round(rect.top),
627
+ width: Math.round(rect.width),
628
+ height: Math.round(rect.height)
629
+ };
630
+ }).filter(Boolean).slice(0, 120);
631
+ return {
632
+ text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
633
+ elements,
634
+ viewport: {
635
+ width: window.innerWidth,
636
+ height: window.innerHeight,
637
+ devicePixelRatio: window.devicePixelRatio,
638
+ scrollX: window.scrollX,
639
+ scrollY: window.scrollY
640
+ }
641
+ };
642
+ });
643
+ return {
644
+ url: session.page.url(),
645
+ title,
646
+ text: snapshot.text,
647
+ elements: snapshot.elements,
648
+ viewport: snapshot.viewport
649
+ };
650
+ }
651
+ async locate(sessionId, targets) {
652
+ const session = this.requireOpenSession(sessionId);
653
+ this.touch(session);
654
+ const located = [];
655
+ for (const target of targets) {
656
+ try {
657
+ const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
658
+ const selected = locator.nth(target.index ?? 0);
659
+ await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
660
+ const box = await selected.boundingBox();
661
+ const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
662
+ located.push({
663
+ input: target,
664
+ found: Boolean(box),
665
+ element: box ? {
666
+ left: Math.round(box.x),
667
+ top: Math.round(box.y),
668
+ width: Math.round(box.width),
669
+ height: Math.round(box.height),
670
+ x: Math.round(box.x + box.width / 2),
671
+ y: Math.round(box.y + box.height / 2),
672
+ text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
673
+ } : null
674
+ });
675
+ } catch (err) {
676
+ located.push({
677
+ input: target,
678
+ found: false,
679
+ element: null,
680
+ error: err instanceof Error ? err.message : String(err)
681
+ });
682
+ }
683
+ }
684
+ const read = await this.read(sessionId);
685
+ return {
686
+ url: read.url,
687
+ title: read.title,
688
+ viewport: read.viewport,
689
+ replay: null,
690
+ targets: located
691
+ };
692
+ }
693
+ async goto(sessionId, url) {
694
+ const session = this.requireOpenSession(sessionId);
695
+ this.touch(session);
696
+ const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
697
+ throw new Error(err instanceof Error ? err.message : String(err));
698
+ });
699
+ await this.waitForPage(session.page);
700
+ return {
701
+ ok: true,
702
+ url: session.page.url(),
703
+ status: response?.status() ?? null,
704
+ mode: "local"
705
+ };
706
+ }
707
+ async click(sessionId, input) {
708
+ const session = this.requireOpenSession(sessionId);
709
+ this.touch(session);
710
+ await session.page.mouse.click(input.x, input.y, {
711
+ button: input.button ?? "left",
712
+ clickCount: input.num_clicks ?? 1
713
+ });
714
+ await this.waitForPage(session.page);
715
+ return { ok: true, mode: "local", url: session.page.url() };
716
+ }
717
+ async type(sessionId, input) {
718
+ const session = this.requireOpenSession(sessionId);
719
+ this.touch(session);
720
+ await session.page.keyboard.type(input.text, { delay: input.delay });
721
+ return { ok: true, mode: "local", url: session.page.url() };
722
+ }
723
+ async scroll(sessionId, input) {
724
+ const session = this.requireOpenSession(sessionId);
725
+ this.touch(session);
726
+ if (typeof input.x === "number" && typeof input.y === "number") {
727
+ await session.page.mouse.move(input.x, input.y);
728
+ }
729
+ await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
730
+ await session.page.waitForTimeout(150).catch(() => void 0);
731
+ return { ok: true, mode: "local", url: session.page.url() };
732
+ }
733
+ async press(sessionId, input) {
734
+ const session = this.requireOpenSession(sessionId);
735
+ this.touch(session);
736
+ for (const key of input.keys) {
737
+ await session.page.keyboard.press(normalizeKeyCombo(key));
738
+ }
739
+ await this.waitForPage(session.page);
740
+ return { ok: true, mode: "local", url: session.page.url() };
741
+ }
742
+ async close(sessionId) {
743
+ const session = this.sessions.get(sessionId);
744
+ if (!session || session.status === "closed") {
745
+ return { ok: true, closed: true, already_closed: true, mode: "local" };
746
+ }
747
+ if (session.timeout) clearTimeout(session.timeout);
748
+ session.status = "closed";
749
+ session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
750
+ await session.page.close().catch(() => void 0);
751
+ await this.closeContextIfUnused(session.contextKey);
752
+ return { ok: true, closed: true, mode: "local" };
753
+ }
754
+ list(includeClosed = false) {
755
+ return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
756
+ }
757
+ async resolveProfile(profile) {
758
+ const directDir = directProfileDir();
759
+ const requestedName = configuredProfileName(profile);
760
+ if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
761
+ return {
762
+ name: requestedName,
763
+ userDataDir: directDir,
764
+ browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
765
+ manifest: null
766
+ };
767
+ }
768
+ const manifest = await loadImportedBrowserProfile(requestedName);
769
+ return {
770
+ name: manifest.name,
771
+ userDataDir: manifest.userDataDir,
772
+ browserExecutablePath: manifest.browserExecutablePath,
773
+ manifest
774
+ };
775
+ }
776
+ async ensureContext(profile) {
777
+ const existing = this.contexts.get(profile.userDataDir);
778
+ if (existing) return existing;
779
+ const executablePath = launchExecutablePath(profile);
780
+ const context = await import_playwright.chromium.launchPersistentContext(profile.userDataDir, {
781
+ headless: false,
782
+ viewport: { width: 1440, height: 900 },
783
+ acceptDownloads: true,
784
+ ...executablePath ? { executablePath } : {},
785
+ ...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
786
+ });
787
+ const state = { key: profile.userDataDir, context, profile };
788
+ context.on("close", () => {
789
+ this.contexts.delete(profile.userDataDir);
790
+ const closedAt = (/* @__PURE__ */ new Date()).toISOString();
791
+ for (const session of this.sessions.values()) {
792
+ if (session.contextKey === profile.userDataDir && session.status === "open") {
793
+ session.status = "closed";
794
+ session.closedAt = closedAt;
795
+ if (session.timeout) clearTimeout(session.timeout);
796
+ }
797
+ }
798
+ });
799
+ this.contexts.set(profile.userDataDir, state);
800
+ return state;
801
+ }
802
+ requireOpenSession(sessionId) {
803
+ const session = this.sessions.get(sessionId);
804
+ if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
805
+ return session;
806
+ }
807
+ touch(session) {
808
+ session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
809
+ }
810
+ hasOpenSessionForContext(contextKey) {
811
+ return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
812
+ }
813
+ async closeContextIfUnused(contextKey) {
814
+ if (this.hasOpenSessionForContext(contextKey)) return;
815
+ const state = this.contexts.get(contextKey);
816
+ if (!state) return;
817
+ this.contexts.delete(contextKey);
818
+ await state.context.close().catch(() => void 0);
819
+ }
820
+ async waitForPage(page) {
821
+ await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
822
+ await page.waitForTimeout(200).catch(() => void 0);
823
+ }
824
+ };
213
825
 
214
826
  // src/version.ts
215
- var PACKAGE_VERSION = "0.2.19";
827
+ var PACKAGE_VERSION = "0.2.20";
216
828
 
217
829
  // src/mcp/browser-agent-tool-schemas.ts
218
830
  var import_zod = require("zod");
831
+ var NullableString = import_zod.z.string().nullable();
832
+ var BrowserRawObject = import_zod.z.record(import_zod.z.unknown());
833
+ var BrowserElementOutput = import_zod.z.record(import_zod.z.unknown());
834
+ var BrowserBaseOutput = {
835
+ ok: import_zod.z.boolean().describe("Whether the browser-agent action succeeded."),
836
+ tool: import_zod.z.string().describe("Browser Agent MCP tool that produced this response."),
837
+ session_id: NullableString.describe("Browser session id when the response is scoped to a session.")
838
+ };
839
+ var BrowserReplayBaseOutput = {
840
+ ...BrowserBaseOutput,
841
+ replay_id: NullableString.describe("Replay id when the response is scoped to a replay.")
842
+ };
219
843
  var BrowserOpenInputSchema = {
220
844
  label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
221
845
  url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
222
846
  profile: import_zod.z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
847
+ save_profile_changes: import_zod.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."),
223
848
  timeout_seconds: import_zod.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.")
224
849
  };
850
+ var BrowserProfileListInputSchema = {
851
+ email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
852
+ user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
853
+ };
854
+ var BrowserProfileOnboardInputSchema = {
855
+ email: import_zod.z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
856
+ profile: import_zod.z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
857
+ user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory for email lookup. Defaults to the current OS Chrome profile root."),
858
+ url: import_zod.z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
859
+ label: import_zod.z.string().optional().describe("Optional human label for this setup session."),
860
+ timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the setup session may live before auto-termination. Defaults to 600.")
861
+ };
862
+ var BrowserProfileImportInputSchema = {
863
+ email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
864
+ name: import_zod.z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
865
+ chrome_profile_directory: import_zod.z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
866
+ user_data_dir: import_zod.z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
867
+ output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
868
+ browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
869
+ overwrite: import_zod.z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
870
+ };
871
+ var BrowserProfileSyncInputSchema = {
872
+ name: import_zod.z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
873
+ output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
874
+ browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
875
+ };
225
876
  var BrowserSessionInputSchema = {
226
- session_id: import_zod.z.string().describe("The session id returned by browser_open.")
877
+ session_id: import_zod.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.")
227
878
  };
228
879
  var BrowserLocateTargetSchema = import_zod.z.object({
229
880
  name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
@@ -235,43 +886,43 @@ var BrowserLocateTargetSchema = import_zod.z.object({
235
886
  message: "target requires selector or text"
236
887
  });
237
888
  var BrowserLocateInputSchema = {
238
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
889
+ session_id: import_zod.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."),
239
890
  targets: import_zod.z.array(BrowserLocateTargetSchema).min(1).max(20).describe("DOM targets to locate in the current viewport. Use selectors for exact elements, or text for visible text ranges.")
240
891
  };
241
892
  var BrowserGotoInputSchema = {
242
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
893
+ session_id: import_zod.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."),
243
894
  url: import_zod.z.string().url().describe("URL to navigate the browser to.")
244
895
  };
245
896
  var BrowserClickInputSchema = {
246
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
247
- x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use the x of an element from the latest screenshot."),
248
- y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels."),
897
+ session_id: import_zod.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."),
898
+ x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
899
+ y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
249
900
  button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
250
901
  num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
251
902
  };
252
903
  var BrowserTypeInputSchema = {
253
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
904
+ session_id: import_zod.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."),
254
905
  text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
255
906
  delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
256
907
  };
257
908
  var BrowserScrollInputSchema = {
258
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
909
+ session_id: import_zod.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."),
259
910
  delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
260
911
  delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
261
912
  x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
262
913
  y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
263
914
  };
264
915
  var BrowserPressInputSchema = {
265
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
916
+ session_id: import_zod.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."),
266
917
  keys: import_zod.z.array(import_zod.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
267
918
  };
268
919
  var BrowserReplayStopInputSchema = {
269
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
270
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start.")
920
+ session_id: import_zod.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."),
921
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
271
922
  };
272
923
  var BrowserReplayDownloadInputSchema = {
273
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
274
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
924
+ session_id: import_zod.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."),
925
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
275
926
  filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
276
927
  };
277
928
  var BrowserReplayAnnotationSchema = import_zod.z.object({
@@ -293,7 +944,7 @@ var BrowserReplayAnnotationSchema = import_zod.z.object({
293
944
  thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
294
945
  });
295
946
  var BrowserReplayMarkInputSchema = {
296
- session_id: import_zod.z.string().describe("The session id returned by browser_open. A replay must already be recording."),
947
+ session_id: import_zod.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. A replay must already be recording."),
297
948
  target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
298
949
  type: import_zod.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
299
950
  label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
@@ -304,8 +955,8 @@ var BrowserReplayMarkInputSchema = {
304
955
  duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
305
956
  };
306
957
  var BrowserReplayAnnotateInputSchema = {
307
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
308
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
958
+ session_id: import_zod.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."),
959
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
309
960
  annotations: import_zod.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations to render on the replay. Prefer annotations returned by browser_replay_mark; otherwise use exact DOM bounds from browser_locate/browser_screenshot/browser_read."),
310
961
  filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
311
962
  source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
@@ -316,12 +967,156 @@ var BrowserReplayAnnotateInputSchema = {
316
967
  var BrowserListInputSchema = {
317
968
  include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
318
969
  };
970
+ var BrowserOpenOutputSchema = {
971
+ ok: import_zod.z.boolean(),
972
+ tool: import_zod.z.literal("browser_open"),
973
+ session_id: import_zod.z.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
974
+ watch_url: import_zod.z.string().describe("Human watch/takeover URL for this browser session."),
975
+ live_view_url: NullableString.describe("Raw live-view URL from the browser service when available."),
976
+ url: NullableString.describe("Initial URL requested by the caller, when provided."),
977
+ hint: import_zod.z.string(),
978
+ raw: BrowserRawObject.optional()
979
+ };
980
+ var BrowserProfileListOutputSchema = {
981
+ ok: import_zod.z.boolean(),
982
+ tool: import_zod.z.literal("browser_profile_list"),
983
+ session_id: import_zod.z.null(),
984
+ profiles: import_zod.z.array(BrowserRawObject),
985
+ count: import_zod.z.number().int().min(0),
986
+ hint: import_zod.z.string()
987
+ };
988
+ var BrowserProfileOnboardOutputSchema = {
989
+ ok: import_zod.z.boolean(),
990
+ tool: import_zod.z.literal("browser_profile_onboard"),
991
+ session_id: import_zod.z.string(),
992
+ watch_url: import_zod.z.string(),
993
+ live_view_url: NullableString,
994
+ profile: import_zod.z.string(),
995
+ setup_url: import_zod.z.string(),
996
+ chrome_profile: BrowserRawObject.nullable(),
997
+ next_steps: import_zod.z.array(import_zod.z.string()),
998
+ raw: BrowserRawObject.optional()
999
+ };
1000
+ var BrowserProfileImportOutputSchema = {
1001
+ ok: import_zod.z.boolean(),
1002
+ tool: import_zod.z.literal("browser_profile_import"),
1003
+ session_id: import_zod.z.null(),
1004
+ profile: BrowserRawObject,
1005
+ source_profile: BrowserRawObject,
1006
+ next_steps: import_zod.z.array(import_zod.z.string()),
1007
+ hint: import_zod.z.string()
1008
+ };
1009
+ var BrowserProfileSyncOutputSchema = {
1010
+ ok: import_zod.z.boolean(),
1011
+ tool: import_zod.z.literal("browser_profile_sync"),
1012
+ session_id: import_zod.z.null(),
1013
+ profile: BrowserRawObject,
1014
+ source_profile: BrowserRawObject,
1015
+ hint: import_zod.z.string()
1016
+ };
1017
+ var BrowserScreenshotOutputSchema = {
1018
+ ...BrowserBaseOutput,
1019
+ tool: import_zod.z.literal("browser_screenshot"),
1020
+ url: NullableString,
1021
+ title: NullableString,
1022
+ text: import_zod.z.string(),
1023
+ elements: import_zod.z.array(BrowserElementOutput),
1024
+ screenshot: import_zod.z.object({
1025
+ mime_type: import_zod.z.string(),
1026
+ inline: import_zod.z.boolean()
1027
+ }).nullable()
1028
+ };
1029
+ var BrowserReadOutputSchema = {
1030
+ ...BrowserBaseOutput,
1031
+ tool: import_zod.z.literal("browser_read"),
1032
+ url: NullableString,
1033
+ title: NullableString,
1034
+ text: import_zod.z.string(),
1035
+ elements: import_zod.z.array(BrowserElementOutput),
1036
+ raw: BrowserRawObject.optional()
1037
+ };
1038
+ var BrowserLocateOutputSchema = {
1039
+ ...BrowserBaseOutput,
1040
+ tool: import_zod.z.literal("browser_locate"),
1041
+ url: NullableString,
1042
+ title: NullableString,
1043
+ viewport: BrowserRawObject.nullable(),
1044
+ replay: BrowserRawObject.nullable(),
1045
+ targets: import_zod.z.array(BrowserRawObject),
1046
+ raw: BrowserRawObject.optional()
1047
+ };
1048
+ var BrowserActionOutputSchema = {
1049
+ ...BrowserBaseOutput,
1050
+ result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
1051
+ nextRecommendedTool: import_zod.z.string().nullable()
1052
+ };
1053
+ var BrowserReplayStartOutputSchema = {
1054
+ ...BrowserReplayBaseOutput,
1055
+ tool: import_zod.z.literal("browser_replay_start"),
1056
+ view_url: NullableString,
1057
+ download_url: NullableString,
1058
+ raw: BrowserRawObject.optional()
1059
+ };
1060
+ var BrowserReplayStopOutputSchema = {
1061
+ ...BrowserReplayBaseOutput,
1062
+ tool: import_zod.z.literal("browser_replay_stop"),
1063
+ view_url: NullableString,
1064
+ download_url: NullableString,
1065
+ raw: BrowserRawObject.optional()
1066
+ };
1067
+ var BrowserListReplaysOutputSchema = {
1068
+ ...BrowserBaseOutput,
1069
+ tool: import_zod.z.literal("browser_list_replays"),
1070
+ replays: import_zod.z.array(BrowserRawObject),
1071
+ count: import_zod.z.number().int().min(0)
1072
+ };
1073
+ var BrowserReplayDownloadOutputSchema = {
1074
+ ...BrowserReplayBaseOutput,
1075
+ tool: import_zod.z.literal("browser_replay_download"),
1076
+ file_path: NullableString,
1077
+ bytes: import_zod.z.number().int().min(0).nullable(),
1078
+ mime_type: NullableString,
1079
+ download_url: NullableString
1080
+ };
1081
+ var BrowserReplayMarkOutputSchema = {
1082
+ ...BrowserReplayBaseOutput,
1083
+ tool: import_zod.z.literal("browser_replay_mark"),
1084
+ annotation: BrowserRawObject,
1085
+ source_width: import_zod.z.number().nullable(),
1086
+ source_height: import_zod.z.number().nullable(),
1087
+ target: BrowserRawObject.nullable(),
1088
+ hint: import_zod.z.string()
1089
+ };
1090
+ var BrowserReplayAnnotateOutputSchema = {
1091
+ ...BrowserReplayBaseOutput,
1092
+ tool: import_zod.z.literal("browser_replay_annotate"),
1093
+ source_file_path: NullableString,
1094
+ annotated_file_path: NullableString,
1095
+ bytes: import_zod.z.number().int().min(0).nullable(),
1096
+ width: import_zod.z.number().int().min(0).nullable(),
1097
+ height: import_zod.z.number().int().min(0).nullable(),
1098
+ annotation_count: import_zod.z.number().int().min(0).nullable(),
1099
+ mime_type: NullableString
1100
+ };
1101
+ var BrowserCloseOutputSchema = {
1102
+ ...BrowserBaseOutput,
1103
+ tool: import_zod.z.literal("browser_close"),
1104
+ closed: import_zod.z.boolean(),
1105
+ raw: BrowserRawObject.optional()
1106
+ };
1107
+ var BrowserListSessionsOutputSchema = {
1108
+ ok: import_zod.z.boolean(),
1109
+ tool: import_zod.z.literal("browser_list_sessions"),
1110
+ session_id: import_zod.z.null(),
1111
+ sessions: import_zod.z.array(BrowserRawObject),
1112
+ count: import_zod.z.number().int().min(0)
1113
+ };
319
1114
 
320
1115
  // src/mcp/replay-annotator.ts
321
1116
  var import_node_child_process = require("child_process");
322
- var import_promises = require("fs/promises");
323
- var import_node_os = require("os");
324
- var import_node_path = require("path");
1117
+ var import_promises3 = require("fs/promises");
1118
+ var import_node_os3 = require("os");
1119
+ var import_node_path4 = require("path");
325
1120
  var import_node_util = require("util");
326
1121
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
327
1122
  function finiteNumber(value) {
@@ -544,10 +1339,10 @@ function ffmpegFilterPath(path) {
544
1339
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
545
1340
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
546
1341
  const size = await videoSize(inputFilePath);
547
- const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
548
- const assPath = (0, import_node_path.join)(tmp, "annotations.ass");
1342
+ const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcp-scraper-ass-"));
1343
+ const assPath = (0, import_node_path4.join)(tmp, "annotations.ass");
549
1344
  try {
550
- await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
1345
+ await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
551
1346
  await execFileAsync("ffmpeg", [
552
1347
  "-y",
553
1348
  "-i",
@@ -564,7 +1359,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
564
1359
  "copy",
565
1360
  outputFilePath
566
1361
  ], { maxBuffer: 1024 * 1024 * 20 });
567
- const out = await (0, import_promises.stat)(outputFilePath);
1362
+ const out = await (0, import_promises3.stat)(outputFilePath);
568
1363
  return {
569
1364
  filePath: outputFilePath,
570
1365
  bytes: out.size,
@@ -573,16 +1368,48 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
573
1368
  annotationCount: options.annotations.length
574
1369
  };
575
1370
  } finally {
576
- await (0, import_promises.rm)(tmp, { recursive: true, force: true });
1371
+ await (0, import_promises3.rm)(tmp, { recursive: true, force: true });
577
1372
  }
578
1373
  }
579
1374
 
580
1375
  // src/mcp/browser-agent-mcp-server.ts
581
- function textResult(value, isError = false) {
582
- return { content: [{ type: "text", text: JSON.stringify(value) }], isError };
1376
+ function structuredResult(value, isError = false) {
1377
+ return {
1378
+ content: [{ type: "text", text: JSON.stringify(value) }],
1379
+ structuredContent: value,
1380
+ isError
1381
+ };
1382
+ }
1383
+ function errorMessage(value) {
1384
+ if (value && typeof value === "object") {
1385
+ const data = value;
1386
+ if (typeof data.error === "string") return data.error;
1387
+ if (typeof data.message === "string") return data.message;
1388
+ }
1389
+ return typeof value === "string" ? value : "Browser Agent request failed";
1390
+ }
1391
+ function errorResult(tool, value, sessionId = null, replayId = null) {
1392
+ return structuredResult({
1393
+ ok: false,
1394
+ tool,
1395
+ session_id: sessionId,
1396
+ ...replayId !== null ? { replay_id: replayId } : {},
1397
+ error: errorMessage(value),
1398
+ raw: value && typeof value === "object" ? value : { value }
1399
+ }, true);
1400
+ }
1401
+ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
1402
+ if (!ok) return errorResult(tool, data, sessionId);
1403
+ return structuredResult({
1404
+ ok: true,
1405
+ tool,
1406
+ session_id: sessionId,
1407
+ result: data && typeof data === "object" ? data : { value: data },
1408
+ nextRecommendedTool
1409
+ });
583
1410
  }
584
1411
  function outputBaseDir() {
585
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
1412
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
586
1413
  }
587
1414
  function safeFilePart(value) {
588
1415
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -591,7 +1418,7 @@ function replayFilePath(sessionId, replayId, filename) {
591
1418
  const requested = filename?.trim();
592
1419
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
593
1420
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
594
- return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
1421
+ return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
595
1422
  }
596
1423
  function annotatedReplayFilePath(sessionId, replayId, filename) {
597
1424
  const requested = filename?.trim();
@@ -622,6 +1449,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
622
1449
  const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
623
1450
  const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
624
1451
  const timeoutMs = opts.timeoutMs ?? 9e4;
1452
+ const localBrowser = new LocalBrowserAgentService();
625
1453
  async function req(method, path, body) {
626
1454
  try {
627
1455
  const res = await fetch(`${baseUrl2}${path}`, {
@@ -650,8 +1478,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
650
1478
  }
651
1479
  const bytes = Buffer.from(await res.arrayBuffer());
652
1480
  const filePath = replayFilePath(sessionId, replayId, filename);
653
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
654
- (0, import_node_fs.writeFileSync)(filePath, bytes);
1481
+ (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1482
+ (0, import_node_fs3.writeFileSync)(filePath, bytes);
655
1483
  return {
656
1484
  ok: true,
657
1485
  data: {
@@ -673,30 +1501,211 @@ function registerBrowserAgentMcpTools(server2, opts) {
673
1501
  idempotentHint: false,
674
1502
  openWorldHint: true
675
1503
  });
1504
+ function selectChromeProfile(matches, email) {
1505
+ const wanted = email?.trim().toLowerCase();
1506
+ if (!wanted) return matches[0] ?? null;
1507
+ return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
1508
+ }
1509
+ function localReplayUnsupported(tool, sessionId = null, replayId = null) {
1510
+ return errorResult(tool, {
1511
+ error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
1512
+ mode: "local"
1513
+ }, sessionId, replayId);
1514
+ }
1515
+ server2.registerTool(
1516
+ "browser_profile_list",
1517
+ {
1518
+ title: "List Local Chrome Profiles",
1519
+ 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.",
1520
+ inputSchema: BrowserProfileListInputSchema,
1521
+ outputSchema: BrowserProfileListOutputSchema,
1522
+ annotations: annotations("List Local Chrome Profiles", true)
1523
+ },
1524
+ async (input) => {
1525
+ try {
1526
+ const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
1527
+ return structuredResult({
1528
+ ok: true,
1529
+ tool: "browser_profile_list",
1530
+ session_id: null,
1531
+ profiles,
1532
+ count: profiles.length,
1533
+ hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
1534
+ });
1535
+ } catch (err) {
1536
+ return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
1537
+ }
1538
+ }
1539
+ );
1540
+ server2.registerTool(
1541
+ "browser_profile_import",
1542
+ {
1543
+ title: "Import Local Chrome Profile",
1544
+ 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.",
1545
+ inputSchema: BrowserProfileImportInputSchema,
1546
+ outputSchema: BrowserProfileImportOutputSchema,
1547
+ annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1548
+ },
1549
+ async (input) => {
1550
+ try {
1551
+ const result = await importChromeProfile({
1552
+ email: input.email,
1553
+ name: input.name,
1554
+ chromeProfileDirectory: input.chrome_profile_directory,
1555
+ sourceUserDataDir: input.user_data_dir,
1556
+ outputDir: input.output_dir,
1557
+ overwrite: input.overwrite,
1558
+ browserExecutablePath: input.browser_executable_path
1559
+ });
1560
+ return structuredResult({
1561
+ ok: true,
1562
+ tool: "browser_profile_import",
1563
+ session_id: null,
1564
+ profile: result.profile,
1565
+ source_profile: result.sourceProfile,
1566
+ next_steps: [
1567
+ `Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
1568
+ `Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
1569
+ "Restart the MCP client so it starts the local browser mode process.",
1570
+ "Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
1571
+ ],
1572
+ hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
1573
+ });
1574
+ } catch (err) {
1575
+ return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
1576
+ }
1577
+ }
1578
+ );
1579
+ server2.registerTool(
1580
+ "browser_profile_sync",
1581
+ {
1582
+ title: "Sync Local Chrome Profile",
1583
+ 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.",
1584
+ inputSchema: BrowserProfileSyncInputSchema,
1585
+ outputSchema: BrowserProfileSyncOutputSchema,
1586
+ annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1587
+ },
1588
+ async (input) => {
1589
+ try {
1590
+ const result = await syncImportedChromeProfile(input.name, {
1591
+ outputDir: input.output_dir,
1592
+ browserExecutablePath: input.browser_executable_path
1593
+ });
1594
+ return structuredResult({
1595
+ ok: true,
1596
+ tool: "browser_profile_sync",
1597
+ session_id: null,
1598
+ profile: result.profile,
1599
+ source_profile: result.sourceProfile,
1600
+ hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
1601
+ });
1602
+ } catch (err) {
1603
+ return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
1604
+ }
1605
+ }
1606
+ );
1607
+ server2.registerTool(
1608
+ "browser_profile_onboard",
1609
+ {
1610
+ title: "Onboard Hosted Browser Profile",
1611
+ 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.",
1612
+ inputSchema: BrowserProfileOnboardInputSchema,
1613
+ outputSchema: BrowserProfileOnboardOutputSchema,
1614
+ annotations: annotations("Onboard Hosted Browser Profile")
1615
+ },
1616
+ async (input) => {
1617
+ let chromeProfile = null;
1618
+ if (input.email) {
1619
+ try {
1620
+ chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
1621
+ } catch (err) {
1622
+ return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
1623
+ }
1624
+ }
1625
+ const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
1626
+ if (!profile) {
1627
+ return errorResult("browser_profile_onboard", {
1628
+ error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
1629
+ });
1630
+ }
1631
+ const setupUrl = input.url ?? "https://accounts.google.com/";
1632
+ const open = await req("POST", "/agent/sessions", {
1633
+ label: input.label ?? `Profile setup: ${profile}`,
1634
+ profile,
1635
+ save_profile_changes: true,
1636
+ timeout_seconds: input.timeout_seconds
1637
+ });
1638
+ if (!open.ok) return errorResult("browser_profile_onboard", open.data);
1639
+ const session = open.data;
1640
+ const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
1641
+ if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
1642
+ return structuredResult({
1643
+ ok: true,
1644
+ tool: "browser_profile_onboard",
1645
+ session_id: session.session_id,
1646
+ watch_url: `${consoleBase}/console/${session.session_id}`,
1647
+ live_view_url: session.live_view_url ?? null,
1648
+ profile,
1649
+ setup_url: setupUrl,
1650
+ chrome_profile: chromeProfile,
1651
+ next_steps: [
1652
+ "Open the watch_url and complete the login manually.",
1653
+ "After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
1654
+ "Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
1655
+ ],
1656
+ raw: session
1657
+ });
1658
+ }
1659
+ );
676
1660
  server2.registerTool(
677
1661
  "browser_open",
678
1662
  {
679
1663
  title: "Open Browser Session",
680
- 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.",
1664
+ 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.",
681
1665
  inputSchema: BrowserOpenInputSchema,
1666
+ outputSchema: BrowserOpenOutputSchema,
682
1667
  annotations: annotations("Open Browser Session")
683
1668
  },
684
1669
  async (input) => {
1670
+ if (localBrowserModeEnabled()) {
1671
+ try {
1672
+ const session2 = await localBrowser.open(input);
1673
+ return structuredResult({
1674
+ ok: true,
1675
+ tool: "browser_open",
1676
+ session_id: session2.session_id,
1677
+ watch_url: session2.watch_url,
1678
+ live_view_url: null,
1679
+ url: input.url ?? null,
1680
+ hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
1681
+ raw: session2
1682
+ });
1683
+ } catch (err) {
1684
+ return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
1685
+ }
1686
+ }
1687
+ const profile = input.profile ?? browserServiceProfileName();
1688
+ const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
685
1689
  const open = await req("POST", "/agent/sessions", {
686
1690
  label: input.label,
687
- profile: input.profile,
1691
+ ...profile ? { profile } : {},
1692
+ ...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
688
1693
  timeout_seconds: input.timeout_seconds
689
1694
  });
690
- if (!open.ok) return textResult(open.data, true);
1695
+ if (!open.ok) return errorResult("browser_open", open.data);
691
1696
  const session = open.data;
692
1697
  if (input.url) {
693
1698
  await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: input.url });
694
1699
  }
695
- return textResult({
1700
+ return structuredResult({
1701
+ ok: true,
1702
+ tool: "browser_open",
696
1703
  session_id: session.session_id,
697
1704
  watch_url: `${consoleBase}/console/${session.session_id}`,
698
1705
  live_view_url: session.live_view_url ?? null,
699
- hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot."
1706
+ url: input.url ?? null,
1707
+ hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
1708
+ raw: session
700
1709
  });
701
1710
  }
702
1711
  );
@@ -706,19 +1715,51 @@ function registerBrowserAgentMcpTools(server2, opts) {
706
1715
  title: "See Page (Screenshot + Elements)",
707
1716
  description: "Capture what the browser currently shows. Returns a screenshot image PLUS a text snapshot listing interactive elements with their center x,y coordinates, the page url and title, and visible text. This is your primary way to perceive the page. Click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
708
1717
  inputSchema: BrowserSessionInputSchema,
1718
+ outputSchema: BrowserScreenshotOutputSchema,
709
1719
  annotations: annotations("See Page", true)
710
1720
  },
711
1721
  async (input) => {
1722
+ if (localBrowserModeEnabled()) {
1723
+ try {
1724
+ const res2 = await localBrowser.screenshot(input.session_id);
1725
+ const content2 = [];
1726
+ if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
1727
+ const structured2 = {
1728
+ ok: true,
1729
+ tool: "browser_screenshot",
1730
+ session_id: input.session_id,
1731
+ url: res2.url ?? null,
1732
+ title: res2.title ?? null,
1733
+ text: res2.text,
1734
+ elements: res2.elements,
1735
+ screenshot: { mime_type: res2.mime_type, inline: true }
1736
+ };
1737
+ content2.push({ type: "text", text: JSON.stringify(structured2) });
1738
+ return { content: content2, structuredContent: structured2 };
1739
+ } catch (err) {
1740
+ return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1741
+ }
1742
+ }
712
1743
  const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
713
- if (!res.ok) return textResult(res.data, true);
1744
+ if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
714
1745
  const { image_base64, mime_type, url, title, elements, text } = res.data;
715
1746
  const content = [];
716
1747
  if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
1748
+ const structured = {
1749
+ ok: true,
1750
+ tool: "browser_screenshot",
1751
+ session_id: input.session_id,
1752
+ url: url ?? null,
1753
+ title: title ?? null,
1754
+ text: typeof text === "string" ? text : "",
1755
+ elements: Array.isArray(elements) ? elements : [],
1756
+ screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
1757
+ };
717
1758
  content.push({
718
1759
  type: "text",
719
- text: JSON.stringify({ url, title, elements, text })
1760
+ text: JSON.stringify(structured)
720
1761
  });
721
- return { content };
1762
+ return { content, structuredContent: structured };
722
1763
  }
723
1764
  );
724
1765
  server2.registerTool(
@@ -727,11 +1768,39 @@ function registerBrowserAgentMcpTools(server2, opts) {
727
1768
  title: "Read Page Text + Elements",
728
1769
  description: "Return the page url, title, visible text, and the list of interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a target element to click.",
729
1770
  inputSchema: BrowserSessionInputSchema,
1771
+ outputSchema: BrowserReadOutputSchema,
730
1772
  annotations: annotations("Read Page", true)
731
1773
  },
732
1774
  async (input) => {
1775
+ if (localBrowserModeEnabled()) {
1776
+ try {
1777
+ const res2 = await localBrowser.read(input.session_id);
1778
+ return structuredResult({
1779
+ ok: true,
1780
+ tool: "browser_read",
1781
+ session_id: input.session_id,
1782
+ url: res2.url ?? null,
1783
+ title: res2.title ?? null,
1784
+ text: res2.text,
1785
+ elements: res2.elements,
1786
+ raw: res2
1787
+ });
1788
+ } catch (err) {
1789
+ return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1790
+ }
1791
+ }
733
1792
  const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
734
- return textResult(res.data, !res.ok);
1793
+ if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
1794
+ return structuredResult({
1795
+ ok: true,
1796
+ tool: "browser_read",
1797
+ session_id: input.session_id,
1798
+ url: res.data?.url ?? null,
1799
+ title: res.data?.title ?? null,
1800
+ text: typeof res.data?.text === "string" ? res.data.text : "",
1801
+ elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
1802
+ raw: res.data
1803
+ });
735
1804
  }
736
1805
  );
737
1806
  server2.registerTool(
@@ -740,86 +1809,161 @@ function registerBrowserAgentMcpTools(server2, opts) {
740
1809
  title: "Locate DOM Targets",
741
1810
  description: "Locate exact visible DOM elements or text ranges in the current browser viewport and return left/top/width/height bounds in screenshot pixels. Use this before drawing annotations or when a callout must literally circle, box, underline, or point to a real element. Prefer CSS selectors for exact UI elements; use text when selector is unknown. When a replay is actively recording, the result includes replay_elapsed_seconds for timing.",
742
1811
  inputSchema: BrowserLocateInputSchema,
1812
+ outputSchema: BrowserLocateOutputSchema,
743
1813
  annotations: annotations("Locate DOM Targets", true)
744
1814
  },
745
1815
  async (input) => {
1816
+ if (localBrowserModeEnabled()) {
1817
+ try {
1818
+ const res2 = await localBrowser.locate(input.session_id, input.targets);
1819
+ return structuredResult({
1820
+ ok: true,
1821
+ tool: "browser_locate",
1822
+ session_id: input.session_id,
1823
+ url: res2.url ?? null,
1824
+ title: res2.title ?? null,
1825
+ viewport: res2.viewport ?? null,
1826
+ replay: null,
1827
+ targets: Array.isArray(res2.targets) ? res2.targets : [],
1828
+ raw: res2
1829
+ });
1830
+ } catch (err) {
1831
+ return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1832
+ }
1833
+ }
746
1834
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
747
- return textResult(res.data, !res.ok);
1835
+ if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
1836
+ return structuredResult({
1837
+ ok: true,
1838
+ tool: "browser_locate",
1839
+ session_id: input.session_id,
1840
+ url: res.data?.url ?? null,
1841
+ title: res.data?.title ?? null,
1842
+ viewport: res.data?.viewport ?? null,
1843
+ replay: res.data?.replay ?? null,
1844
+ targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
1845
+ raw: res.data
1846
+ });
748
1847
  }
749
1848
  );
750
1849
  server2.registerTool(
751
1850
  "browser_goto",
752
1851
  {
753
1852
  title: "Navigate To URL",
754
- description: "Navigate the browser to a URL. Follow with browser_screenshot to see the result.",
1853
+ description: "Navigate an existing browser session to a URL when the user asks you to go to another page or continue browsing in the same session. Use browser_open first if no session exists. Follow with browser_screenshot to see redirects, login walls, CAPTCHA/Cloudflare state, or the loaded page.",
755
1854
  inputSchema: BrowserGotoInputSchema,
1855
+ outputSchema: BrowserActionOutputSchema,
756
1856
  annotations: annotations("Navigate To URL")
757
1857
  },
758
1858
  async (input) => {
1859
+ if (localBrowserModeEnabled()) {
1860
+ try {
1861
+ const res2 = await localBrowser.goto(input.session_id, input.url);
1862
+ return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
1863
+ } catch (err) {
1864
+ return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1865
+ }
1866
+ }
759
1867
  const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
760
- return textResult(res.data, !res.ok);
1868
+ return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
761
1869
  }
762
1870
  );
763
1871
  server2.registerTool(
764
1872
  "browser_click",
765
1873
  {
766
1874
  title: "Click",
767
- description: "Click at x,y (screenshot pixel coordinates). Use the x,y of a target element from the latest browser_screenshot or browser_read.",
1875
+ description: "Click a visible page target using screenshot pixel coordinates. Use this when the user asks you to press a button, open a menu, choose a result, or activate a visible UI element. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates. Follow with browser_screenshot when the click may change the page.",
768
1876
  inputSchema: BrowserClickInputSchema,
1877
+ outputSchema: BrowserActionOutputSchema,
769
1878
  annotations: annotations("Click")
770
1879
  },
771
1880
  async (input) => {
1881
+ if (localBrowserModeEnabled()) {
1882
+ try {
1883
+ const res2 = await localBrowser.click(input.session_id, input);
1884
+ return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
1885
+ } catch (err) {
1886
+ return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1887
+ }
1888
+ }
772
1889
  const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
773
1890
  x: input.x,
774
1891
  y: input.y,
775
1892
  button: input.button,
776
1893
  num_clicks: input.num_clicks
777
1894
  });
778
- return textResult(res.data, !res.ok);
1895
+ return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
779
1896
  }
780
1897
  );
781
1898
  server2.registerTool(
782
1899
  "browser_type",
783
1900
  {
784
1901
  title: "Type Text",
785
- description: 'Type text at the current focus. Click an input field first to focus it. Use browser_press with ["Return"] to submit.',
1902
+ description: 'Type text into the currently focused browser field. Use this when the user asks you to fill a search box, form field, login field, or editable text area. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit, and browser_screenshot afterward when page state matters.',
786
1903
  inputSchema: BrowserTypeInputSchema,
1904
+ outputSchema: BrowserActionOutputSchema,
787
1905
  annotations: annotations("Type Text")
788
1906
  },
789
1907
  async (input) => {
1908
+ if (localBrowserModeEnabled()) {
1909
+ try {
1910
+ const res2 = await localBrowser.type(input.session_id, input);
1911
+ return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
1912
+ } catch (err) {
1913
+ return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1914
+ }
1915
+ }
790
1916
  const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
791
- return textResult(res.data, !res.ok);
1917
+ return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
792
1918
  }
793
1919
  );
794
1920
  server2.registerTool(
795
1921
  "browser_scroll",
796
1922
  {
797
1923
  title: "Scroll",
798
- description: "Scroll the page. Positive delta_y scrolls down. Follow with browser_screenshot to see newly revealed content.",
1924
+ description: "Scroll the page to reveal more content before reading, clicking, or locating elements. Positive delta_y scrolls down; negative delta_y scrolls up. Follow with browser_screenshot or browser_read to inspect newly revealed content.",
799
1925
  inputSchema: BrowserScrollInputSchema,
1926
+ outputSchema: BrowserActionOutputSchema,
800
1927
  annotations: annotations("Scroll")
801
1928
  },
802
1929
  async (input) => {
1930
+ if (localBrowserModeEnabled()) {
1931
+ try {
1932
+ const res2 = await localBrowser.scroll(input.session_id, input);
1933
+ return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
1934
+ } catch (err) {
1935
+ return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1936
+ }
1937
+ }
803
1938
  const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
804
1939
  delta_y: input.delta_y,
805
1940
  delta_x: input.delta_x,
806
1941
  x: input.x,
807
1942
  y: input.y
808
1943
  });
809
- return textResult(res.data, !res.ok);
1944
+ return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
810
1945
  }
811
1946
  );
812
1947
  server2.registerTool(
813
1948
  "browser_press",
814
1949
  {
815
1950
  title: "Press Keys",
816
- description: 'Press keys or combinations, e.g. ["Return"] to submit, ["Ctrl+a"] to select all, ["Ctrl+Shift+Tab"] to switch tabs.',
1951
+ description: 'Press keyboard keys or combinations in the active browser session. Use this for submit, Escape, Tab navigation, select-all, or keyboard shortcuts. Examples: ["Return"], ["Escape"], ["Tab"], ["Ctrl+a"], ["Ctrl+Shift+Tab"]. Use browser_type for text entry and browser_screenshot after keypresses that may change the page.',
817
1952
  inputSchema: BrowserPressInputSchema,
1953
+ outputSchema: BrowserActionOutputSchema,
818
1954
  annotations: annotations("Press Keys")
819
1955
  },
820
1956
  async (input) => {
1957
+ if (localBrowserModeEnabled()) {
1958
+ try {
1959
+ const res2 = await localBrowser.press(input.session_id, input);
1960
+ return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
1961
+ } catch (err) {
1962
+ return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1963
+ }
1964
+ }
821
1965
  const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
822
- return textResult(res.data, !res.ok);
1966
+ return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
823
1967
  }
824
1968
  );
825
1969
  server2.registerTool(
@@ -828,11 +1972,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
828
1972
  title: "Start Recording",
829
1973
  description: "Start recording an MP4 replay of the session. Returns replay_id, view_url when available, and a download_url. Use to capture a task for later review; stop with browser_replay_stop.",
830
1974
  inputSchema: BrowserSessionInputSchema,
1975
+ outputSchema: BrowserReplayStartOutputSchema,
831
1976
  annotations: annotations("Start Recording")
832
1977
  },
833
1978
  async (input) => {
1979
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
834
1980
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
835
- return textResult(res.data, !res.ok);
1981
+ if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
1982
+ return structuredResult({
1983
+ ok: true,
1984
+ tool: "browser_replay_start",
1985
+ session_id: input.session_id,
1986
+ replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
1987
+ view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
1988
+ download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
1989
+ raw: res.data
1990
+ });
836
1991
  }
837
1992
  );
838
1993
  server2.registerTool(
@@ -841,11 +1996,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
841
1996
  title: "Stop Recording",
842
1997
  description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 locally.",
843
1998
  inputSchema: BrowserReplayStopInputSchema,
1999
+ outputSchema: BrowserReplayStopOutputSchema,
844
2000
  annotations: annotations("Stop Recording")
845
2001
  },
846
2002
  async (input) => {
2003
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
847
2004
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
848
- return textResult(res.data, !res.ok);
2005
+ if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
2006
+ return structuredResult({
2007
+ ok: true,
2008
+ tool: "browser_replay_stop",
2009
+ session_id: input.session_id,
2010
+ replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
2011
+ view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
2012
+ download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
2013
+ raw: res.data
2014
+ });
849
2015
  }
850
2016
  );
851
2017
  server2.registerTool(
@@ -854,11 +2020,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
854
2020
  title: "List Replay Videos",
855
2021
  description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
856
2022
  inputSchema: BrowserSessionInputSchema,
2023
+ outputSchema: BrowserListReplaysOutputSchema,
857
2024
  annotations: annotations("List Replay Videos", true)
858
2025
  },
859
2026
  async (input) => {
2027
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
860
2028
  const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
861
- return textResult(res.data, !res.ok);
2029
+ if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
2030
+ const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
2031
+ return structuredResult({
2032
+ ok: true,
2033
+ tool: "browser_list_replays",
2034
+ session_id: input.session_id,
2035
+ replays,
2036
+ count: replays.length
2037
+ });
862
2038
  }
863
2039
  );
864
2040
  server2.registerTool(
@@ -867,11 +2043,23 @@ function registerBrowserAgentMcpTools(server2, opts) {
867
2043
  title: "Download Replay MP4",
868
2044
  description: "Download a replay recording through MCP Scraper and save the MP4 locally under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
869
2045
  inputSchema: BrowserReplayDownloadInputSchema,
870
- annotations: annotations("Download Replay MP4", true)
2046
+ outputSchema: BrowserReplayDownloadOutputSchema,
2047
+ annotations: annotations("Download Replay MP4")
871
2048
  },
872
2049
  async (input) => {
2050
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
873
2051
  const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
874
- return textResult(res.data, !res.ok);
2052
+ if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
2053
+ return structuredResult({
2054
+ ok: true,
2055
+ tool: "browser_replay_download",
2056
+ session_id: input.session_id,
2057
+ replay_id: input.replay_id,
2058
+ file_path: res.data?.file_path ?? null,
2059
+ bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
2060
+ mime_type: res.data?.mime_type ?? null,
2061
+ download_url: res.data?.download_url ?? null
2062
+ });
875
2063
  }
876
2064
  );
877
2065
  server2.registerTool(
@@ -880,19 +2068,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
880
2068
  title: "Mark Replay Annotation",
881
2069
  description: "While a replay is actively recording, locate one exact DOM target and return a ready-to-use annotation object with DOM bounds and replay-relative timing. Use this instead of guessing start_seconds or drawing rough rectangles. Workflow: start browser_replay_start, navigate until the target is visible and stable, call browser_replay_mark for each callout, then stop the replay and pass the returned annotations to browser_replay_annotate.",
882
2070
  inputSchema: BrowserReplayMarkInputSchema,
2071
+ outputSchema: BrowserReplayMarkOutputSchema,
883
2072
  annotations: annotations("Mark Replay Annotation", true)
884
2073
  },
885
2074
  async (input) => {
2075
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
886
2076
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
887
- if (!res.ok) return textResult(res.data, true);
2077
+ if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
888
2078
  const target = res.data?.targets?.[0];
889
2079
  const element = target?.element;
890
2080
  const elapsed = res.data?.replay?.replay_elapsed_seconds;
891
2081
  if (!target?.found || !element) {
892
- return textResult({ error: target?.error ?? "target not found in current viewport", target }, true);
2082
+ return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
893
2083
  }
894
2084
  if (!finiteNumber2(elapsed)) {
895
- return textResult({ error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, true);
2085
+ return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
896
2086
  }
897
2087
  const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
898
2088
  const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
@@ -909,7 +2099,11 @@ function registerBrowserAgentMcpTools(server2, opts) {
909
2099
  ...input.color ? { color: input.color } : {},
910
2100
  ...input.thickness ? { thickness: input.thickness } : {}
911
2101
  };
912
- return textResult({
2102
+ return structuredResult({
2103
+ ok: true,
2104
+ tool: "browser_replay_mark",
2105
+ session_id: input.session_id,
2106
+ replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
913
2107
  annotation,
914
2108
  source_width: padded.sourceWidth,
915
2109
  source_height: padded.sourceHeight,
@@ -925,16 +2119,18 @@ function registerBrowserAgentMcpTools(server2, opts) {
925
2119
  title: "Annotate Replay MP4",
926
2120
  description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 locally. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
927
2121
  inputSchema: BrowserReplayAnnotateInputSchema,
928
- annotations: annotations("Annotate Replay MP4", true)
2122
+ outputSchema: BrowserReplayAnnotateOutputSchema,
2123
+ annotations: annotations("Annotate Replay MP4")
929
2124
  },
930
2125
  async (input) => {
2126
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
931
2127
  const sourceName = input.filename ? `${input.filename}-source` : void 0;
932
2128
  const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
933
- if (!downloaded.ok) return textResult(downloaded.data, true);
2129
+ if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
934
2130
  try {
935
2131
  const sourcePath = String(downloaded.data.file_path);
936
2132
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
937
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
2133
+ (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
938
2134
  const result = await annotateReplayVideo(sourcePath, outputPath, {
939
2135
  annotations: input.annotations,
940
2136
  sourceWidth: input.source_width,
@@ -942,7 +2138,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
942
2138
  sourceLeftOffset: input.source_left_offset,
943
2139
  sourceTopOffset: input.source_top_offset
944
2140
  });
945
- return textResult({
2141
+ return structuredResult({
2142
+ ok: true,
2143
+ tool: "browser_replay_annotate",
2144
+ session_id: input.session_id,
946
2145
  replay_id: input.replay_id,
947
2146
  source_file_path: sourcePath,
948
2147
  annotated_file_path: result.filePath,
@@ -953,7 +2152,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
953
2152
  mime_type: "video/mp4"
954
2153
  });
955
2154
  } catch (err) {
956
- return textResult({ error: err instanceof Error ? err.message : String(err) }, true);
2155
+ return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
957
2156
  }
958
2157
  }
959
2158
  );
@@ -961,41 +2160,80 @@ function registerBrowserAgentMcpTools(server2, opts) {
961
2160
  "browser_close",
962
2161
  {
963
2162
  title: "Close Browser Session",
964
- description: "Close and release the browser session when the task is done.",
2163
+ description: "Close and release a browser session when the task is done, when the user asks to stop the browser, or when active browser billing should end. Use browser_list_sessions first if you need to recover the session_id.",
965
2164
  inputSchema: BrowserSessionInputSchema,
2165
+ outputSchema: BrowserCloseOutputSchema,
966
2166
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
967
2167
  },
968
2168
  async (input) => {
2169
+ if (localBrowserModeEnabled()) {
2170
+ try {
2171
+ const res2 = await localBrowser.close(input.session_id);
2172
+ return structuredResult({
2173
+ ok: true,
2174
+ tool: "browser_close",
2175
+ session_id: input.session_id,
2176
+ closed: true,
2177
+ raw: res2
2178
+ });
2179
+ } catch (err) {
2180
+ return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
2181
+ }
2182
+ }
969
2183
  const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
970
- return textResult(res.data, !res.ok);
2184
+ if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
2185
+ return structuredResult({
2186
+ ok: true,
2187
+ tool: "browser_close",
2188
+ session_id: input.session_id,
2189
+ closed: true,
2190
+ raw: res.data
2191
+ });
971
2192
  }
972
2193
  );
973
2194
  server2.registerTool(
974
2195
  "browser_list_sessions",
975
2196
  {
976
2197
  title: "List Browser Sessions",
977
- description: "List your browser sessions and their status, with a watch_url for each.",
2198
+ description: "List browser sessions and their status, with a watch_url for each. Use this to recover a session_id, find an active browser, or decide which session to close. Use browser_open to create a new session.",
978
2199
  inputSchema: BrowserListInputSchema,
2200
+ outputSchema: BrowserListSessionsOutputSchema,
979
2201
  annotations: annotations("List Browser Sessions", true)
980
2202
  },
981
2203
  async (input) => {
2204
+ if (localBrowserModeEnabled()) {
2205
+ const sessions2 = localBrowser.list(input.include_closed);
2206
+ return structuredResult({
2207
+ ok: true,
2208
+ tool: "browser_list_sessions",
2209
+ session_id: null,
2210
+ sessions: sessions2,
2211
+ count: sessions2.length
2212
+ });
2213
+ }
982
2214
  const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
983
- if (!res.ok) return textResult(res.data, true);
2215
+ if (!res.ok) return errorResult("browser_list_sessions", res.data);
984
2216
  const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
985
- return textResult({ sessions });
2217
+ return structuredResult({
2218
+ ok: true,
2219
+ tool: "browser_list_sessions",
2220
+ session_id: null,
2221
+ sessions,
2222
+ count: sessions.length
2223
+ });
986
2224
  }
987
2225
  );
988
2226
  }
989
2227
 
990
2228
  // src/mcp/paa-mcp-server.ts
991
2229
  var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
992
- var import_node_fs3 = require("fs");
993
- var import_node_path4 = require("path");
2230
+ var import_node_fs5 = require("fs");
2231
+ var import_node_path7 = require("path");
994
2232
 
995
2233
  // src/mcp/mcp-response-formatter.ts
996
- var import_node_fs2 = require("fs");
997
- var import_node_os3 = require("os");
998
- var import_node_path3 = require("path");
2234
+ var import_node_fs4 = require("fs");
2235
+ var import_node_os5 = require("os");
2236
+ var import_node_path6 = require("path");
999
2237
 
1000
2238
  // src/errors.ts
1001
2239
  function sanitizeVendorName(message) {
@@ -1132,16 +2370,16 @@ function reportTitle(full) {
1132
2370
  return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
1133
2371
  }
1134
2372
  function outputBaseDir2() {
1135
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
2373
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
1136
2374
  }
1137
2375
  function saveFullReport(full) {
1138
2376
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
1139
2377
  const outDir = outputBaseDir2();
1140
2378
  try {
1141
- (0, import_node_fs2.mkdirSync)(outDir, { recursive: true });
2379
+ (0, import_node_fs4.mkdirSync)(outDir, { recursive: true });
1142
2380
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1143
- const file = (0, import_node_path3.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
1144
- (0, import_node_fs2.writeFileSync)(file, full, "utf8");
2381
+ const file = (0, import_node_path6.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
2382
+ (0, import_node_fs4.writeFileSync)(file, full, "utf8");
1145
2383
  return file;
1146
2384
  } catch {
1147
2385
  return null;
@@ -1150,12 +2388,12 @@ function saveFullReport(full) {
1150
2388
  function persistScreenshotLocally(base64, url) {
1151
2389
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
1152
2390
  try {
1153
- const dir = (0, import_node_path3.join)(outputBaseDir2(), "screenshots");
1154
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
2391
+ const dir = (0, import_node_path6.join)(outputBaseDir2(), "screenshots");
2392
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
1155
2393
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1156
2394
  const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1157
- const filePath = (0, import_node_path3.join)(dir, `${stamp}-${slug}.png`);
1158
- (0, import_node_fs2.writeFileSync)(filePath, Buffer.from(base64, "base64"));
2395
+ const filePath = (0, import_node_path6.join)(dir, `${stamp}-${slug}.png`);
2396
+ (0, import_node_fs4.writeFileSync)(filePath, Buffer.from(base64, "base64"));
1159
2397
  return filePath;
1160
2398
  } catch {
1161
2399
  return null;
@@ -1462,7 +2700,7 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
1462
2700
  const full = `# URL Extract: ${url}
1463
2701
  **${title}**
1464
2702
  ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
1465
- const textResult2 = oneBlock(full);
2703
+ const textResult = oneBlock(full);
1466
2704
  const structuredContent = {
1467
2705
  url,
1468
2706
  title: d.title ?? null,
@@ -1477,13 +2715,13 @@ ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSectio
1477
2715
  if (screenshotMeta?.base64) {
1478
2716
  return {
1479
2717
  content: [
1480
- ...textResult2.content,
2718
+ ...textResult.content,
1481
2719
  { type: "image", data: screenshotMeta.base64, mimeType: "image/png" }
1482
2720
  ],
1483
2721
  structuredContent
1484
2722
  };
1485
2723
  }
1486
- return { ...textResult2, structuredContent };
2724
+ return { ...textResult, structuredContent };
1487
2725
  }
1488
2726
  function formatMapSiteUrls(raw, input) {
1489
2727
  const parsed = parseData(raw);
@@ -1612,13 +2850,25 @@ ${videoRows}`,
1612
2850
  }
1613
2851
  };
1614
2852
  }
2853
+ function structuredTranscriptChunks(chunks) {
2854
+ return chunks.map((c) => ({
2855
+ startSec: Number.isFinite(c.timestamp?.[0]) ? c.timestamp[0] : 0,
2856
+ endSec: Number.isFinite(c.timestamp?.[1]) ? c.timestamp[1] : 0,
2857
+ text: c.text
2858
+ }));
2859
+ }
2860
+ function wordCount(text) {
2861
+ return text.trim() ? text.trim().split(/\s+/).length : 0;
2862
+ }
1615
2863
  function formatYoutubeTranscribe(raw, input) {
1616
2864
  const parsed = parseData(raw);
1617
2865
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1618
2866
  const d = parsed.data;
1619
2867
  const text = d.text ?? "";
1620
2868
  const chunks = d.chunks ?? [];
2869
+ const videoId = d.videoId ?? input.videoId ?? null;
1621
2870
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
2871
+ const words = wordCount(text);
1622
2872
  const chunkRows = chunks.slice(0, 50).map((c) => {
1623
2873
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
1624
2874
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -1626,8 +2876,8 @@ function formatYoutubeTranscribe(raw, input) {
1626
2876
  return `| ${mm}:${ss} | ${cell(truncate(c.text, 120))} |`;
1627
2877
  }).join("\n");
1628
2878
  const full = [
1629
- `# YouTube Transcript: \`${input.videoId}\``,
1630
- `**Duration:** ${durSec}s \xB7 **${text.split(" ").length} words**`,
2879
+ `# YouTube Transcript: \`${videoId ?? input.url ?? "video"}\``,
2880
+ `**Duration:** ${durSec}s \xB7 **${words} words**`,
1631
2881
  `
1632
2882
  ## Full Transcript
1633
2883
  ${text}`,
@@ -1640,7 +2890,22 @@ ${chunkRows}` : "",
1640
2890
  ---
1641
2891
  \u{1F4A1} Harvest more from this channel: use \`youtube_harvest\` with \`mode: "channel"\``
1642
2892
  ].filter(Boolean).join("\n");
1643
- return oneBlock(full);
2893
+ return {
2894
+ ...oneBlock(full),
2895
+ structuredContent: {
2896
+ videoId,
2897
+ url: videoId ? `https://www.youtube.com/watch?v=${videoId}` : input.url ?? null,
2898
+ wordCount: words,
2899
+ chunkCount: chunks.length,
2900
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
2901
+ transcriptText: text,
2902
+ chunks: structuredTranscriptChunks(chunks),
2903
+ resolvedInputs: {
2904
+ videoId,
2905
+ url: input.url ?? null
2906
+ }
2907
+ }
2908
+ };
1644
2909
  }
1645
2910
  function formatFacebookPageIntel(raw, input) {
1646
2911
  const parsed = parseData(raw);
@@ -1650,12 +2915,13 @@ function formatFacebookPageIntel(raw, input) {
1650
2915
  const ads = d.ads ?? [];
1651
2916
  const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
1652
2917
  const adBlocks = ads.map((ad, i) => [
1653
- `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? "\u2014"}`,
2918
+ `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
1654
2919
  ad.headline ? `**Headline:** ${ad.headline}` : "",
1655
2920
  ad.primaryText ? `**Copy:** ${truncate(ad.primaryText, 200)}` : "",
1656
2921
  ad.cta ? `**CTA:** ${ad.cta}` : "",
1657
- ad.videoUrl ? `**Video URL:** \`${ad.videoUrl}\`` : "",
1658
- ad.variations ? `**Variations:** ${ad.variations}` : ""
2922
+ ad.landingUrl ? `**Landing URL:** ${ad.landingUrl}` : "",
2923
+ ad.videoUrl ?? ad.videoSrc ? `**Video URL:** \`${ad.videoUrl ?? ad.videoSrc}\`` : "",
2924
+ ad.variations ?? ad.clusterCount ? `**Variations:** ${ad.variations ?? ad.clusterCount}` : ""
1659
2925
  ].filter(Boolean).join("\n")).join("\n\n---\n\n");
1660
2926
  const full = [
1661
2927
  `# Facebook Ad Intel: ${advertiser}`,
@@ -1681,11 +2947,16 @@ ${adBlocks}`,
1681
2947
  libraryId: ad.libraryId ?? null,
1682
2948
  status: ad.status ?? null,
1683
2949
  creativeType: ad.creativeType ?? null,
2950
+ primaryText: ad.primaryText ?? null,
1684
2951
  headline: ad.headline ?? null,
1685
2952
  cta: ad.cta ?? null,
1686
- startDate: ad.startDate ?? null,
1687
- videoUrl: ad.videoUrl ?? null,
1688
- variations: typeof ad.variations === "number" ? ad.variations : null
2953
+ startDate: ad.startDate ?? ad.started ?? null,
2954
+ landingUrl: ad.landingUrl ?? null,
2955
+ domain: ad.domain ?? null,
2956
+ videoUrl: ad.videoUrl ?? ad.videoSrc ?? null,
2957
+ imageUrl: ad.imageUrl ?? ad.imageSrc ?? null,
2958
+ videoPoster: ad.videoPoster ?? null,
2959
+ variations: typeof ad.variations === "number" ? ad.variations : typeof ad.clusterCount === "number" ? ad.clusterCount : null
1689
2960
  }))
1690
2961
  }
1691
2962
  };
@@ -1719,8 +2990,11 @@ ${rows}`,
1719
2990
  advertiserCount: advertisers.length,
1720
2991
  advertisers: advertisers.map((a) => ({
1721
2992
  name: a.pageName ?? a.name ?? null,
2993
+ pageId: a.pageId ?? null,
2994
+ pageUrl: a.pageUrl ?? null,
1722
2995
  adCount: typeof a.adCount === "number" ? a.adCount : null,
1723
- libraryId: a.sampleLibraryId ?? a.libraryId ?? null
2996
+ libraryId: a.sampleLibraryId ?? a.libraryId ?? null,
2997
+ sampleLibraryId: a.sampleLibraryId ?? null
1724
2998
  }))
1725
2999
  }
1726
3000
  };
@@ -1818,6 +3092,34 @@ function formatWorkflowSuggest(input) {
1818
3092
  }
1819
3093
  };
1820
3094
  }
3095
+ function workflowStepLines(data) {
3096
+ const step = data.step;
3097
+ const nextStep = data.nextStep;
3098
+ const done = data.done === true;
3099
+ const lines = [];
3100
+ if (step) {
3101
+ const index = Number(step.index ?? 0);
3102
+ const totalSteps = step.totalSteps != null ? Number(step.totalSteps) : void 0;
3103
+ const stepLabel = totalSteps ? `${index + 1}/${totalSteps}` : `${index + 1}`;
3104
+ lines.push(`
3105
+ ## Step ${stepLabel}: ${step.title ?? step.id ?? ""}`);
3106
+ const output = step.output;
3107
+ if (output && Object.keys(output).length) {
3108
+ lines.push(Object.entries(output).map(([k, v]) => `- ${k}: ${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join("\n"));
3109
+ }
3110
+ const warnings = Array.isArray(step.warnings) ? step.warnings : [];
3111
+ if (warnings.length) lines.push(`
3112
+ **Warnings:**
3113
+ ${warnings.map((w) => `- ${w}`).join("\n")}`);
3114
+ }
3115
+ if (done) {
3116
+ lines.push("\n**Done.** All steps complete.");
3117
+ } else if (nextStep && typeof nextStep === "object") {
3118
+ lines.push(`
3119
+ **Next step:** \`${nextStep.id ?? nextStep.index}\` \u2014 call \`workflow_step\` with this run id to continue.`);
3120
+ }
3121
+ return lines;
3122
+ }
1821
3123
  function formatWorkflowRun(raw, input) {
1822
3124
  const parsed = parseData(raw);
1823
3125
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -1831,6 +3133,7 @@ function formatWorkflowRun(raw, input) {
1831
3133
  `**Run ID:** \`${runId || "unknown"}\``,
1832
3134
  `**Status:** ${status}`,
1833
3135
  summary?.title ? `**Title:** ${summary.title}` : "",
3136
+ ...workflowStepLines(parsed.data),
1834
3137
  summary?.summary ? `
1835
3138
  ## Summary
1836
3139
  ${summary.summary}` : "",
@@ -1846,6 +3149,42 @@ ${workflowArtifactRows(artifacts)}` : "",
1846
3149
  input: input.input ?? {},
1847
3150
  run,
1848
3151
  summary,
3152
+ step: parsed.data.step,
3153
+ nextStep: parsed.data.nextStep ?? null,
3154
+ done: parsed.data.done === true,
3155
+ artifacts
3156
+ }
3157
+ };
3158
+ }
3159
+ function formatWorkflowStep(raw, input) {
3160
+ const parsed = parseData(raw);
3161
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
3162
+ const run = parsed.data.run;
3163
+ const summary = parsed.data.summary;
3164
+ const artifacts = workflowArtifactsFrom(run);
3165
+ const done = parsed.data.done === true;
3166
+ const full = [
3167
+ `# Workflow Step`,
3168
+ `**Run ID:** \`${input.runId}\``,
3169
+ `**Status:** ${run?.status ?? (done ? "done" : "running")}`,
3170
+ ...workflowStepLines(parsed.data),
3171
+ done && summary?.summary ? `
3172
+ ## Summary
3173
+ ${summary.summary}` : "",
3174
+ artifacts.length ? `
3175
+ ## Artifacts
3176
+ ${workflowArtifactRows(artifacts)}` : "",
3177
+ done && artifacts.length ? "\nUse `workflow_artifact_read` with the run id and artifact id to pull CSV, JSON, Markdown, or report content into context." : ""
3178
+ ].filter(Boolean).join("\n");
3179
+ return {
3180
+ ...oneBlock(full),
3181
+ structuredContent: {
3182
+ runId: input.runId,
3183
+ run,
3184
+ summary: summary ?? null,
3185
+ step: parsed.data.step,
3186
+ nextStep: parsed.data.nextStep ?? null,
3187
+ done,
1849
3188
  artifacts
1850
3189
  }
1851
3190
  };
@@ -2250,6 +3589,7 @@ function formatFacebookAdTranscribe(raw, input) {
2250
3589
  const text = d.text ?? "";
2251
3590
  const chunks = d.chunks ?? [];
2252
3591
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
3592
+ const words = wordCount(text);
2253
3593
  const chunkRows = chunks.slice(0, 50).map((c) => {
2254
3594
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
2255
3595
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -2258,7 +3598,7 @@ function formatFacebookAdTranscribe(raw, input) {
2258
3598
  }).join("\n");
2259
3599
  const full = [
2260
3600
  `# Facebook Ad Transcript`,
2261
- `**Duration:** ${durSec}s \xB7 **${text.split(" ").length} words**`,
3601
+ `**Duration:** ${durSec}s \xB7 **${words} words**`,
2262
3602
  `
2263
3603
  ## Full Transcript
2264
3604
  ${text}`,
@@ -2271,7 +3611,20 @@ ${chunkRows}` : "",
2271
3611
  ---
2272
3612
  \u{1F4A1} Get more ads from this advertiser: use \`facebook_page_intel\`. For public Facebook reel/post URLs, use \`facebook_video_transcribe\`.`
2273
3613
  ].filter(Boolean).join("\n");
2274
- return oneBlock(full);
3614
+ return {
3615
+ ...oneBlock(full),
3616
+ structuredContent: {
3617
+ videoUrl: input.videoUrl,
3618
+ wordCount: words,
3619
+ chunkCount: chunks.length,
3620
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
3621
+ transcriptText: text,
3622
+ chunks: structuredTranscriptChunks(chunks),
3623
+ resolvedInputs: {
3624
+ videoUrl: input.videoUrl
3625
+ }
3626
+ }
3627
+ };
2275
3628
  }
2276
3629
  function formatFacebookVideoTranscribe(raw, input) {
2277
3630
  const parsed = parseData(raw);
@@ -2279,7 +3632,7 @@ function formatFacebookVideoTranscribe(raw, input) {
2279
3632
  const d = parsed.data;
2280
3633
  const text = d.text ?? "";
2281
3634
  const chunks = d.chunks ?? [];
2282
- const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
3635
+ const wordCount2 = text.trim() ? text.trim().split(/\s+/).length : 0;
2283
3636
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
2284
3637
  const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
2285
3638
  const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
@@ -2292,7 +3645,7 @@ function formatFacebookVideoTranscribe(raw, input) {
2292
3645
  const full = [
2293
3646
  `# ${label} Transcript`,
2294
3647
  d.ownerName ? `**Owner:** ${d.ownerName}` : "",
2295
- `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount} words**`,
3648
+ `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
2296
3649
  d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
2297
3650
  d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
2298
3651
  `
@@ -2318,7 +3671,7 @@ ${chunkRows}` : "",
2318
3671
  bitrate: typeof d.bitrate === "number" ? d.bitrate : null,
2319
3672
  videoDurationSec: typeof d.videoDurationSec === "number" ? d.videoDurationSec : null,
2320
3673
  videoUrl: d.videoUrl ?? "",
2321
- wordCount,
3674
+ wordCount: wordCount2,
2322
3675
  chunkCount: chunks.length,
2323
3676
  transcriptText: text,
2324
3677
  chunks: chunks.map((c) => ({
@@ -2331,451 +3684,612 @@ ${chunkRows}` : "",
2331
3684
  }
2332
3685
 
2333
3686
  // src/mcp/mcp-tool-schemas.ts
3687
+ var import_zod3 = require("zod");
3688
+
3689
+ // src/schemas.ts
2334
3690
  var import_zod2 = require("zod");
3691
+ var DEFAULT_PROXY_MODE = "configured";
3692
+ var DEFAULT_MAPS_PROXY_MODE = "location";
3693
+ var HarvestOptionsSchema = import_zod2.z.object({
3694
+ query: import_zod2.z.string().min(1),
3695
+ location: import_zod2.z.string().optional(),
3696
+ gl: import_zod2.z.string().length(2).default("us"),
3697
+ hl: import_zod2.z.string().length(2).default("en"),
3698
+ device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop"),
3699
+ proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE),
3700
+ proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
3701
+ debug: import_zod2.z.boolean().default(false),
3702
+ depth: import_zod2.z.number().int().min(1).max(30).default(3),
3703
+ maxQuestions: import_zod2.z.number().int().min(1).max(1e3).default(100),
3704
+ headless: import_zod2.z.boolean().default(false),
3705
+ profileDir: import_zod2.z.string().optional(),
3706
+ proxy: import_zod2.z.string().url().optional(),
3707
+ kernelApiKey: import_zod2.z.string().optional(),
3708
+ kernelProxyId: import_zod2.z.string().optional(),
3709
+ kernelProxyResolution: import_zod2.z.unknown().optional(),
3710
+ outputDir: import_zod2.z.string().default("./paa-output"),
3711
+ format: import_zod2.z.enum(["json", "csv", "both"]).default("both"),
3712
+ serpOnly: import_zod2.z.boolean().default(false),
3713
+ pages: import_zod2.z.number().int().min(1).max(2).default(1)
3714
+ });
3715
+ var MapsPlaceOptionsSchema = import_zod2.z.object({
3716
+ businessName: import_zod2.z.string().min(1),
3717
+ location: import_zod2.z.string().min(1),
3718
+ gl: import_zod2.z.string().length(2).default("us"),
3719
+ hl: import_zod2.z.string().length(2).default("en"),
3720
+ includeReviews: import_zod2.z.boolean().default(false),
3721
+ maxReviews: import_zod2.z.number().int().min(1).max(500).default(50),
3722
+ kernelApiKey: import_zod2.z.string().optional(),
3723
+ kernelProxyId: import_zod2.z.string().optional(),
3724
+ headless: import_zod2.z.boolean().default(true)
3725
+ });
3726
+ var MapsSearchOptionsSchema = import_zod2.z.object({
3727
+ query: import_zod2.z.string().min(1),
3728
+ location: import_zod2.z.string().optional(),
3729
+ gl: import_zod2.z.string().length(2).default("us"),
3730
+ hl: import_zod2.z.string().length(2).default("en"),
3731
+ maxResults: import_zod2.z.number().int().min(1).max(50).default(10),
3732
+ proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
3733
+ proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
3734
+ debug: import_zod2.z.boolean().default(false),
3735
+ kernelApiKey: import_zod2.z.string().optional(),
3736
+ kernelProxyId: import_zod2.z.string().optional(),
3737
+ kernelProxyResolution: import_zod2.z.unknown().optional(),
3738
+ headless: import_zod2.z.boolean().default(true)
3739
+ });
3740
+ var RawPAAItemSchema = import_zod2.z.object({
3741
+ question: import_zod2.z.string().min(1),
3742
+ answer: import_zod2.z.string().optional(),
3743
+ sourceTitle: import_zod2.z.string().optional(),
3744
+ sourceSite: import_zod2.z.string().optional(),
3745
+ sourceCite: import_zod2.z.string().optional()
3746
+ });
3747
+ var RawMapsOverviewSchema = import_zod2.z.object({
3748
+ name: import_zod2.z.string().nullable(),
3749
+ rating: import_zod2.z.string().nullable(),
3750
+ reviewCount: import_zod2.z.string().nullable(),
3751
+ category: import_zod2.z.string().nullable(),
3752
+ address: import_zod2.z.string().nullable(),
3753
+ hoursSummary: import_zod2.z.string().nullable(),
3754
+ phone: import_zod2.z.string().nullable(),
3755
+ phoneDisplay: import_zod2.z.string().nullable(),
3756
+ website: import_zod2.z.string().nullable(),
3757
+ plusCode: import_zod2.z.string().nullable(),
3758
+ bookingUrl: import_zod2.z.string().nullable()
3759
+ });
3760
+ var RawMapsHoursRowSchema = import_zod2.z.object({
3761
+ day: import_zod2.z.string(),
3762
+ hours: import_zod2.z.string()
3763
+ });
3764
+ var RawMapsReviewStatsSchema = import_zod2.z.object({
3765
+ reviewHistogram: import_zod2.z.array(import_zod2.z.object({
3766
+ stars: import_zod2.z.number(),
3767
+ count: import_zod2.z.string()
3768
+ })),
3769
+ reviewTopics: import_zod2.z.array(import_zod2.z.object({
3770
+ label: import_zod2.z.string(),
3771
+ count: import_zod2.z.string()
3772
+ }))
3773
+ });
3774
+ var RawMapsReviewCardSchema = import_zod2.z.object({
3775
+ reviewId: import_zod2.z.string(),
3776
+ author: import_zod2.z.string().nullable(),
3777
+ stars: import_zod2.z.string().nullable(),
3778
+ date: import_zod2.z.string().nullable(),
3779
+ text: import_zod2.z.string().nullable(),
3780
+ ownerResponse: import_zod2.z.string().nullable()
3781
+ });
3782
+ var RawMapsAboutAttributeSchema = import_zod2.z.object({
3783
+ section: import_zod2.z.string(),
3784
+ attribute: import_zod2.z.string()
3785
+ });
3786
+
3787
+ // src/mcp/mcp-tool-schemas.ts
2335
3788
  var HarvestPaaInputSchema = {
2336
- query: import_zod2.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
2337
- location: import_zod2.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
2338
- maxQuestions: import_zod2.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
2339
- gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
2340
- hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
2341
- device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
2342
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
2343
- proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or when city-center targeting needs to be forced. With proxyMode location this ZIP is used for each fresh proxy attempt."),
2344
- debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
3789
+ query: import_zod3.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
3790
+ location: import_zod3.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
3791
+ maxQuestions: import_zod3.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
3792
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
3793
+ hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
3794
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
3795
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
3796
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
3797
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
2345
3798
  };
2346
3799
  var ExtractUrlInputSchema = {
2347
- url: import_zod2.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
2348
- screenshot: import_zod2.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
2349
- screenshotDevice: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
2350
- extractBranding: import_zod2.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
2351
- downloadMedia: import_zod2.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
2352
- mediaTypes: import_zod2.z.array(import_zod2.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
2353
- allowLocal: import_zod2.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
3800
+ url: import_zod3.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
3801
+ screenshot: import_zod3.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
3802
+ screenshotDevice: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
3803
+ extractBranding: import_zod3.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
3804
+ downloadMedia: import_zod3.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
3805
+ mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
3806
+ allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
2354
3807
  };
2355
3808
  var MapSiteUrlsInputSchema = {
2356
- url: import_zod2.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
2357
- maxUrls: import_zod2.z.number().int().min(1).max(500).optional().describe("Maximum URLs to discover. Use 100 for normal maps, higher when the user asks for a full inventory.")
3809
+ url: import_zod3.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
3810
+ maxUrls: import_zod3.z.number().int().min(1).max(500).optional().describe("Maximum URLs to discover. Use 100 for normal maps, higher when the user asks for a full inventory.")
2358
3811
  };
2359
3812
  var ExtractSiteInputSchema = {
2360
- url: import_zod2.z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
2361
- maxPages: import_zod2.z.number().int().min(1).max(50).optional().describe("Maximum pages to extract. Use 50 when the user asks for full results or a complete crawl within MCP limits.")
3813
+ url: import_zod3.z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
3814
+ maxPages: import_zod3.z.number().int().min(1).max(50).optional().describe("Maximum pages to extract. Use 50 when the user asks for full results or a complete crawl within MCP limits.")
2362
3815
  };
2363
3816
  var YoutubeHarvestInputSchema = {
2364
- mode: import_zod2.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
2365
- query: import_zod2.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
2366
- channelHandle: import_zod2.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
2367
- maxVideos: import_zod2.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50. Increase when user asks for full channel/history.")
3817
+ mode: import_zod3.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
3818
+ query: import_zod3.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
3819
+ channelHandle: import_zod3.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
3820
+ maxVideos: import_zod3.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50, maximum 500. Use 10-25 for quick topic discovery, 50 for normal channel/search harvests, and larger values only when the user asks for full channel/history because large responses consume more context.")
2368
3821
  };
2369
3822
  var YoutubeTranscribeInputSchema = {
2370
- videoId: import_zod2.z.string().min(1).describe("YouTube video ID, e.g. dQw4w9WgXcQ")
3823
+ videoId: import_zod3.z.string().min(1).optional().describe("YouTube video ID, e.g. dQw4w9WgXcQ. Use only an ID returned by youtube_harvest or visible in a YouTube URL; do not invent one."),
3824
+ url: import_zod3.z.string().url().optional().describe("Full YouTube URL, e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ or https://youtu.be/dQw4w9WgXcQ. Use this when the user pasted a URL instead of an ID. Provide videoId or url.")
2371
3825
  };
2372
3826
  var FacebookPageIntelInputSchema = {
2373
- pageId: import_zod2.z.string().optional(),
2374
- libraryId: import_zod2.z.string().optional(),
2375
- query: import_zod2.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
2376
- maxAds: import_zod2.z.number().int().min(1).max(200).default(50),
2377
- country: import_zod2.z.string().length(2).default("US")
3827
+ pageId: import_zod3.z.string().optional().describe("Facebook advertiser/page ID. Use only a pageId returned by facebook_ad_search/facebook_page_intel or copied from Facebook Ad Library; do not construct one yourself."),
3828
+ libraryId: import_zod3.z.string().optional().describe("Facebook Ad Library archive ID for a known ad or advertiser sample. Use a libraryId returned by facebook_ad_search, or a libraryId/adArchiveId visible in Ad Library."),
3829
+ query: import_zod3.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
3830
+ maxAds: import_zod3.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200. Prefer 25-50 for focused advertiser scans; use 100-200 only when the user asks for a broad ad archive sweep."),
3831
+ country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU. Infer from the user request when they name a country.")
2378
3832
  };
2379
3833
  var FacebookAdSearchInputSchema = {
2380
- query: import_zod2.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
2381
- country: import_zod2.z.string().length(2).default("US"),
2382
- maxResults: import_zod2.z.number().int().min(1).max(20).default(10)
3834
+ query: import_zod3.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
3835
+ country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
3836
+ maxResults: import_zod3.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20. Prefer tighter search terms over maxing this out.")
2383
3837
  };
2384
3838
  var FacebookAdTranscribeInputSchema = {
2385
- videoUrl: import_zod2.z.string().url().describe("Direct Facebook CDN video URL from a facebook_page_intel ad result. Do not pass a public Facebook reel/post/share URL here; use facebook_video_transcribe for organic Facebook URLs.")
3839
+ videoUrl: import_zod3.z.string().url().describe("Direct Facebook CDN video URL from a facebook_page_intel ad result. Do not pass a public Facebook reel/post/share URL here; use facebook_video_transcribe for organic Facebook URLs.")
2386
3840
  };
2387
3841
  var FacebookVideoTranscribeInputSchema = {
2388
- url: import_zod2.z.string().url().describe("Organic Facebook reel, video, watch, post, or share URL from facebook.com, m.facebook.com, or fb.watch. The tool renders the page, extracts the best matching public Facebook CDN MP4 URL, then transcribes it. Use this when the user pastes a normal Facebook video page URL and asks for the transcript or downloadable MP4."),
2389
- quality: import_zod2.z.enum(["best", "hd", "sd"]).default("best").describe("Preferred progressive MP4 quality. Use best by default; hd prefers the highest HD progressive URL; sd forces the SD URL.")
3842
+ url: import_zod3.z.string().url().describe("Organic Facebook reel, video, watch, post, or share URL from facebook.com, m.facebook.com, or fb.watch. The tool renders the page, extracts the best matching public Facebook CDN MP4 URL, then transcribes it. Use this when the user pastes a normal Facebook video page URL and asks for the transcript or downloadable MP4."),
3843
+ quality: import_zod3.z.enum(["best", "hd", "sd"]).default("best").describe("Preferred progressive MP4 quality. Use best by default; hd prefers the highest HD progressive URL; sd forces the SD URL.")
2390
3844
  };
2391
3845
  var MapsPlaceIntelInputSchema = {
2392
- businessName: import_zod2.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
2393
- location: import_zod2.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO". Infer from the user request when possible.'),
2394
- gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location."),
2395
- hl: import_zod2.z.string().length(2).default("en").describe("Language inferred from user request."),
2396
- includeReviews: import_zod2.z.boolean().default(false).describe("Whether to fetch individual review cards"),
2397
- maxReviews: import_zod2.z.number().int().min(1).max(500).default(50).describe("Max review cards to return (requires includeReviews: true)")
3846
+ businessName: import_zod3.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
3847
+ location: import_zod3.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO". Infer from the user request when possible.'),
3848
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
3849
+ hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
3850
+ includeReviews: import_zod3.z.boolean().default(false).describe("Whether to fetch individual review cards. Use true when the user asks for reviews, customer pain, complaints, praise, themes, or review evidence."),
3851
+ maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards to return when includeReviews is true. Default 50, maximum 500. Use 50 for normal review analysis and larger values only for deep review mining.")
2398
3852
  };
2399
3853
  var MapsSearchInputSchema = {
2400
- query: import_zod2.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
2401
- location: import_zod2.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
2402
- gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location."),
2403
- hl: import_zod2.z.string().length(2).default("en").describe("Language inferred from user request."),
2404
- maxResults: import_zod2.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more."),
2405
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state Maps searches; retryable failures create a new residential proxy ID and new browser session for up to 5 attempts. Use configured for the server proxy ID, and none only for local direct-network debugging."),
2406
- proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or city-center ZIP."),
2407
- debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
3854
+ query: import_zod3.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
3855
+ location: import_zod3.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
3856
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
3857
+ hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
3858
+ maxResults: import_zod3.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more."),
3859
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode. Maps defaults to location so local market searches get city/state residential proxy targeting and rotation. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
3860
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or city-center ZIP."),
3861
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
2408
3862
  };
2409
3863
  var DirectoryWorkflowInputSchema = {
2410
- query: import_zod2.z.string().min(1).describe("Business category, niche, or keyword to search on Google Maps for every selected market, e.g. roofers, dentists, med spas. Do not include the city here."),
2411
- state: import_zod2.z.string().min(2).default("TN").describe("US state abbreviation or state name used to select Census places, e.g. TN or Tennessee."),
2412
- minPopulation: import_zod2.z.number().int().min(0).default(1e5).describe('Minimum Census place population for market selection. Use 100000 for "cities above 100k population".'),
2413
- populationYear: import_zod2.z.number().int().min(2020).max(2025).default(2025).describe("Census population estimate year from the 2020-2025 Population Estimates Program city/place dataset."),
2414
- maxCities: import_zod2.z.number().int().min(1).max(100).default(25).describe("Maximum number of markets to process after sorting by population descending."),
2415
- maxResultsPerCity: import_zod2.z.number().int().min(1).max(50).default(50).describe("Google Maps business/profile candidates to collect for each city. Maximum 50."),
2416
- concurrency: import_zod2.z.number().int().min(1).max(5).default(5).describe("How many city Maps searches to run in parallel. Use 5 for broad directory batches unless debugging."),
2417
- includeZipGroups: import_zod2.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available. Set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath in local/test mode."),
2418
- usZipsCsvPath: import_zod2.z.string().optional().describe("Local/test-only path to a US ZIPS CSV with state_abbr, zipcode, county, city columns, such as Lead Magician tools/analytics/data/uszips.csv. Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead."),
2419
- saveCsv: import_zod2.z.boolean().default(true).describe("Save a directory-ready CSV to the MCP Scraper output directory and return its path. CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
2420
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode for every city Maps search. Use location by default for US city/state batches; retryable failures create a new residential proxy ID and new browser session for up to 5 attempts per city. Use configured for the server proxy ID, and none only for local direct-network debugging."),
2421
- proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its Lead Magician ZIP group or city/state location."),
2422
- debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
2423
- };
2424
- var RankTrackerModeSchema = import_zod2.z.enum(["maps", "organic", "ai_overview", "paa"]);
3864
+ query: import_zod3.z.string().min(1).describe("Business category, niche, or keyword to search on Google Maps for every selected market, e.g. roofers, dentists, med spas. Do not include the city here."),
3865
+ state: import_zod3.z.string().min(2).default("TN").describe("US state abbreviation or state name used to select Census places, e.g. TN or Tennessee."),
3866
+ minPopulation: import_zod3.z.number().int().min(0).default(1e5).describe('Minimum Census place population for market selection. Use 100000 for "cities above 100k population".'),
3867
+ populationYear: import_zod3.z.number().int().min(2020).max(2025).default(2025).describe("Census population estimate year from the 2020-2025 Population Estimates Program city/place dataset."),
3868
+ maxCities: import_zod3.z.number().int().min(1).max(100).default(25).describe("Maximum number of markets to process after sorting by population descending."),
3869
+ maxResultsPerCity: import_zod3.z.number().int().min(1).max(50).default(50).describe("Google Maps business/profile candidates to collect for each city. Maximum 50."),
3870
+ concurrency: import_zod3.z.number().int().min(1).max(5).default(5).describe("How many city Maps searches to run in parallel. Use 5 for broad directory batches unless debugging."),
3871
+ includeZipGroups: import_zod3.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available. Set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath in local/test mode."),
3872
+ usZipsCsvPath: import_zod3.z.string().optional().describe("Local/test-only path to a US ZIPS CSV with state_abbr, zipcode, county, city columns, such as Lead Magician tools/analytics/data/uszips.csv. Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead."),
3873
+ saveCsv: import_zod3.z.boolean().default(true).describe("Save a directory-ready CSV to the MCP Scraper output directory and return its path. CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
3874
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode for every city Maps search. Maps workflows default to location so each city can use city/state or ZIP-group residential proxy targeting. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
3875
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its ZIP group or city/state location."),
3876
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
3877
+ };
3878
+ var RankTrackerModeSchema = import_zod3.z.enum(["maps", "organic", "ai_overview", "paa"]);
2425
3879
  var RankTrackerBlueprintInputSchema = {
2426
- projectName: import_zod2.z.string().min(1).optional().describe("Optional name for the rank tracker project, client, or campaign."),
2427
- targetDomain: import_zod2.z.string().min(1).optional().describe("Primary domain to track in organic results, AI Overview citations, and PAA sources, e.g. example.com."),
2428
- targetBusinessName: import_zod2.z.string().min(1).optional().describe("Primary Google Business Profile or brand/business name to match in Maps results. Required for reliable Maps rank tracking."),
2429
- trackingModes: import_zod2.z.array(RankTrackerModeSchema).min(1).max(4).default(["maps", "organic", "ai_overview", "paa"]).describe("Rank tracker surfaces to build. maps uses directory_workflow/maps_search. organic uses search_serp. ai_overview uses search_serp/harvest_paa. paa uses harvest_paa source presence."),
2430
- keywords: import_zod2.z.array(import_zod2.z.string().min(1)).max(200).default([]).describe("Seed keywords or service queries to track. Leave empty when the downstream AI should create the keyword table from user input."),
2431
- locations: import_zod2.z.array(import_zod2.z.string().min(1)).max(100).default([]).describe("Markets, cities, ZIPs, or service areas to track. Use city/state strings like Denver, CO for localized SERP and Maps checks."),
2432
- competitors: import_zod2.z.array(import_zod2.z.string().min(1)).max(100).default([]).describe("Optional competitor domains or business names to persist as comparison targets."),
2433
- database: import_zod2.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family the downstream AI should target when generating migrations."),
2434
- scheduleCadence: import_zod2.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence for the generated cron/heartbeat plan."),
2435
- customCron: import_zod2.z.string().min(1).optional().describe("Cron expression to use when scheduleCadence is custom."),
2436
- timezone: import_zod2.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks, e.g. America/Denver."),
2437
- includeCron: import_zod2.z.boolean().default(true).describe("Include a cron or heartbeat worker plan. Keep true for production rank trackers."),
2438
- includeDashboard: import_zod2.z.boolean().default(true).describe("Include dashboard/reporting requirements in the generated prompt."),
2439
- includeAlerts: import_zod2.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature gains/losses."),
2440
- notes: import_zod2.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
2441
- };
2442
- var NullableString = import_zod2.z.string().nullable();
2443
- var MapsSearchAttemptOutput = import_zod2.z.object({
2444
- attemptNumber: import_zod2.z.number().int().min(1),
2445
- maxAttempts: import_zod2.z.number().int().min(1),
2446
- status: import_zod2.z.enum(["ok", "failed"]),
2447
- outcome: import_zod2.z.string(),
2448
- willRetry: import_zod2.z.boolean(),
2449
- durationMs: import_zod2.z.number().int().min(0),
2450
- resultCount: import_zod2.z.number().int().min(0),
2451
- error: NullableString,
2452
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]),
2453
- proxyResolutionSource: import_zod2.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
2454
- proxyIdSuffix: NullableString,
2455
- proxyTargetLevel: import_zod2.z.enum(["zip", "city", "state"]).nullable(),
2456
- proxyTargetLocation: NullableString,
2457
- proxyTargetZip: NullableString,
2458
- browserSessionIdSuffix: NullableString,
2459
- observedIp: NullableString,
2460
- observedCity: NullableString,
2461
- observedRegion: NullableString
3880
+ projectName: import_zod3.z.string().min(1).optional().describe("Optional name for the rank tracker project, client, or campaign."),
3881
+ targetDomain: import_zod3.z.string().min(1).optional().describe("Primary domain to track in organic results, AI Overview citations, and PAA sources, e.g. example.com."),
3882
+ targetBusinessName: import_zod3.z.string().min(1).optional().describe("Primary Google Business Profile or brand/business name to match in Maps results. Required for reliable Maps rank tracking."),
3883
+ trackingModes: import_zod3.z.array(RankTrackerModeSchema).min(1).max(4).default(["maps", "organic", "ai_overview", "paa"]).describe("Rank tracker surfaces to build. maps uses directory_workflow/maps_search. organic uses search_serp. ai_overview uses search_serp/harvest_paa. paa uses harvest_paa source presence."),
3884
+ keywords: import_zod3.z.array(import_zod3.z.string().min(1)).max(200).default([]).describe("Seed keywords or service queries to track. Leave empty when the downstream AI should create the keyword table from user input."),
3885
+ locations: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Markets, cities, ZIPs, or service areas to track. Use city/state strings like Denver, CO for localized SERP and Maps checks."),
3886
+ competitors: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Optional competitor domains or business names to persist as comparison targets."),
3887
+ database: import_zod3.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family the downstream AI should target when generating migrations."),
3888
+ scheduleCadence: import_zod3.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence for the generated cron/heartbeat plan."),
3889
+ customCron: import_zod3.z.string().min(1).optional().describe("Cron expression to use when scheduleCadence is custom."),
3890
+ timezone: import_zod3.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks, e.g. America/Denver."),
3891
+ includeCron: import_zod3.z.boolean().default(true).describe("Include a cron or heartbeat worker plan. Keep true for production rank trackers."),
3892
+ includeDashboard: import_zod3.z.boolean().default(true).describe("Include dashboard/reporting requirements in the generated prompt."),
3893
+ includeAlerts: import_zod3.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature gains/losses."),
3894
+ notes: import_zod3.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
3895
+ };
3896
+ var NullableString2 = import_zod3.z.string().nullable();
3897
+ var MapsSearchAttemptOutput = import_zod3.z.object({
3898
+ attemptNumber: import_zod3.z.number().int().min(1),
3899
+ maxAttempts: import_zod3.z.number().int().min(1),
3900
+ status: import_zod3.z.enum(["ok", "failed"]),
3901
+ outcome: import_zod3.z.string(),
3902
+ willRetry: import_zod3.z.boolean(),
3903
+ durationMs: import_zod3.z.number().int().min(0),
3904
+ resultCount: import_zod3.z.number().int().min(0),
3905
+ error: NullableString2,
3906
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]),
3907
+ proxyResolutionSource: import_zod3.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
3908
+ proxyIdSuffix: NullableString2,
3909
+ proxyTargetLevel: import_zod3.z.enum(["zip", "city", "state"]).nullable(),
3910
+ proxyTargetLocation: NullableString2,
3911
+ proxyTargetZip: NullableString2,
3912
+ browserSessionIdSuffix: NullableString2,
3913
+ observedIp: NullableString2,
3914
+ observedCity: NullableString2,
3915
+ observedRegion: NullableString2
2462
3916
  });
2463
3917
  var MapsSearchOutputSchema = {
2464
- query: import_zod2.z.string(),
2465
- location: import_zod2.z.string().nullable(),
2466
- searchQuery: import_zod2.z.string(),
2467
- searchUrl: import_zod2.z.string().url(),
2468
- extractedAt: import_zod2.z.string(),
2469
- requestedMaxResults: import_zod2.z.number().int().min(1).max(50),
2470
- resultCount: import_zod2.z.number().int().min(0).max(50),
2471
- results: import_zod2.z.array(import_zod2.z.object({
2472
- position: import_zod2.z.number().int().min(1),
2473
- name: import_zod2.z.string(),
2474
- placeUrl: import_zod2.z.string().url(),
2475
- cid: NullableString,
2476
- cidDecimal: NullableString,
2477
- rating: NullableString,
2478
- reviewCount: NullableString,
2479
- category: NullableString,
2480
- address: NullableString,
2481
- phone: NullableString,
2482
- hoursStatus: NullableString,
2483
- websiteUrl: NullableString,
2484
- directionsUrl: NullableString,
2485
- metadata: import_zod2.z.array(import_zod2.z.string())
3918
+ query: import_zod3.z.string(),
3919
+ location: import_zod3.z.string().nullable(),
3920
+ searchQuery: import_zod3.z.string(),
3921
+ searchUrl: import_zod3.z.string().url(),
3922
+ extractedAt: import_zod3.z.string(),
3923
+ requestedMaxResults: import_zod3.z.number().int().min(1).max(50),
3924
+ resultCount: import_zod3.z.number().int().min(0).max(50),
3925
+ results: import_zod3.z.array(import_zod3.z.object({
3926
+ position: import_zod3.z.number().int().min(1),
3927
+ name: import_zod3.z.string(),
3928
+ placeUrl: import_zod3.z.string().url(),
3929
+ cid: NullableString2,
3930
+ cidDecimal: NullableString2,
3931
+ rating: NullableString2,
3932
+ reviewCount: NullableString2,
3933
+ category: NullableString2,
3934
+ address: NullableString2,
3935
+ phone: NullableString2,
3936
+ hoursStatus: NullableString2,
3937
+ websiteUrl: NullableString2,
3938
+ directionsUrl: NullableString2,
3939
+ metadata: import_zod3.z.array(import_zod3.z.string())
2486
3940
  })),
2487
- attempts: import_zod2.z.array(MapsSearchAttemptOutput),
2488
- durationMs: import_zod2.z.number().int().min(0)
2489
- };
2490
- var DirectoryMapsBusinessOutput = import_zod2.z.object({
2491
- position: import_zod2.z.number().int().min(1),
2492
- name: import_zod2.z.string(),
2493
- placeUrl: import_zod2.z.string().url(),
2494
- cid: NullableString,
2495
- cidDecimal: NullableString,
2496
- rating: NullableString,
2497
- reviewCount: NullableString,
2498
- category: NullableString,
2499
- address: NullableString,
2500
- phone: NullableString,
2501
- hoursStatus: NullableString,
2502
- websiteUrl: NullableString,
2503
- directionsUrl: NullableString,
2504
- metadata: import_zod2.z.array(import_zod2.z.string())
3941
+ attempts: import_zod3.z.array(MapsSearchAttemptOutput),
3942
+ durationMs: import_zod3.z.number().int().min(0)
3943
+ };
3944
+ var DirectoryMapsBusinessOutput = import_zod3.z.object({
3945
+ position: import_zod3.z.number().int().min(1),
3946
+ name: import_zod3.z.string(),
3947
+ placeUrl: import_zod3.z.string().url(),
3948
+ cid: NullableString2,
3949
+ cidDecimal: NullableString2,
3950
+ rating: NullableString2,
3951
+ reviewCount: NullableString2,
3952
+ category: NullableString2,
3953
+ address: NullableString2,
3954
+ phone: NullableString2,
3955
+ hoursStatus: NullableString2,
3956
+ websiteUrl: NullableString2,
3957
+ directionsUrl: NullableString2,
3958
+ metadata: import_zod3.z.array(import_zod3.z.string())
2505
3959
  });
2506
3960
  var DirectoryWorkflowOutputSchema = {
2507
- query: import_zod2.z.string(),
2508
- state: import_zod2.z.string(),
2509
- minPopulation: import_zod2.z.number().int().min(0),
2510
- populationYear: import_zod2.z.number().int().min(2020).max(2025),
2511
- maxResultsPerCity: import_zod2.z.number().int().min(1).max(50),
2512
- concurrency: import_zod2.z.number().int().min(1).max(5),
2513
- censusSourceUrl: import_zod2.z.string().url(),
2514
- usZipsSourcePath: NullableString,
2515
- warnings: import_zod2.z.array(import_zod2.z.string()),
2516
- extractedAt: import_zod2.z.string(),
2517
- selectedCityCount: import_zod2.z.number().int().min(0),
2518
- totalResultCount: import_zod2.z.number().int().min(0),
2519
- csvPath: NullableString,
2520
- cities: import_zod2.z.array(import_zod2.z.object({
2521
- city: import_zod2.z.string(),
2522
- state: import_zod2.z.string(),
2523
- location: import_zod2.z.string(),
2524
- cityKey: import_zod2.z.string(),
2525
- censusName: import_zod2.z.string(),
2526
- population: import_zod2.z.number().int().min(0),
2527
- populationYear: import_zod2.z.number().int().min(2020).max(2025),
2528
- zips: import_zod2.z.array(import_zod2.z.string()),
2529
- counties: import_zod2.z.array(import_zod2.z.string()),
2530
- status: import_zod2.z.enum(["ok", "empty", "failed"]),
2531
- error: NullableString,
2532
- resultCount: import_zod2.z.number().int().min(0),
2533
- durationMs: import_zod2.z.number().int().min(0),
2534
- attempts: import_zod2.z.array(MapsSearchAttemptOutput),
2535
- results: import_zod2.z.array(DirectoryMapsBusinessOutput)
3961
+ query: import_zod3.z.string(),
3962
+ state: import_zod3.z.string(),
3963
+ minPopulation: import_zod3.z.number().int().min(0),
3964
+ populationYear: import_zod3.z.number().int().min(2020).max(2025),
3965
+ maxResultsPerCity: import_zod3.z.number().int().min(1).max(50),
3966
+ concurrency: import_zod3.z.number().int().min(1).max(5),
3967
+ censusSourceUrl: import_zod3.z.string().url(),
3968
+ usZipsSourcePath: NullableString2,
3969
+ warnings: import_zod3.z.array(import_zod3.z.string()),
3970
+ extractedAt: import_zod3.z.string(),
3971
+ selectedCityCount: import_zod3.z.number().int().min(0),
3972
+ totalResultCount: import_zod3.z.number().int().min(0),
3973
+ csvPath: NullableString2,
3974
+ cities: import_zod3.z.array(import_zod3.z.object({
3975
+ city: import_zod3.z.string(),
3976
+ state: import_zod3.z.string(),
3977
+ location: import_zod3.z.string(),
3978
+ cityKey: import_zod3.z.string(),
3979
+ censusName: import_zod3.z.string(),
3980
+ population: import_zod3.z.number().int().min(0),
3981
+ populationYear: import_zod3.z.number().int().min(2020).max(2025),
3982
+ zips: import_zod3.z.array(import_zod3.z.string()),
3983
+ counties: import_zod3.z.array(import_zod3.z.string()),
3984
+ status: import_zod3.z.enum(["ok", "empty", "failed"]),
3985
+ error: NullableString2,
3986
+ resultCount: import_zod3.z.number().int().min(0),
3987
+ durationMs: import_zod3.z.number().int().min(0),
3988
+ attempts: import_zod3.z.array(MapsSearchAttemptOutput),
3989
+ results: import_zod3.z.array(DirectoryMapsBusinessOutput)
2536
3990
  })),
2537
- durationMs: import_zod2.z.number().int().min(0)
3991
+ durationMs: import_zod3.z.number().int().min(0)
2538
3992
  };
2539
- var RankTrackerToolPlanOutput = import_zod2.z.object({
2540
- tool: import_zod2.z.string(),
2541
- purpose: import_zod2.z.string()
3993
+ var RankTrackerToolPlanOutput = import_zod3.z.object({
3994
+ tool: import_zod3.z.string(),
3995
+ purpose: import_zod3.z.string()
2542
3996
  });
2543
- var RankTrackerTableOutput = import_zod2.z.object({
2544
- name: import_zod2.z.string(),
2545
- purpose: import_zod2.z.string(),
2546
- keyColumns: import_zod2.z.array(import_zod2.z.string())
3997
+ var RankTrackerTableOutput = import_zod3.z.object({
3998
+ name: import_zod3.z.string(),
3999
+ purpose: import_zod3.z.string(),
4000
+ keyColumns: import_zod3.z.array(import_zod3.z.string())
2547
4001
  });
2548
- var RankTrackerCronJobOutput = import_zod2.z.object({
2549
- name: import_zod2.z.string(),
2550
- purpose: import_zod2.z.string(),
2551
- modes: import_zod2.z.array(RankTrackerModeSchema),
2552
- recommendedTools: import_zod2.z.array(import_zod2.z.string())
4002
+ var RankTrackerCronJobOutput = import_zod3.z.object({
4003
+ name: import_zod3.z.string(),
4004
+ purpose: import_zod3.z.string(),
4005
+ modes: import_zod3.z.array(RankTrackerModeSchema),
4006
+ recommendedTools: import_zod3.z.array(import_zod3.z.string())
2553
4007
  });
2554
4008
  var RankTrackerBlueprintOutputSchema = {
2555
- projectName: import_zod2.z.string(),
2556
- targetDomain: NullableString,
2557
- targetBusinessName: NullableString,
2558
- trackingModes: import_zod2.z.array(RankTrackerModeSchema),
2559
- database: import_zod2.z.string(),
2560
- recommendedTools: import_zod2.z.array(RankTrackerToolPlanOutput),
2561
- tables: import_zod2.z.array(RankTrackerTableOutput),
2562
- cron: import_zod2.z.object({
2563
- enabled: import_zod2.z.boolean(),
2564
- cadence: import_zod2.z.string(),
2565
- expression: import_zod2.z.string(),
2566
- timezone: import_zod2.z.string(),
2567
- jobs: import_zod2.z.array(RankTrackerCronJobOutput)
4009
+ projectName: import_zod3.z.string(),
4010
+ targetDomain: NullableString2,
4011
+ targetBusinessName: NullableString2,
4012
+ trackingModes: import_zod3.z.array(RankTrackerModeSchema),
4013
+ database: import_zod3.z.string(),
4014
+ recommendedTools: import_zod3.z.array(RankTrackerToolPlanOutput),
4015
+ tables: import_zod3.z.array(RankTrackerTableOutput),
4016
+ cron: import_zod3.z.object({
4017
+ enabled: import_zod3.z.boolean(),
4018
+ cadence: import_zod3.z.string(),
4019
+ expression: import_zod3.z.string(),
4020
+ timezone: import_zod3.z.string(),
4021
+ jobs: import_zod3.z.array(RankTrackerCronJobOutput)
2568
4022
  }),
2569
- metrics: import_zod2.z.array(import_zod2.z.string()),
2570
- implementationPrompt: import_zod2.z.string()
2571
- };
2572
- var OrganicResultOutput = import_zod2.z.object({
2573
- position: import_zod2.z.number().int(),
2574
- title: import_zod2.z.string(),
2575
- url: import_zod2.z.string(),
2576
- domain: import_zod2.z.string(),
2577
- snippet: NullableString
4023
+ metrics: import_zod3.z.array(import_zod3.z.string()),
4024
+ implementationPrompt: import_zod3.z.string()
4025
+ };
4026
+ var OrganicResultOutput = import_zod3.z.object({
4027
+ position: import_zod3.z.number().int(),
4028
+ title: import_zod3.z.string(),
4029
+ url: import_zod3.z.string(),
4030
+ domain: import_zod3.z.string(),
4031
+ snippet: NullableString2
2578
4032
  });
2579
- var AiOverviewOutput = import_zod2.z.object({
2580
- detected: import_zod2.z.boolean(),
2581
- text: NullableString
4033
+ var AiOverviewOutput = import_zod3.z.object({
4034
+ detected: import_zod3.z.boolean(),
4035
+ text: NullableString2
2582
4036
  }).nullable();
2583
- var EntityIdsOutput = import_zod2.z.object({
2584
- kgIds: import_zod2.z.array(import_zod2.z.string()),
2585
- cids: import_zod2.z.array(import_zod2.z.string()),
2586
- gcids: import_zod2.z.array(import_zod2.z.string())
4037
+ var EntityIdsOutput = import_zod3.z.object({
4038
+ kgIds: import_zod3.z.array(import_zod3.z.string()),
4039
+ cids: import_zod3.z.array(import_zod3.z.string()),
4040
+ gcids: import_zod3.z.array(import_zod3.z.string())
2587
4041
  }).nullable();
2588
4042
  var HarvestPaaOutputSchema = {
2589
- query: import_zod2.z.string(),
2590
- location: NullableString,
2591
- questionCount: import_zod2.z.number().int().min(0),
2592
- completionStatus: NullableString,
2593
- questions: import_zod2.z.array(import_zod2.z.object({
2594
- question: import_zod2.z.string(),
2595
- answer: NullableString,
2596
- sourceTitle: NullableString,
2597
- sourceSite: NullableString
4043
+ query: import_zod3.z.string(),
4044
+ location: NullableString2,
4045
+ questionCount: import_zod3.z.number().int().min(0),
4046
+ completionStatus: NullableString2,
4047
+ questions: import_zod3.z.array(import_zod3.z.object({
4048
+ question: import_zod3.z.string(),
4049
+ answer: NullableString2,
4050
+ sourceTitle: NullableString2,
4051
+ sourceSite: NullableString2
2598
4052
  })),
2599
- organicResults: import_zod2.z.array(OrganicResultOutput),
4053
+ organicResults: import_zod3.z.array(OrganicResultOutput),
2600
4054
  aiOverview: AiOverviewOutput,
2601
4055
  entityIds: EntityIdsOutput,
2602
- durationMs: import_zod2.z.number().min(0).nullable()
4056
+ durationMs: import_zod3.z.number().min(0).nullable()
2603
4057
  };
2604
4058
  var SearchSerpOutputSchema = {
2605
- query: import_zod2.z.string(),
2606
- location: NullableString,
2607
- organicResults: import_zod2.z.array(OrganicResultOutput),
2608
- localPack: import_zod2.z.array(import_zod2.z.object({
2609
- position: import_zod2.z.number().int(),
2610
- name: import_zod2.z.string(),
2611
- rating: NullableString,
2612
- reviewCount: NullableString,
2613
- websiteUrl: NullableString
4059
+ query: import_zod3.z.string(),
4060
+ location: NullableString2,
4061
+ organicResults: import_zod3.z.array(OrganicResultOutput),
4062
+ localPack: import_zod3.z.array(import_zod3.z.object({
4063
+ position: import_zod3.z.number().int(),
4064
+ name: import_zod3.z.string(),
4065
+ rating: NullableString2,
4066
+ reviewCount: NullableString2,
4067
+ websiteUrl: NullableString2
2614
4068
  })),
2615
4069
  aiOverview: AiOverviewOutput,
2616
4070
  entityIds: EntityIdsOutput
2617
4071
  };
2618
4072
  var ExtractUrlOutputSchema = {
2619
- url: import_zod2.z.string(),
2620
- title: NullableString,
2621
- headings: import_zod2.z.array(import_zod2.z.object({
2622
- level: import_zod2.z.number().int(),
2623
- text: import_zod2.z.string()
4073
+ url: import_zod3.z.string(),
4074
+ title: NullableString2,
4075
+ headings: import_zod3.z.array(import_zod3.z.object({
4076
+ level: import_zod3.z.number().int(),
4077
+ text: import_zod3.z.string()
2624
4078
  })),
2625
- schemaBlockCount: import_zod2.z.number().int().min(0),
2626
- entityName: NullableString,
2627
- entityTypes: import_zod2.z.array(import_zod2.z.string()),
2628
- napScore: import_zod2.z.number().nullable(),
2629
- missingSchemaFields: import_zod2.z.array(import_zod2.z.string()),
2630
- screenshotSaved: NullableString
4079
+ schemaBlockCount: import_zod3.z.number().int().min(0),
4080
+ entityName: NullableString2,
4081
+ entityTypes: import_zod3.z.array(import_zod3.z.string()),
4082
+ napScore: import_zod3.z.number().nullable(),
4083
+ missingSchemaFields: import_zod3.z.array(import_zod3.z.string()),
4084
+ screenshotSaved: NullableString2
2631
4085
  };
2632
4086
  var ExtractSiteOutputSchema = {
2633
- url: import_zod2.z.string(),
2634
- pageCount: import_zod2.z.number().int().min(0),
2635
- pages: import_zod2.z.array(import_zod2.z.object({
2636
- url: import_zod2.z.string(),
2637
- title: NullableString,
2638
- schemaTypes: import_zod2.z.array(import_zod2.z.string())
4087
+ url: import_zod3.z.string(),
4088
+ pageCount: import_zod3.z.number().int().min(0),
4089
+ pages: import_zod3.z.array(import_zod3.z.object({
4090
+ url: import_zod3.z.string(),
4091
+ title: NullableString2,
4092
+ schemaTypes: import_zod3.z.array(import_zod3.z.string())
2639
4093
  })),
2640
- durationMs: import_zod2.z.number().min(0)
4094
+ durationMs: import_zod3.z.number().min(0)
2641
4095
  };
2642
4096
  var MapsPlaceIntelOutputSchema = {
2643
- name: import_zod2.z.string(),
2644
- rating: NullableString,
2645
- reviewCount: NullableString,
2646
- category: NullableString,
2647
- address: NullableString,
2648
- phone: NullableString,
2649
- website: NullableString,
2650
- hoursSummary: NullableString,
2651
- bookingUrl: NullableString,
2652
- kgmid: NullableString,
2653
- cidDecimal: NullableString,
2654
- cidUrl: NullableString,
2655
- lat: import_zod2.z.number().nullable(),
2656
- lng: import_zod2.z.number().nullable(),
2657
- reviewsStatus: import_zod2.z.string(),
2658
- reviewsCollected: import_zod2.z.number().int().min(0),
2659
- reviewTopics: import_zod2.z.array(import_zod2.z.object({
2660
- label: import_zod2.z.string(),
2661
- count: import_zod2.z.string()
4097
+ name: import_zod3.z.string(),
4098
+ rating: NullableString2,
4099
+ reviewCount: NullableString2,
4100
+ category: NullableString2,
4101
+ address: NullableString2,
4102
+ phone: NullableString2,
4103
+ website: NullableString2,
4104
+ hoursSummary: NullableString2,
4105
+ bookingUrl: NullableString2,
4106
+ kgmid: NullableString2,
4107
+ cidDecimal: NullableString2,
4108
+ cidUrl: NullableString2,
4109
+ lat: import_zod3.z.number().nullable(),
4110
+ lng: import_zod3.z.number().nullable(),
4111
+ reviewsStatus: import_zod3.z.string(),
4112
+ reviewsCollected: import_zod3.z.number().int().min(0),
4113
+ reviewTopics: import_zod3.z.array(import_zod3.z.object({
4114
+ label: import_zod3.z.string(),
4115
+ count: import_zod3.z.string()
2662
4116
  }))
2663
4117
  };
2664
4118
  var CreditsInfoOutputSchema = {
2665
- balanceCredits: import_zod2.z.number().nullable(),
2666
- matchedCost: import_zod2.z.object({
2667
- label: import_zod2.z.string(),
2668
- credits: import_zod2.z.number(),
2669
- unit: import_zod2.z.string(),
2670
- notes: NullableString
4119
+ balanceCredits: import_zod3.z.number().nullable(),
4120
+ matchedCost: import_zod3.z.object({
4121
+ label: import_zod3.z.string(),
4122
+ credits: import_zod3.z.number(),
4123
+ unit: import_zod3.z.string(),
4124
+ notes: NullableString2
2671
4125
  }).nullable(),
2672
- costs: import_zod2.z.array(import_zod2.z.object({
2673
- key: import_zod2.z.string(),
2674
- label: import_zod2.z.string(),
2675
- credits: import_zod2.z.number(),
2676
- unit: import_zod2.z.string(),
2677
- notes: NullableString
4126
+ costs: import_zod3.z.array(import_zod3.z.object({
4127
+ key: import_zod3.z.string(),
4128
+ label: import_zod3.z.string(),
4129
+ credits: import_zod3.z.number(),
4130
+ unit: import_zod3.z.string(),
4131
+ notes: NullableString2
2678
4132
  })),
2679
- ledger: import_zod2.z.array(import_zod2.z.object({
2680
- createdAt: import_zod2.z.string(),
2681
- operation: import_zod2.z.string(),
2682
- credits: import_zod2.z.number(),
2683
- description: NullableString
4133
+ ledger: import_zod3.z.array(import_zod3.z.object({
4134
+ createdAt: import_zod3.z.string(),
4135
+ operation: import_zod3.z.string(),
4136
+ credits: import_zod3.z.number(),
4137
+ description: NullableString2
2684
4138
  })),
2685
- concurrency: import_zod2.z.object({
2686
- currentExtraSlots: import_zod2.z.number().int().min(0),
2687
- currentLimit: import_zod2.z.number().int().min(1),
2688
- hasSubscription: import_zod2.z.boolean(),
2689
- upgrade: import_zod2.z.object({
2690
- product: import_zod2.z.string(),
2691
- priceLabel: import_zod2.z.string(),
2692
- unitAmountUsd: import_zod2.z.number(),
2693
- currency: import_zod2.z.string(),
2694
- interval: import_zod2.z.string(),
2695
- billingUrl: import_zod2.z.string().url(),
2696
- terminalCommand: import_zod2.z.string(),
2697
- terminalCommandWithApiKeyEnv: import_zod2.z.string()
4139
+ concurrency: import_zod3.z.object({
4140
+ currentExtraSlots: import_zod3.z.number().int().min(0),
4141
+ currentLimit: import_zod3.z.number().int().min(1),
4142
+ hasSubscription: import_zod3.z.boolean(),
4143
+ upgrade: import_zod3.z.object({
4144
+ product: import_zod3.z.string(),
4145
+ priceLabel: import_zod3.z.string(),
4146
+ unitAmountUsd: import_zod3.z.number(),
4147
+ currency: import_zod3.z.string(),
4148
+ interval: import_zod3.z.string(),
4149
+ billingUrl: import_zod3.z.string().url(),
4150
+ terminalCommand: import_zod3.z.string(),
4151
+ terminalCommandWithApiKeyEnv: import_zod3.z.string()
2698
4152
  })
2699
4153
  }).nullable()
2700
4154
  };
2701
4155
  var MapSiteUrlsOutputSchema = {
2702
- startUrl: import_zod2.z.string(),
2703
- totalFound: import_zod2.z.number().int().min(0),
2704
- truncated: import_zod2.z.boolean(),
2705
- okCount: import_zod2.z.number().int().min(0),
2706
- redirectCount: import_zod2.z.number().int().min(0),
2707
- brokenCount: import_zod2.z.number().int().min(0),
2708
- urls: import_zod2.z.array(import_zod2.z.object({
2709
- url: import_zod2.z.string(),
2710
- status: import_zod2.z.number().int().nullable()
4156
+ startUrl: import_zod3.z.string(),
4157
+ totalFound: import_zod3.z.number().int().min(0),
4158
+ truncated: import_zod3.z.boolean(),
4159
+ okCount: import_zod3.z.number().int().min(0),
4160
+ redirectCount: import_zod3.z.number().int().min(0),
4161
+ brokenCount: import_zod3.z.number().int().min(0),
4162
+ urls: import_zod3.z.array(import_zod3.z.object({
4163
+ url: import_zod3.z.string(),
4164
+ status: import_zod3.z.number().int().nullable()
2711
4165
  })),
2712
- durationMs: import_zod2.z.number().min(0)
4166
+ durationMs: import_zod3.z.number().min(0)
2713
4167
  };
2714
4168
  var YoutubeHarvestOutputSchema = {
2715
- mode: import_zod2.z.string(),
2716
- videoCount: import_zod2.z.number().int().min(0),
2717
- channel: import_zod2.z.object({
2718
- title: NullableString,
2719
- subscriberCount: NullableString
4169
+ mode: import_zod3.z.string(),
4170
+ videoCount: import_zod3.z.number().int().min(0),
4171
+ channel: import_zod3.z.object({
4172
+ title: NullableString2,
4173
+ subscriberCount: NullableString2
2720
4174
  }).nullable(),
2721
- videos: import_zod2.z.array(import_zod2.z.object({
2722
- videoId: import_zod2.z.string(),
2723
- title: import_zod2.z.string(),
2724
- channelName: NullableString,
2725
- views: NullableString,
2726
- duration: NullableString,
2727
- url: NullableString
4175
+ videos: import_zod3.z.array(import_zod3.z.object({
4176
+ videoId: import_zod3.z.string(),
4177
+ title: import_zod3.z.string(),
4178
+ channelName: NullableString2,
4179
+ views: NullableString2,
4180
+ duration: NullableString2,
4181
+ url: NullableString2
2728
4182
  }))
2729
4183
  };
2730
4184
  var FacebookAdSearchOutputSchema = {
2731
- query: import_zod2.z.string(),
2732
- advertiserCount: import_zod2.z.number().int().min(0),
2733
- advertisers: import_zod2.z.array(import_zod2.z.object({
2734
- name: NullableString,
2735
- adCount: import_zod2.z.number().int().nullable(),
2736
- libraryId: NullableString
4185
+ query: import_zod3.z.string(),
4186
+ advertiserCount: import_zod3.z.number().int().min(0),
4187
+ advertisers: import_zod3.z.array(import_zod3.z.object({
4188
+ name: NullableString2,
4189
+ pageId: NullableString2,
4190
+ pageUrl: NullableString2,
4191
+ adCount: import_zod3.z.number().int().nullable(),
4192
+ libraryId: NullableString2,
4193
+ sampleLibraryId: NullableString2
2737
4194
  }))
2738
4195
  };
2739
4196
  var FacebookPageIntelOutputSchema = {
2740
- advertiserName: NullableString,
2741
- totalAds: import_zod2.z.number().int().min(0),
2742
- activeCount: import_zod2.z.number().int().min(0),
2743
- videoCount: import_zod2.z.number().int().min(0),
2744
- imageCount: import_zod2.z.number().int().min(0),
2745
- ads: import_zod2.z.array(import_zod2.z.object({
2746
- libraryId: NullableString,
2747
- status: NullableString,
2748
- creativeType: NullableString,
2749
- headline: NullableString,
2750
- cta: NullableString,
2751
- startDate: NullableString,
2752
- videoUrl: NullableString,
2753
- variations: import_zod2.z.number().int().nullable()
4197
+ advertiserName: NullableString2,
4198
+ totalAds: import_zod3.z.number().int().min(0),
4199
+ activeCount: import_zod3.z.number().int().min(0),
4200
+ videoCount: import_zod3.z.number().int().min(0),
4201
+ imageCount: import_zod3.z.number().int().min(0),
4202
+ ads: import_zod3.z.array(import_zod3.z.object({
4203
+ libraryId: NullableString2,
4204
+ status: NullableString2,
4205
+ creativeType: NullableString2,
4206
+ primaryText: NullableString2,
4207
+ headline: NullableString2,
4208
+ cta: NullableString2,
4209
+ startDate: NullableString2,
4210
+ landingUrl: NullableString2,
4211
+ domain: NullableString2,
4212
+ videoUrl: NullableString2,
4213
+ imageUrl: NullableString2,
4214
+ videoPoster: NullableString2,
4215
+ variations: import_zod3.z.number().int().nullable()
2754
4216
  }))
2755
4217
  };
2756
4218
  var FacebookVideoTranscribeOutputSchema = {
2757
- sourceUrl: import_zod2.z.string().url(),
2758
- pageUrl: import_zod2.z.string().url(),
2759
- videoId: NullableString,
2760
- ownerName: NullableString,
2761
- selectedQuality: import_zod2.z.string(),
2762
- bitrate: import_zod2.z.number().int().nullable(),
2763
- videoDurationSec: import_zod2.z.number().nullable(),
2764
- videoUrl: import_zod2.z.string().url(),
2765
- wordCount: import_zod2.z.number().int().min(0),
2766
- chunkCount: import_zod2.z.number().int().min(0),
2767
- transcriptText: import_zod2.z.string(),
2768
- chunks: import_zod2.z.array(import_zod2.z.object({
2769
- startSec: import_zod2.z.number(),
2770
- endSec: import_zod2.z.number(),
2771
- text: import_zod2.z.string()
4219
+ sourceUrl: import_zod3.z.string().url(),
4220
+ pageUrl: import_zod3.z.string().url(),
4221
+ videoId: NullableString2,
4222
+ ownerName: NullableString2,
4223
+ selectedQuality: import_zod3.z.string(),
4224
+ bitrate: import_zod3.z.number().int().nullable(),
4225
+ videoDurationSec: import_zod3.z.number().nullable(),
4226
+ videoUrl: import_zod3.z.string().url(),
4227
+ wordCount: import_zod3.z.number().int().min(0),
4228
+ chunkCount: import_zod3.z.number().int().min(0),
4229
+ transcriptText: import_zod3.z.string(),
4230
+ chunks: import_zod3.z.array(import_zod3.z.object({
4231
+ startSec: import_zod3.z.number(),
4232
+ endSec: import_zod3.z.number(),
4233
+ text: import_zod3.z.string()
2772
4234
  }))
2773
4235
  };
4236
+ var TranscriptChunkOutput = import_zod3.z.object({
4237
+ startSec: import_zod3.z.number(),
4238
+ endSec: import_zod3.z.number(),
4239
+ text: import_zod3.z.string()
4240
+ });
4241
+ var YoutubeTranscribeOutputSchema = {
4242
+ videoId: NullableString2,
4243
+ url: NullableString2,
4244
+ wordCount: import_zod3.z.number().int().min(0),
4245
+ chunkCount: import_zod3.z.number().int().min(0),
4246
+ durationMs: import_zod3.z.number().nullable(),
4247
+ transcriptText: import_zod3.z.string(),
4248
+ chunks: import_zod3.z.array(TranscriptChunkOutput),
4249
+ resolvedInputs: import_zod3.z.object({
4250
+ videoId: NullableString2,
4251
+ url: NullableString2
4252
+ })
4253
+ };
4254
+ var FacebookAdTranscribeOutputSchema = {
4255
+ videoUrl: import_zod3.z.string().url(),
4256
+ wordCount: import_zod3.z.number().int().min(0),
4257
+ chunkCount: import_zod3.z.number().int().min(0),
4258
+ durationMs: import_zod3.z.number().nullable(),
4259
+ transcriptText: import_zod3.z.string(),
4260
+ chunks: import_zod3.z.array(TranscriptChunkOutput),
4261
+ resolvedInputs: import_zod3.z.object({
4262
+ videoUrl: import_zod3.z.string().url()
4263
+ })
4264
+ };
4265
+ var CaptureSerpSnapshotOutputSchema = {
4266
+ schemaVersion: import_zod3.z.literal("serp-intelligence.capture.v1"),
4267
+ status: import_zod3.z.string(),
4268
+ query: NullableString2,
4269
+ location: NullableString2,
4270
+ capturedAt: NullableString2,
4271
+ resultCount: import_zod3.z.number().int().min(0).nullable(),
4272
+ snapshotId: NullableString2,
4273
+ resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
4274
+ artifacts: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
4275
+ diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
4276
+ providerPayload: import_zod3.z.record(import_zod3.z.unknown())
4277
+ };
4278
+ var CaptureSerpPageSnapshotsOutputSchema = {
4279
+ schemaVersion: import_zod3.z.literal("serp-intelligence.page-snapshots.v1"),
4280
+ status: import_zod3.z.string(),
4281
+ count: import_zod3.z.number().int().min(0),
4282
+ failedCount: import_zod3.z.number().int().min(0),
4283
+ captures: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
4284
+ resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
4285
+ diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
4286
+ providerPayload: import_zod3.z.record(import_zod3.z.unknown())
4287
+ };
2774
4288
  var CreditsInfoInputSchema = {
2775
- item: import_zod2.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", "YouTube transcription", or "concurrency"'),
2776
- includeLedger: import_zod2.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
4289
+ item: import_zod3.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", "YouTube transcription", or "concurrency"'),
4290
+ includeLedger: import_zod3.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
2777
4291
  };
2778
- var WorkflowIdSchema = import_zod2.z.enum([
4292
+ var WorkflowIdSchema = import_zod3.z.enum([
2779
4293
  "directory",
2780
4294
  "agent-packet",
2781
4295
  "local-competitive-audit",
@@ -2785,114 +4299,129 @@ var WorkflowIdSchema = import_zod2.z.enum([
2785
4299
  "ai-overview-language"
2786
4300
  ]);
2787
4301
  var WorkflowListInputSchema = {
2788
- includeRecipes: import_zod2.z.boolean().default(true).describe("Include high-level AI-facing recipes such as market analysis, ICP research, forum/review research, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.")
4302
+ includeRecipes: import_zod3.z.boolean().default(true).describe("Include high-level AI-facing recipes such as market analysis, ICP research, forum/review research, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.")
2789
4303
  };
2790
4304
  var WorkflowSuggestInputSchema = {
2791
- goal: import_zod2.z.string().min(1).describe('The user goal or job to route, e.g. "market analysis for roofers in Tennessee", "ICP research for med spas", "CRO audit for this URL", or "brand design briefing".'),
2792
- query: import_zod2.z.string().optional().describe("Business category, niche, or Maps query when known."),
2793
- keyword: import_zod2.z.string().optional().describe("Search keyword, audience problem, or content topic when known."),
2794
- domain: import_zod2.z.string().optional().describe("Target domain or brand domain when known."),
2795
- url: import_zod2.z.string().url().optional().describe("Target URL when the workflow should inspect a specific page."),
2796
- location: import_zod2.z.string().optional().describe("City/region/country for localized research, e.g. Denver, CO."),
2797
- state: import_zod2.z.string().optional().describe("US state abbreviation or name for state-wide market research."),
2798
- maxSuggestions: import_zod2.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
4305
+ goal: import_zod3.z.string().min(1).describe('The user goal or job to route, e.g. "market analysis for roofers in Tennessee", "ICP research for med spas", "CRO audit for this URL", or "brand design briefing".'),
4306
+ query: import_zod3.z.string().optional().describe("Business category, niche, or Maps query when known."),
4307
+ keyword: import_zod3.z.string().optional().describe("Search keyword, audience problem, or content topic when known."),
4308
+ domain: import_zod3.z.string().optional().describe("Target domain or brand domain when known."),
4309
+ url: import_zod3.z.string().url().optional().describe("Target URL when the workflow should inspect a specific page."),
4310
+ location: import_zod3.z.string().optional().describe("City/region/country for localized research, e.g. Denver, CO."),
4311
+ state: import_zod3.z.string().optional().describe("US state abbreviation or name for state-wide market research."),
4312
+ maxSuggestions: import_zod3.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
2799
4313
  };
2800
4314
  var WorkflowRunInputSchema = {
2801
- workflowId: WorkflowIdSchema.describe("Workflow to run. Use workflow_list or workflow_suggest first when unsure."),
2802
- input: import_zod2.z.record(import_zod2.z.unknown()).default({}).describe("Workflow-specific input object. Examples: agent-packet uses {keyword, domain?, location?, maxQuestions?}; local-competitive-audit uses {query, state, minPopulation?, maxCities?, maxResultsPerCity?, hydrateTop?, maxReviews?}; serp-comparison uses {keyword, domain?, url?, location?, extractTop?}."),
2803
- webhookUrl: import_zod2.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
4315
+ workflowId: WorkflowIdSchema.describe("Workflow to run: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. Use only these values; call workflow_list or workflow_suggest first when unsure."),
4316
+ input: import_zod3.z.record(import_zod3.z.unknown()).default({}).describe("Workflow-specific input object. Examples: agent-packet uses {keyword, domain?, location?, maxQuestions?}; local-competitive-audit uses {query, state, minPopulation?, maxCities?, maxResultsPerCity?, hydrateTop?, maxReviews?}; serp-comparison uses {keyword, domain?, url?, location?, extractTop?}."),
4317
+ webhookUrl: import_zod3.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
4318
+ };
4319
+ var WorkflowStepInputSchema = {
4320
+ runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself. Advances the run by exactly one step (one logical leg, e.g. one live harvest).")
2804
4321
  };
2805
4322
  var WorkflowStatusInputSchema = {
2806
- runId: import_zod2.z.string().min(1).describe("Workflow run id returned by workflow_run.")
4323
+ runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself.")
2807
4324
  };
2808
4325
  var WorkflowArtifactReadInputSchema = {
2809
- runId: import_zod2.z.string().min(1).describe("Workflow run id returned by workflow_run or workflow_status."),
2810
- artifactId: import_zod2.z.string().min(1).describe("Artifact id from the run artifact list."),
2811
- maxBytes: import_zod2.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline. Use lower values for large CSV/JSON artifacts; call again with the downloadUrl if needed outside MCP.")
2812
- };
2813
- var WorkflowRecipeOutput = import_zod2.z.object({
2814
- id: import_zod2.z.string(),
2815
- title: import_zod2.z.string(),
2816
- description: import_zod2.z.string(),
2817
- primaryWorkflowId: import_zod2.z.string().nullable(),
2818
- recommendedTools: import_zod2.z.array(import_zod2.z.string()),
2819
- requiredInputs: import_zod2.z.array(import_zod2.z.string()),
2820
- optionalInputs: import_zod2.z.array(import_zod2.z.string()),
2821
- produces: import_zod2.z.array(import_zod2.z.string()),
2822
- runHint: import_zod2.z.string()
4326
+ runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself."),
4327
+ artifactId: import_zod3.z.string().min(1).describe("Artifact id from the run artifact list returned by workflow_run, workflow_step, or workflow_status. Use only a returned artifactId; do not construct one yourself."),
4328
+ maxBytes: import_zod3.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline. Use lower values for large CSV/JSON artifacts; call again with the downloadUrl if needed outside MCP.")
4329
+ };
4330
+ var WorkflowRecipeOutput = import_zod3.z.object({
4331
+ id: import_zod3.z.string(),
4332
+ title: import_zod3.z.string(),
4333
+ description: import_zod3.z.string(),
4334
+ primaryWorkflowId: import_zod3.z.string().nullable(),
4335
+ recommendedTools: import_zod3.z.array(import_zod3.z.string()),
4336
+ requiredInputs: import_zod3.z.array(import_zod3.z.string()),
4337
+ optionalInputs: import_zod3.z.array(import_zod3.z.string()),
4338
+ produces: import_zod3.z.array(import_zod3.z.string()),
4339
+ runHint: import_zod3.z.string()
2823
4340
  });
2824
- var WorkflowDefinitionOutput = import_zod2.z.object({
2825
- id: import_zod2.z.string(),
2826
- title: import_zod2.z.string(),
2827
- description: import_zod2.z.string()
4341
+ var WorkflowDefinitionOutput = import_zod3.z.object({
4342
+ id: import_zod3.z.string(),
4343
+ title: import_zod3.z.string(),
4344
+ description: import_zod3.z.string()
2828
4345
  });
2829
- var WorkflowArtifactOutput = import_zod2.z.record(import_zod2.z.unknown());
4346
+ var WorkflowArtifactOutput = import_zod3.z.record(import_zod3.z.unknown());
2830
4347
  var WorkflowListOutputSchema = {
2831
- workflows: import_zod2.z.array(WorkflowDefinitionOutput),
2832
- recipes: import_zod2.z.array(WorkflowRecipeOutput)
4348
+ workflows: import_zod3.z.array(WorkflowDefinitionOutput),
4349
+ recipes: import_zod3.z.array(WorkflowRecipeOutput)
2833
4350
  };
2834
4351
  var WorkflowSuggestOutputSchema = {
2835
- goal: import_zod2.z.string(),
2836
- suggestions: import_zod2.z.array(WorkflowRecipeOutput)
4352
+ goal: import_zod3.z.string(),
4353
+ suggestions: import_zod3.z.array(WorkflowRecipeOutput)
2837
4354
  };
2838
4355
  var WorkflowRunOutputSchema = {
2839
- workflowId: import_zod2.z.string(),
2840
- input: import_zod2.z.record(import_zod2.z.unknown()),
2841
- run: import_zod2.z.record(import_zod2.z.unknown()).optional(),
2842
- summary: import_zod2.z.record(import_zod2.z.unknown()).optional(),
2843
- artifacts: import_zod2.z.array(WorkflowArtifactOutput)
4356
+ workflowId: import_zod3.z.string(),
4357
+ input: import_zod3.z.record(import_zod3.z.unknown()),
4358
+ run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
4359
+ summary: import_zod3.z.record(import_zod3.z.unknown()).optional(),
4360
+ step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
4361
+ nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
4362
+ done: import_zod3.z.boolean().optional(),
4363
+ artifacts: import_zod3.z.array(WorkflowArtifactOutput)
4364
+ };
4365
+ var WorkflowStepOutputSchema = {
4366
+ runId: import_zod3.z.string(),
4367
+ run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
4368
+ summary: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
4369
+ step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
4370
+ nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
4371
+ done: import_zod3.z.boolean(),
4372
+ artifacts: import_zod3.z.array(WorkflowArtifactOutput)
2844
4373
  };
2845
4374
  var WorkflowStatusOutputSchema = {
2846
- run: import_zod2.z.record(import_zod2.z.unknown()).optional(),
2847
- artifacts: import_zod2.z.array(WorkflowArtifactOutput)
4375
+ run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
4376
+ artifacts: import_zod3.z.array(WorkflowArtifactOutput)
2848
4377
  };
2849
4378
  var WorkflowArtifactReadOutputSchema = {
2850
- runId: import_zod2.z.string(),
2851
- artifactId: import_zod2.z.string(),
2852
- contentType: import_zod2.z.string(),
2853
- bytes: import_zod2.z.number().int().min(0),
2854
- truncated: import_zod2.z.boolean(),
2855
- text: import_zod2.z.string()
4379
+ runId: import_zod3.z.string(),
4380
+ artifactId: import_zod3.z.string(),
4381
+ contentType: import_zod3.z.string(),
4382
+ bytes: import_zod3.z.number().int().min(0),
4383
+ truncated: import_zod3.z.boolean(),
4384
+ text: import_zod3.z.string()
2856
4385
  };
2857
4386
  var SearchSerpInputSchema = {
2858
- query: import_zod2.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
2859
- location: import_zod2.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
2860
- gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
2861
- hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from user request."),
2862
- device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
2863
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
2864
- proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or when city-center targeting needs to be forced. With proxyMode location this ZIP is used for each fresh proxy attempt."),
2865
- debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
2866
- pages: import_zod2.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
4387
+ query: import_zod3.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
4388
+ location: import_zod3.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
4389
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
4390
+ hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from user request."),
4391
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
4392
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general SERP success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
4393
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
4394
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
4395
+ pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
2867
4396
  };
2868
4397
  var CaptureSerpSnapshotInputSchema = {
2869
- query: import_zod2.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
2870
- location: import_zod2.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
2871
- gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
2872
- hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from the user request."),
2873
- device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
2874
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy behavior for capture. Use location for localized US residential evidence; it creates a fresh proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static residential proxy, and none only for direct-network debugging."),
2875
- proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting when a precise city-center or ZIP proxy is needed. With proxyMode location this ZIP is used for each fresh proxy attempt."),
2876
- pages: import_zod2.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
2877
- debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
2878
- includePageSnapshots: import_zod2.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
2879
- pageSnapshotLimit: import_zod2.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
4398
+ query: import_zod3.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
4399
+ location: import_zod3.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
4400
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
4401
+ hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
4402
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
4403
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy behavior for capture. Default configured uses the service proxy without city/ZIP targeting for the highest general capture success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
4404
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when a precise city-center or ZIP proxy is needed."),
4405
+ pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
4406
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
4407
+ includePageSnapshots: import_zod3.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
4408
+ pageSnapshotLimit: import_zod3.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
2880
4409
  };
2881
4410
  var ScreenshotInputSchema = {
2882
- url: import_zod2.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
2883
- device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport profile. desktop = 1440\xD7900. mobile = 390\xD7844. Use desktop by default; use mobile when the user asks for a mobile view."),
2884
- allowLocal: import_zod2.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
4411
+ url: import_zod3.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
4412
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport profile. desktop = 1440\xD7900. mobile = 390\xD7844. Use desktop by default; use mobile when the user asks for a mobile view."),
4413
+ allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
2885
4414
  };
2886
4415
  var CaptureSerpPageSnapshotsInputSchema = {
2887
- urls: import_zod2.z.array(import_zod2.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
2888
- targets: import_zod2.z.array(import_zod2.z.object({
2889
- url: import_zod2.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
2890
- sourceKind: import_zod2.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
2891
- sourcePosition: import_zod2.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
4416
+ urls: import_zod3.z.array(import_zod3.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
4417
+ targets: import_zod3.z.array(import_zod3.z.object({
4418
+ url: import_zod3.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
4419
+ sourceKind: import_zod3.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
4420
+ sourcePosition: import_zod3.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
2892
4421
  }).strict()).min(1).max(25).optional().describe("Structured page snapshot targets. Use this instead of urls when source kind or position should be preserved."),
2893
- maxConcurrency: import_zod2.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
2894
- timeoutMs: import_zod2.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
2895
- debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
4422
+ maxConcurrency: import_zod3.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
4423
+ timeoutMs: import_zod3.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
4424
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
2896
4425
  };
2897
4426
 
2898
4427
  // src/mcp/rank-tracker-blueprint.ts
@@ -3240,7 +4769,7 @@ function localPlanningToolAnnotations(title) {
3240
4769
  function listSavedReports() {
3241
4770
  try {
3242
4771
  const dir = outputBaseDir2();
3243
- return (0, import_node_fs3.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs3.statSync)((0, import_node_path4.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
4772
+ return (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs5.statSync)((0, import_node_path7.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
3244
4773
  } catch {
3245
4774
  return [];
3246
4775
  }
@@ -3264,9 +4793,9 @@ function registerSavedReportResources(server2) {
3264
4793
  },
3265
4794
  async (uri, variables) => {
3266
4795
  const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
3267
- const filename = (0, import_node_path4.basename)(decodeURIComponent(String(requested ?? "")));
4796
+ const filename = (0, import_node_path7.basename)(decodeURIComponent(String(requested ?? "")));
3268
4797
  if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
3269
- const text = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(outputBaseDir2(), filename), "utf8");
4798
+ const text = (0, import_node_fs5.readFileSync)((0, import_node_path7.join)(outputBaseDir2(), filename), "utf8");
3270
4799
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
3271
4800
  }
3272
4801
  );
@@ -3278,70 +4807,72 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
3278
4807
  if (savesReports) registerSavedReportResources(server2);
3279
4808
  server2.registerTool("harvest_paa", {
3280
4809
  title: "Google PAA + SERP Harvest",
3281
- description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). For US local SERPs, leave proxyMode as location so the service uses fresh residential proxy IDs across retries, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear or the market evidence is wrong. Use maxQuestions 30 normally, 100-200 for "full", "deep", "all", or comprehensive research. Deep harvests above 100 questions can run for several minutes with no interim progress \u2014 warn the user before starting one and keep maxQuestions at or below 100 unless they explicitly want a deep harvest. Credits are charged by extracted question; unused request hold is refunded.'),
4810
+ description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting for the highest general success rate. Use proxyMode location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use maxQuestions 30 normally, 100-200 for "full", "deep", "all", or comprehensive research. Deep harvests above 100 questions can run for several minutes with no interim progress \u2014 warn the user before starting one and keep maxQuestions at or below 100 unless they explicitly want a deep harvest. Credits are charged by extracted question; unused request hold is refunded.'),
3282
4811
  inputSchema: HarvestPaaInputSchema,
3283
4812
  outputSchema: HarvestPaaOutputSchema,
3284
4813
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
3285
4814
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
3286
4815
  server2.registerTool("search_serp", {
3287
4816
  title: "Google SERP Lookup",
3288
- description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request. For US city/state rankings, keep proxyMode as location and pass proxyZip when a city-center ZIP is known; location mode uses fresh residential proxy IDs, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear, the proxy tunnel fails, or location evidence is wrong."),
4817
+ description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request. Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting. Use proxyMode location and proxyZip only when the user explicitly needs city/ZIP-targeted residential proxy evidence."),
3289
4818
  inputSchema: SearchSerpInputSchema,
3290
4819
  outputSchema: SearchSerpOutputSchema,
3291
4820
  annotations: liveWebToolAnnotations("Google SERP Lookup")
3292
4821
  }, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
3293
4822
  server2.registerTool("extract_url", {
3294
4823
  title: "Single URL Extract",
3295
- description: withReportNote("Extract structured data from one public URL: page content as Markdown, heading structure, JSON-LD schema, entity details, NAP score, metadata, and missing schema fields. Use when the user provides a single URL or asks to inspect/scrape one page."),
4824
+ description: withReportNote("Extract structured data from one public URL when the user provides one page, asks to inspect/scrape a page, or needs page content, schema, headings, metadata, screenshots, branding, or media assets. Returns structured page fields plus artifact handles for saved reports/screenshots/media when requested. Use map_site_urls before extracting a site inventory; use extract_site for multi-page crawling."),
3296
4825
  inputSchema: ExtractUrlInputSchema,
3297
4826
  outputSchema: ExtractUrlOutputSchema,
3298
4827
  annotations: liveWebToolAnnotations("Single URL Extract")
3299
4828
  }, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
3300
4829
  server2.registerTool("map_site_urls", {
3301
4830
  title: "Site URL Map",
3302
- description: withReportNote("Map/crawl a public website to build a URL inventory with HTTP status codes, broken links, redirects, and site scope. Use before extract_site for audits or when the user asks for a sitemap/URL inventory."),
4831
+ description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
3303
4832
  inputSchema: MapSiteUrlsInputSchema,
3304
4833
  outputSchema: MapSiteUrlsOutputSchema,
3305
4834
  annotations: liveWebToolAnnotations("Site URL Map")
3306
4835
  }, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
3307
4836
  server2.registerTool("extract_site", {
3308
4837
  title: "Multi-Page Site Extract",
3309
- description: withReportNote("Run multi-page extraction across a public website. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use for website audits, competitor audits, and full-site extraction."),
4838
+ description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
3310
4839
  inputSchema: ExtractSiteInputSchema,
3311
4840
  outputSchema: ExtractSiteOutputSchema,
3312
4841
  annotations: liveWebToolAnnotations("Multi-Page Site Extract")
3313
4842
  }, async (input) => formatExtractSite(await executor.extractSite(input), input));
3314
4843
  server2.registerTool("youtube_harvest", {
3315
4844
  title: "YouTube Video Harvest",
3316
- description: withReportNote('Harvest YouTube video metadata by search query or channel handle/ID/URL. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, dates, durations, URLs, thumbnails, and videoIds for follow-up transcription.'),
4845
+ description: withReportNote('Harvest YouTube video metadata when the user wants to find videos by topic, inspect a channel library, compare video angles, or get videoIds for later transcription. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, durations, URLs, and videoIds for follow-up transcription. Use youtube_transcribe after selecting one video.'),
3317
4846
  inputSchema: YoutubeHarvestInputSchema,
3318
4847
  outputSchema: YoutubeHarvestOutputSchema,
3319
4848
  annotations: liveWebToolAnnotations("YouTube Video Harvest")
3320
4849
  }, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
3321
4850
  server2.registerTool("youtube_transcribe", {
3322
4851
  title: "YouTube Transcription",
3323
- description: withReportNote("Fetch and transcribe captions from a YouTube video. Returns full transcript, timestamped chunks, and word count. Pass a videoId from youtube_harvest results or infer it from a YouTube URL if the user provided one."),
4852
+ description: withReportNote("Fetch and transcribe captions from a YouTube video. Use this when the user asks what was said in a YouTube video, wants claims/offers/lessons extracted from a video, or provides a YouTube URL for transcript work. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Pass videoId from youtube_harvest results, or pass url when the user pasted a YouTube URL. Use youtube_harvest first when you need to discover videos by topic/channel."),
3324
4853
  inputSchema: YoutubeTranscribeInputSchema,
4854
+ outputSchema: YoutubeTranscribeOutputSchema,
3325
4855
  annotations: liveWebToolAnnotations("YouTube Transcription")
3326
4856
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
3327
4857
  server2.registerTool("facebook_page_intel", {
3328
4858
  title: "Facebook Advertiser Ad Intel",
3329
- description: withReportNote("Harvest ads from a Facebook advertiser. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and direct ad video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name as query. Use after facebook_ad_search when possible. For normal public Facebook reels/posts/watch/share URLs, use facebook_video_transcribe instead."),
4859
+ description: withReportNote("Harvest ads from a Facebook advertiser when the user wants current ad copy, creative angles, CTAs, video URLs, or competitive ad intelligence for one brand/page. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and direct ad video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name as query. Use facebook_ad_search first when the advertiser handle is unknown. For normal public Facebook reels/posts/watch/share URLs, use facebook_video_transcribe instead."),
3330
4860
  inputSchema: FacebookPageIntelInputSchema,
3331
4861
  outputSchema: FacebookPageIntelOutputSchema,
3332
4862
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
3333
4863
  }, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
3334
4864
  server2.registerTool("facebook_ad_search", {
3335
4865
  title: "Facebook Ad Library Search",
3336
- description: withReportNote("Search Facebook Ad Library by brand, advertiser, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use to discover competitors, then pass libraryId to facebook_page_intel."),
4866
+ description: withReportNote("Search Facebook Ad Library when the user wants to find advertisers by brand, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use this to discover competitor/page handles, then pass libraryId or pageId to facebook_page_intel for ad details."),
3337
4867
  inputSchema: FacebookAdSearchInputSchema,
3338
4868
  outputSchema: FacebookAdSearchOutputSchema,
3339
4869
  annotations: liveWebToolAnnotations("Facebook Ad Library Search")
3340
4870
  }, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
3341
4871
  server2.registerTool("facebook_ad_transcribe", {
3342
4872
  title: "Facebook Ad Transcription",
3343
- description: "Transcribe audio from a Facebook ad video CDN URL. Returns full transcript and timestamped chunks. Use only with the direct videoUrl value from facebook_page_intel results, not public Facebook post/reel/share URLs.",
4873
+ description: "Transcribe audio from a Facebook ad video CDN URL. Use this when facebook_page_intel returned a direct videoUrl and the user asks what the ad says, what claims it makes, or wants ad-message extraction. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Use only with the direct videoUrl value from facebook_page_intel results; do not pass public Facebook post/reel/share URLs. Use facebook_video_transcribe for organic Facebook URLs, and facebook_page_intel first when you only have a brand/page/ad library handle.",
3344
4874
  inputSchema: FacebookAdTranscribeInputSchema,
4875
+ outputSchema: FacebookAdTranscribeOutputSchema,
3345
4876
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
3346
4877
  }, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
3347
4878
  server2.registerTool("facebook_video_transcribe", {
@@ -3360,14 +4891,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
3360
4891
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
3361
4892
  server2.registerTool("maps_search", {
3362
4893
  title: "Google Maps Business Search",
3363
- description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." For US city/state Maps searches, keep proxyMode as location so the browser service creates a fresh residential proxy ID and browser session for the market; retryable failures rotate to a new proxy and new browser session for up to 5 attempts. Pass proxyZip only when a specific ZIP or city-center ZIP is known. Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, profile metadata, and sanitized attempt telemetry. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
4894
+ description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." Maps is the default location-targeted surface: omit proxyMode for normal Maps use so the service creates residential proxy evidence for the requested market and rotates on retryable failures. Pass proxyZip only when a specific ZIP or city-center ZIP is known. Use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, profile metadata, and sanitized attempt telemetry. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
3364
4895
  inputSchema: MapsSearchInputSchema,
3365
4896
  outputSchema: MapsSearchOutputSchema,
3366
4897
  annotations: liveWebToolAnnotations("Google Maps Business Search")
3367
4898
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
3368
4899
  server2.registerTool("directory_workflow", {
3369
4900
  title: "Directory Workflow: Markets + Maps",
3370
- description: withReportNote('Build directory/prospecting datasets by selecting US city markets from the free Census Population Estimates city/place dataset, optionally joining configured US ZIPS/Lead Magician ZIP groups, then running Google Maps business searches for each city in parallel. Use this when the user wants "all cities over 100k population in a state", "build a directory CSV", "find markets then get Maps data", or similar location-database + Maps workflows. Set minPopulation, state, query, maxResultsPerCity, and concurrency. Use concurrency up to 5 for parallel city sessions. Keep proxyMode as location so each city search creates fresh residential proxy IDs and browser sessions; retryable city failures rotate to a new proxy and new browser session for up to 5 attempts. Saved CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, cid, cid_decimal, city population, and ZIP groups. Structured city results include sanitized attempt telemetry. Use maps_place_intel only when a selected profile needs deeper review topics, profile review count confirmation, or review cards. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
4901
+ description: withReportNote('Build directory/prospecting datasets by selecting US city markets from the free Census Population Estimates city/place dataset, optionally joining configured US ZIPS/Lead Magician ZIP groups, then running Google Maps business searches for each city in parallel. Use this when the user wants "all cities over 100k population in a state", "build a directory CSV", "find markets then get Maps data", or similar location-database + Maps workflows. Set minPopulation, state, query, maxResultsPerCity, and concurrency. Maps workflows default to location-targeted proxying so each city can use city/state or ZIP-group residential proxy evidence; use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. Saved CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, cid, cid_decimal, city population, and ZIP groups. Structured city results include sanitized attempt telemetry. Use maps_place_intel only when a selected profile needs deeper review topics, profile review count confirmation, or review cards. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
3371
4902
  inputSchema: DirectoryWorkflowInputSchema,
3372
4903
  outputSchema: DirectoryWorkflowOutputSchema,
3373
4904
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
@@ -3388,21 +4919,28 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
3388
4919
  }, async (input) => formatWorkflowSuggest(input));
3389
4920
  server2.registerTool("workflow_run", {
3390
4921
  title: "Run Workflow",
3391
- description: withReportNote("Run a higher-level MCP Scraper workflow and return the run id, summary, and artifacts. Use after workflow_suggest or workflow_list. Runnable workflow ids: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. This is the main MCP tool for executing market analysis, ICP evidence packets, local competitive audits, Maps/SERP comparisons, content gap briefs, and AI Overview language guidance."),
4922
+ description: withReportNote("Start a higher-level MCP Scraper workflow. Use after workflow_suggest or workflow_list. Runnable workflow ids: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. This is the main MCP tool for market analysis, ICP evidence packets, local competitive audits, Maps/SERP comparisons, content gap briefs, and AI Overview language guidance. Stepwise workflows (e.g. agent-packet) run ONE leg per call and return runId, the step output, and nextStep \u2014 when nextStep is present, call workflow_step with the runId to run the next leg, and keep calling it until done is true. This keeps each call short instead of one long blocking run, so report each step result to the user as it arrives."),
3392
4923
  inputSchema: WorkflowRunInputSchema,
3393
4924
  outputSchema: WorkflowRunOutputSchema,
3394
4925
  annotations: liveWebToolAnnotations("Run Workflow")
3395
4926
  }, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
4927
+ server2.registerTool("workflow_step", {
4928
+ title: "Advance Workflow Step",
4929
+ description: withReportNote("Run the next leg of a stepwise MCP Scraper workflow started with workflow_run. Pass the runId. Each call executes exactly one logical step (typically one live harvest), persists that step's artifacts, and returns the step output plus nextStep. Keep calling workflow_step with the same runId until done is true, reporting each step result to the user as it lands. Use this instead of waiting on one long workflow call \u2014 it avoids client timeouts on long multi-step jobs."),
4930
+ inputSchema: WorkflowStepInputSchema,
4931
+ outputSchema: WorkflowStepOutputSchema,
4932
+ annotations: liveWebToolAnnotations("Advance Workflow Step")
4933
+ }, async (input) => formatWorkflowStep(await executor.workflowStep(input), input));
3396
4934
  server2.registerTool("workflow_status", {
3397
4935
  title: "Workflow Status",
3398
- description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use after workflow_run when the model needs to re-open a run, inspect artifact ids, or recover from a long workflow.",
4936
+ description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use when a workflow may still be running, when the model needs to re-open a run, inspect artifact ids, recover from a long workflow, or decide whether to call workflow_step or workflow_artifact_read next. Use only a runId returned by workflow_run/workflow_step/workflow_status; do not construct one yourself.",
3399
4937
  inputSchema: WorkflowStatusInputSchema,
3400
4938
  outputSchema: WorkflowStatusOutputSchema,
3401
4939
  annotations: liveWebToolAnnotations("Workflow Status")
3402
4940
  }, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
3403
4941
  server2.registerTool("workflow_artifact_read", {
3404
4942
  title: "Read Workflow Artifact",
3405
- description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use maxBytes to limit large CSV/JSON artifacts.",
4943
+ description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use workflow_status first when artifact ids are unknown. Use only artifactId values returned by workflow_run/workflow_step/workflow_status; do not construct one yourself. Use maxBytes to limit large CSV/JSON artifacts.",
3406
4944
  inputSchema: WorkflowArtifactReadInputSchema,
3407
4945
  outputSchema: WorkflowArtifactReadOutputSchema,
3408
4946
  annotations: liveWebToolAnnotations("Read Workflow Artifact")
@@ -3482,7 +5020,7 @@ function renderInstallTerminal(options) {
3482
5020
  "1/1 install surfaces ready",
3483
5021
  colorize("Newest: MCP workflow tools for market analysis, ICP research, CRO audits, brand briefs, and AI search visibility.", "lime", color),
3484
5022
  "",
3485
- `${colorize("Tools", "cyan", color)} ${colorize("(38 MCP tools)", "muted", color)}`,
5023
+ `${colorize("Tools", "cyan", color)} ${colorize("(39 MCP tools)", "muted", color)}`,
3486
5024
  toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
3487
5025
  toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "directory_workflow"], color),
3488
5026
  toolRow("build", ["rank_tracker_blueprint", "cron plan", "database prompt"], color),
@@ -3490,7 +5028,7 @@ function renderInstallTerminal(options) {
3490
5028
  toolRow("browser", ["browser_open", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
3491
5029
  toolRow("account", ["credits_info", "local reports", "MCP resources"], color),
3492
5030
  `${colorize("Workflows", "cyan", color)} ${colorize("(MCP + CLI + API)", "muted", color)}`,
3493
- toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_status", "workflow_artifact_read"], color),
5031
+ toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_step", "workflow_status", "workflow_artifact_read"], color),
3494
5032
  toolRow("seo", ["directory", "agent-packet", "local audit", "map/serp comparison", "PAA/AIO briefs", "scheduled runs"], color),
3495
5033
  "",
3496
5034
  colorize("Usage tips:", "amber", color),
@@ -3536,10 +5074,10 @@ if (!forceStdio && (interactiveTerminal || wantsHelp)) {
3536
5074
  }
3537
5075
  function readApiKeyFile() {
3538
5076
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
3539
- const paths = [explicitPath, (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".mcp-scraper-key")].filter(Boolean);
5077
+ const paths = [explicitPath, (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".mcp-scraper-key")].filter(Boolean);
3540
5078
  for (const path of paths) {
3541
5079
  try {
3542
- const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
5080
+ const value = (0, import_node_fs6.readFileSync)(path, "utf8").trim();
3543
5081
  if (value) return value;
3544
5082
  } catch {
3545
5083
  }