mcp-scraper 0.2.19 → 0.2.20

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 (71) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/api-server.cjs +1020 -348
  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 +314 -55
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +321 -167
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +3 -2
  11. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +1128 -530
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-scraper-combined-stdio-server.js +5 -4
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  16. package/dist/bin/mcp-scraper-install.cjs +3 -3
  17. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  18. package/dist/bin/mcp-scraper-install.js +2 -2
  19. package/dist/bin/mcp-stdio-server.cjs +726 -387
  20. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  21. package/dist/bin/mcp-stdio-server.js +3 -2
  22. package/dist/bin/mcp-stdio-server.js.map +1 -1
  23. package/dist/bin/paa-harvest.cjs +5 -3
  24. package/dist/bin/paa-harvest.cjs.map +1 -1
  25. package/dist/bin/paa-harvest.js +2 -1
  26. package/dist/bin/paa-harvest.js.map +1 -1
  27. package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
  28. package/dist/chunk-DBQDG7EH.js.map +1 -0
  29. package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
  30. package/dist/chunk-H2R232HK.js.map +1 -0
  31. package/dist/{chunk-76P4DKWR.js → chunk-L4OWOUGR.js} +315 -56
  32. package/dist/chunk-L4OWOUGR.js.map +1 -0
  33. package/dist/chunk-LFATOGDF.js +111 -0
  34. package/dist/chunk-LFATOGDF.js.map +1 -0
  35. package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
  36. package/dist/chunk-MLNCKQ2B.js.map +1 -0
  37. package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
  38. package/dist/chunk-O5J7E4VX.js.map +1 -0
  39. package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
  40. package/dist/{chunk-CQTAKXBN.js → chunk-XGUDTDZ2.js} +7 -104
  41. package/dist/chunk-XGUDTDZ2.js.map +1 -0
  42. package/dist/chunk-XKUDVN2E.js +7 -0
  43. package/dist/chunk-XKUDVN2E.js.map +1 -0
  44. package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
  45. package/dist/index.cjs +5 -3
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +2 -2
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +2 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/{server-CRUSZA2Q.js → server-7NE45K2A.js} +211 -37
  52. package/dist/server-7NE45K2A.js.map +1 -0
  53. package/dist/{worker-OZSWIS3F.js → worker-MIYG2B2I.js} +5 -4
  54. package/dist/{worker-OZSWIS3F.js.map → worker-MIYG2B2I.js.map} +1 -1
  55. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
  56. package/docs/mcp-tool-craft-lint.generated.md +51 -0
  57. package/docs/mcp-tool-manifest.generated.json +1406 -0
  58. package/docs/specs/local-competitive-audit-spec.md +1 -2
  59. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
  60. package/package.json +4 -1
  61. package/dist/chunk-4UEVYYDW.js.map +0 -1
  62. package/dist/chunk-76P4DKWR.js.map +0 -1
  63. package/dist/chunk-CQTAKXBN.js.map +0 -1
  64. package/dist/chunk-F44RBOJ5.js.map +0 -1
  65. package/dist/chunk-MCBOZ2JF.js.map +0 -1
  66. package/dist/chunk-RU2HZD55.js +0 -7
  67. package/dist/chunk-RU2HZD55.js.map +0 -1
  68. package/dist/chunk-TL7YTFLH.js.map +0 -1
  69. package/dist/server-CRUSZA2Q.js.map +0 -1
  70. /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
  71. /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
@@ -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
  }
@@ -212,10 +251,22 @@ var import_node_os2 = require("os");
212
251
  var import_node_path2 = require("path");
213
252
 
214
253
  // src/version.ts
215
- var PACKAGE_VERSION = "0.2.19";
254
+ var PACKAGE_VERSION = "0.2.20";
216
255
 
217
256
  // src/mcp/browser-agent-tool-schemas.ts
218
257
  var import_zod = require("zod");
258
+ var NullableString = import_zod.z.string().nullable();
259
+ var BrowserRawObject = import_zod.z.record(import_zod.z.unknown());
260
+ var BrowserElementOutput = import_zod.z.record(import_zod.z.unknown());
261
+ var BrowserBaseOutput = {
262
+ ok: import_zod.z.boolean().describe("Whether the browser-agent action succeeded."),
263
+ tool: import_zod.z.string().describe("Browser Agent MCP tool that produced this response."),
264
+ session_id: NullableString.describe("Browser session id when the response is scoped to a session.")
265
+ };
266
+ var BrowserReplayBaseOutput = {
267
+ ...BrowserBaseOutput,
268
+ replay_id: NullableString.describe("Replay id when the response is scoped to a replay.")
269
+ };
219
270
  var BrowserOpenInputSchema = {
220
271
  label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
221
272
  url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
@@ -223,7 +274,7 @@ var BrowserOpenInputSchema = {
223
274
  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
275
  };
225
276
  var BrowserSessionInputSchema = {
226
- session_id: import_zod.z.string().describe("The session id returned by browser_open.")
277
+ 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
278
  };
228
279
  var BrowserLocateTargetSchema = import_zod.z.object({
229
280
  name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
@@ -235,43 +286,43 @@ var BrowserLocateTargetSchema = import_zod.z.object({
235
286
  message: "target requires selector or text"
236
287
  });
237
288
  var BrowserLocateInputSchema = {
238
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
289
+ 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
290
  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
291
  };
241
292
  var BrowserGotoInputSchema = {
242
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
293
+ 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
294
  url: import_zod.z.string().url().describe("URL to navigate the browser to.")
244
295
  };
245
296
  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."),
297
+ 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."),
298
+ 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."),
299
+ 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
300
  button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
250
301
  num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
251
302
  };
252
303
  var BrowserTypeInputSchema = {
253
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
304
+ 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
305
  text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
255
306
  delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
256
307
  };
257
308
  var BrowserScrollInputSchema = {
258
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
309
+ 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
310
  delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
260
311
  delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
261
312
  x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
262
313
  y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
263
314
  };
264
315
  var BrowserPressInputSchema = {
265
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
316
+ 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
317
  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
318
  };
268
319
  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.")
320
+ 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."),
321
+ 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
322
  };
272
323
  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."),
324
+ 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."),
325
+ 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
326
  filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
276
327
  };
277
328
  var BrowserReplayAnnotationSchema = import_zod.z.object({
@@ -293,7 +344,7 @@ var BrowserReplayAnnotationSchema = import_zod.z.object({
293
344
  thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
294
345
  });
295
346
  var BrowserReplayMarkInputSchema = {
296
- session_id: import_zod.z.string().describe("The session id returned by browser_open. A replay must already be recording."),
347
+ 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
348
  target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
298
349
  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
350
  label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
@@ -304,8 +355,8 @@ var BrowserReplayMarkInputSchema = {
304
355
  duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
305
356
  };
306
357
  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."),
358
+ 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."),
359
+ 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
360
  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
361
  filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
311
362
  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,6 +367,113 @@ var BrowserReplayAnnotateInputSchema = {
316
367
  var BrowserListInputSchema = {
317
368
  include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
318
369
  };
370
+ var BrowserOpenOutputSchema = {
371
+ ok: import_zod.z.boolean(),
372
+ tool: import_zod.z.literal("browser_open"),
373
+ 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."),
374
+ watch_url: import_zod.z.string().describe("Human watch/takeover URL for this browser session."),
375
+ live_view_url: NullableString.describe("Raw live-view URL from the browser service when available."),
376
+ url: NullableString.describe("Initial URL requested by the caller, when provided."),
377
+ hint: import_zod.z.string(),
378
+ raw: BrowserRawObject.optional()
379
+ };
380
+ var BrowserScreenshotOutputSchema = {
381
+ ...BrowserBaseOutput,
382
+ tool: import_zod.z.literal("browser_screenshot"),
383
+ url: NullableString,
384
+ title: NullableString,
385
+ text: import_zod.z.string(),
386
+ elements: import_zod.z.array(BrowserElementOutput),
387
+ screenshot: import_zod.z.object({
388
+ mime_type: import_zod.z.string(),
389
+ inline: import_zod.z.boolean()
390
+ }).nullable()
391
+ };
392
+ var BrowserReadOutputSchema = {
393
+ ...BrowserBaseOutput,
394
+ tool: import_zod.z.literal("browser_read"),
395
+ url: NullableString,
396
+ title: NullableString,
397
+ text: import_zod.z.string(),
398
+ elements: import_zod.z.array(BrowserElementOutput),
399
+ raw: BrowserRawObject.optional()
400
+ };
401
+ var BrowserLocateOutputSchema = {
402
+ ...BrowserBaseOutput,
403
+ tool: import_zod.z.literal("browser_locate"),
404
+ url: NullableString,
405
+ title: NullableString,
406
+ viewport: BrowserRawObject.nullable(),
407
+ replay: BrowserRawObject.nullable(),
408
+ targets: import_zod.z.array(BrowserRawObject),
409
+ raw: BrowserRawObject.optional()
410
+ };
411
+ var BrowserActionOutputSchema = {
412
+ ...BrowserBaseOutput,
413
+ result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
414
+ nextRecommendedTool: import_zod.z.string().nullable()
415
+ };
416
+ var BrowserReplayStartOutputSchema = {
417
+ ...BrowserReplayBaseOutput,
418
+ tool: import_zod.z.literal("browser_replay_start"),
419
+ view_url: NullableString,
420
+ download_url: NullableString,
421
+ raw: BrowserRawObject.optional()
422
+ };
423
+ var BrowserReplayStopOutputSchema = {
424
+ ...BrowserReplayBaseOutput,
425
+ tool: import_zod.z.literal("browser_replay_stop"),
426
+ view_url: NullableString,
427
+ download_url: NullableString,
428
+ raw: BrowserRawObject.optional()
429
+ };
430
+ var BrowserListReplaysOutputSchema = {
431
+ ...BrowserBaseOutput,
432
+ tool: import_zod.z.literal("browser_list_replays"),
433
+ replays: import_zod.z.array(BrowserRawObject),
434
+ count: import_zod.z.number().int().min(0)
435
+ };
436
+ var BrowserReplayDownloadOutputSchema = {
437
+ ...BrowserReplayBaseOutput,
438
+ tool: import_zod.z.literal("browser_replay_download"),
439
+ file_path: NullableString,
440
+ bytes: import_zod.z.number().int().min(0).nullable(),
441
+ mime_type: NullableString,
442
+ download_url: NullableString
443
+ };
444
+ var BrowserReplayMarkOutputSchema = {
445
+ ...BrowserReplayBaseOutput,
446
+ tool: import_zod.z.literal("browser_replay_mark"),
447
+ annotation: BrowserRawObject,
448
+ source_width: import_zod.z.number().nullable(),
449
+ source_height: import_zod.z.number().nullable(),
450
+ target: BrowserRawObject.nullable(),
451
+ hint: import_zod.z.string()
452
+ };
453
+ var BrowserReplayAnnotateOutputSchema = {
454
+ ...BrowserReplayBaseOutput,
455
+ tool: import_zod.z.literal("browser_replay_annotate"),
456
+ source_file_path: NullableString,
457
+ annotated_file_path: NullableString,
458
+ bytes: import_zod.z.number().int().min(0).nullable(),
459
+ width: import_zod.z.number().int().min(0).nullable(),
460
+ height: import_zod.z.number().int().min(0).nullable(),
461
+ annotation_count: import_zod.z.number().int().min(0).nullable(),
462
+ mime_type: NullableString
463
+ };
464
+ var BrowserCloseOutputSchema = {
465
+ ...BrowserBaseOutput,
466
+ tool: import_zod.z.literal("browser_close"),
467
+ closed: import_zod.z.boolean(),
468
+ raw: BrowserRawObject.optional()
469
+ };
470
+ var BrowserListSessionsOutputSchema = {
471
+ ok: import_zod.z.boolean(),
472
+ tool: import_zod.z.literal("browser_list_sessions"),
473
+ session_id: import_zod.z.null(),
474
+ sessions: import_zod.z.array(BrowserRawObject),
475
+ count: import_zod.z.number().int().min(0)
476
+ };
319
477
 
320
478
  // src/mcp/replay-annotator.ts
321
479
  var import_node_child_process = require("child_process");
@@ -578,8 +736,40 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
578
736
  }
579
737
 
580
738
  // src/mcp/browser-agent-mcp-server.ts
581
- function textResult(value, isError = false) {
582
- return { content: [{ type: "text", text: JSON.stringify(value) }], isError };
739
+ function structuredResult(value, isError = false) {
740
+ return {
741
+ content: [{ type: "text", text: JSON.stringify(value) }],
742
+ structuredContent: value,
743
+ isError
744
+ };
745
+ }
746
+ function errorMessage(value) {
747
+ if (value && typeof value === "object") {
748
+ const data = value;
749
+ if (typeof data.error === "string") return data.error;
750
+ if (typeof data.message === "string") return data.message;
751
+ }
752
+ return typeof value === "string" ? value : "Browser Agent request failed";
753
+ }
754
+ function errorResult(tool, value, sessionId = null, replayId = null) {
755
+ return structuredResult({
756
+ ok: false,
757
+ tool,
758
+ session_id: sessionId,
759
+ ...replayId !== null ? { replay_id: replayId } : {},
760
+ error: errorMessage(value),
761
+ raw: value && typeof value === "object" ? value : { value }
762
+ }, true);
763
+ }
764
+ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
765
+ if (!ok) return errorResult(tool, data, sessionId);
766
+ return structuredResult({
767
+ ok: true,
768
+ tool,
769
+ session_id: sessionId,
770
+ result: data && typeof data === "object" ? data : { value: data },
771
+ nextRecommendedTool
772
+ });
583
773
  }
584
774
  function outputBaseDir() {
585
775
  return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
@@ -679,6 +869,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
679
869
  title: "Open Browser Session",
680
870
  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.",
681
871
  inputSchema: BrowserOpenInputSchema,
872
+ outputSchema: BrowserOpenOutputSchema,
682
873
  annotations: annotations("Open Browser Session")
683
874
  },
684
875
  async (input) => {
@@ -687,16 +878,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
687
878
  profile: input.profile,
688
879
  timeout_seconds: input.timeout_seconds
689
880
  });
690
- if (!open.ok) return textResult(open.data, true);
881
+ if (!open.ok) return errorResult("browser_open", open.data);
691
882
  const session = open.data;
692
883
  if (input.url) {
693
884
  await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: input.url });
694
885
  }
695
- return textResult({
886
+ return structuredResult({
887
+ ok: true,
888
+ tool: "browser_open",
696
889
  session_id: session.session_id,
697
890
  watch_url: `${consoleBase}/console/${session.session_id}`,
698
891
  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."
892
+ url: input.url ?? null,
893
+ hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
894
+ raw: session
700
895
  });
701
896
  }
702
897
  );
@@ -706,19 +901,30 @@ function registerBrowserAgentMcpTools(server2, opts) {
706
901
  title: "See Page (Screenshot + Elements)",
707
902
  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
903
  inputSchema: BrowserSessionInputSchema,
904
+ outputSchema: BrowserScreenshotOutputSchema,
709
905
  annotations: annotations("See Page", true)
710
906
  },
711
907
  async (input) => {
712
908
  const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
713
- if (!res.ok) return textResult(res.data, true);
909
+ if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
714
910
  const { image_base64, mime_type, url, title, elements, text } = res.data;
715
911
  const content = [];
716
912
  if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
913
+ const structured = {
914
+ ok: true,
915
+ tool: "browser_screenshot",
916
+ session_id: input.session_id,
917
+ url: url ?? null,
918
+ title: title ?? null,
919
+ text: typeof text === "string" ? text : "",
920
+ elements: Array.isArray(elements) ? elements : [],
921
+ screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
922
+ };
717
923
  content.push({
718
924
  type: "text",
719
- text: JSON.stringify({ url, title, elements, text })
925
+ text: JSON.stringify(structured)
720
926
  });
721
- return { content };
927
+ return { content, structuredContent: structured };
722
928
  }
723
929
  );
724
930
  server2.registerTool(
@@ -727,11 +933,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
727
933
  title: "Read Page Text + Elements",
728
934
  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
935
  inputSchema: BrowserSessionInputSchema,
936
+ outputSchema: BrowserReadOutputSchema,
730
937
  annotations: annotations("Read Page", true)
731
938
  },
732
939
  async (input) => {
733
940
  const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
734
- return textResult(res.data, !res.ok);
941
+ if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
942
+ return structuredResult({
943
+ ok: true,
944
+ tool: "browser_read",
945
+ session_id: input.session_id,
946
+ url: res.data?.url ?? null,
947
+ title: res.data?.title ?? null,
948
+ text: typeof res.data?.text === "string" ? res.data.text : "",
949
+ elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
950
+ raw: res.data
951
+ });
735
952
  }
736
953
  );
737
954
  server2.registerTool(
@@ -740,32 +957,46 @@ function registerBrowserAgentMcpTools(server2, opts) {
740
957
  title: "Locate DOM Targets",
741
958
  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
959
  inputSchema: BrowserLocateInputSchema,
960
+ outputSchema: BrowserLocateOutputSchema,
743
961
  annotations: annotations("Locate DOM Targets", true)
744
962
  },
745
963
  async (input) => {
746
964
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
747
- return textResult(res.data, !res.ok);
965
+ if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
966
+ return structuredResult({
967
+ ok: true,
968
+ tool: "browser_locate",
969
+ session_id: input.session_id,
970
+ url: res.data?.url ?? null,
971
+ title: res.data?.title ?? null,
972
+ viewport: res.data?.viewport ?? null,
973
+ replay: res.data?.replay ?? null,
974
+ targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
975
+ raw: res.data
976
+ });
748
977
  }
749
978
  );
750
979
  server2.registerTool(
751
980
  "browser_goto",
752
981
  {
753
982
  title: "Navigate To URL",
754
- description: "Navigate the browser to a URL. Follow with browser_screenshot to see the result.",
983
+ 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
984
  inputSchema: BrowserGotoInputSchema,
985
+ outputSchema: BrowserActionOutputSchema,
756
986
  annotations: annotations("Navigate To URL")
757
987
  },
758
988
  async (input) => {
759
989
  const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
760
- return textResult(res.data, !res.ok);
990
+ return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
761
991
  }
762
992
  );
763
993
  server2.registerTool(
764
994
  "browser_click",
765
995
  {
766
996
  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.",
997
+ 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
998
  inputSchema: BrowserClickInputSchema,
999
+ outputSchema: BrowserActionOutputSchema,
769
1000
  annotations: annotations("Click")
770
1001
  },
771
1002
  async (input) => {
@@ -775,28 +1006,30 @@ function registerBrowserAgentMcpTools(server2, opts) {
775
1006
  button: input.button,
776
1007
  num_clicks: input.num_clicks
777
1008
  });
778
- return textResult(res.data, !res.ok);
1009
+ return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
779
1010
  }
780
1011
  );
781
1012
  server2.registerTool(
782
1013
  "browser_type",
783
1014
  {
784
1015
  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.',
1016
+ 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
1017
  inputSchema: BrowserTypeInputSchema,
1018
+ outputSchema: BrowserActionOutputSchema,
787
1019
  annotations: annotations("Type Text")
788
1020
  },
789
1021
  async (input) => {
790
1022
  const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
791
- return textResult(res.data, !res.ok);
1023
+ return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
792
1024
  }
793
1025
  );
794
1026
  server2.registerTool(
795
1027
  "browser_scroll",
796
1028
  {
797
1029
  title: "Scroll",
798
- description: "Scroll the page. Positive delta_y scrolls down. Follow with browser_screenshot to see newly revealed content.",
1030
+ 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
1031
  inputSchema: BrowserScrollInputSchema,
1032
+ outputSchema: BrowserActionOutputSchema,
800
1033
  annotations: annotations("Scroll")
801
1034
  },
802
1035
  async (input) => {
@@ -806,20 +1039,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
806
1039
  x: input.x,
807
1040
  y: input.y
808
1041
  });
809
- return textResult(res.data, !res.ok);
1042
+ return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
810
1043
  }
811
1044
  );
812
1045
  server2.registerTool(
813
1046
  "browser_press",
814
1047
  {
815
1048
  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.',
1049
+ 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
1050
  inputSchema: BrowserPressInputSchema,
1051
+ outputSchema: BrowserActionOutputSchema,
818
1052
  annotations: annotations("Press Keys")
819
1053
  },
820
1054
  async (input) => {
821
1055
  const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
822
- return textResult(res.data, !res.ok);
1056
+ return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
823
1057
  }
824
1058
  );
825
1059
  server2.registerTool(
@@ -828,11 +1062,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
828
1062
  title: "Start Recording",
829
1063
  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
1064
  inputSchema: BrowserSessionInputSchema,
1065
+ outputSchema: BrowserReplayStartOutputSchema,
831
1066
  annotations: annotations("Start Recording")
832
1067
  },
833
1068
  async (input) => {
834
1069
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
835
- return textResult(res.data, !res.ok);
1070
+ if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
1071
+ return structuredResult({
1072
+ ok: true,
1073
+ tool: "browser_replay_start",
1074
+ session_id: input.session_id,
1075
+ replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
1076
+ view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
1077
+ download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
1078
+ raw: res.data
1079
+ });
836
1080
  }
837
1081
  );
838
1082
  server2.registerTool(
@@ -841,11 +1085,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
841
1085
  title: "Stop Recording",
842
1086
  description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 locally.",
843
1087
  inputSchema: BrowserReplayStopInputSchema,
1088
+ outputSchema: BrowserReplayStopOutputSchema,
844
1089
  annotations: annotations("Stop Recording")
845
1090
  },
846
1091
  async (input) => {
847
1092
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
848
- return textResult(res.data, !res.ok);
1093
+ if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
1094
+ return structuredResult({
1095
+ ok: true,
1096
+ tool: "browser_replay_stop",
1097
+ session_id: input.session_id,
1098
+ replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
1099
+ view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
1100
+ download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
1101
+ raw: res.data
1102
+ });
849
1103
  }
850
1104
  );
851
1105
  server2.registerTool(
@@ -854,11 +1108,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
854
1108
  title: "List Replay Videos",
855
1109
  description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
856
1110
  inputSchema: BrowserSessionInputSchema,
1111
+ outputSchema: BrowserListReplaysOutputSchema,
857
1112
  annotations: annotations("List Replay Videos", true)
858
1113
  },
859
1114
  async (input) => {
860
1115
  const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
861
- return textResult(res.data, !res.ok);
1116
+ if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
1117
+ const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
1118
+ return structuredResult({
1119
+ ok: true,
1120
+ tool: "browser_list_replays",
1121
+ session_id: input.session_id,
1122
+ replays,
1123
+ count: replays.length
1124
+ });
862
1125
  }
863
1126
  );
864
1127
  server2.registerTool(
@@ -867,11 +1130,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
867
1130
  title: "Download Replay MP4",
868
1131
  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
1132
  inputSchema: BrowserReplayDownloadInputSchema,
870
- annotations: annotations("Download Replay MP4", true)
1133
+ outputSchema: BrowserReplayDownloadOutputSchema,
1134
+ annotations: annotations("Download Replay MP4")
871
1135
  },
872
1136
  async (input) => {
873
1137
  const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
874
- return textResult(res.data, !res.ok);
1138
+ if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
1139
+ return structuredResult({
1140
+ ok: true,
1141
+ tool: "browser_replay_download",
1142
+ session_id: input.session_id,
1143
+ replay_id: input.replay_id,
1144
+ file_path: res.data?.file_path ?? null,
1145
+ bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
1146
+ mime_type: res.data?.mime_type ?? null,
1147
+ download_url: res.data?.download_url ?? null
1148
+ });
875
1149
  }
876
1150
  );
877
1151
  server2.registerTool(
@@ -880,19 +1154,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
880
1154
  title: "Mark Replay Annotation",
881
1155
  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
1156
  inputSchema: BrowserReplayMarkInputSchema,
1157
+ outputSchema: BrowserReplayMarkOutputSchema,
883
1158
  annotations: annotations("Mark Replay Annotation", true)
884
1159
  },
885
1160
  async (input) => {
886
1161
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
887
- if (!res.ok) return textResult(res.data, true);
1162
+ if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
888
1163
  const target = res.data?.targets?.[0];
889
1164
  const element = target?.element;
890
1165
  const elapsed = res.data?.replay?.replay_elapsed_seconds;
891
1166
  if (!target?.found || !element) {
892
- return textResult({ error: target?.error ?? "target not found in current viewport", target }, true);
1167
+ return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
893
1168
  }
894
1169
  if (!finiteNumber2(elapsed)) {
895
- return textResult({ error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, true);
1170
+ return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
896
1171
  }
897
1172
  const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
898
1173
  const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
@@ -909,7 +1184,11 @@ function registerBrowserAgentMcpTools(server2, opts) {
909
1184
  ...input.color ? { color: input.color } : {},
910
1185
  ...input.thickness ? { thickness: input.thickness } : {}
911
1186
  };
912
- return textResult({
1187
+ return structuredResult({
1188
+ ok: true,
1189
+ tool: "browser_replay_mark",
1190
+ session_id: input.session_id,
1191
+ replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
913
1192
  annotation,
914
1193
  source_width: padded.sourceWidth,
915
1194
  source_height: padded.sourceHeight,
@@ -925,12 +1204,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
925
1204
  title: "Annotate Replay MP4",
926
1205
  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
1206
  inputSchema: BrowserReplayAnnotateInputSchema,
928
- annotations: annotations("Annotate Replay MP4", true)
1207
+ outputSchema: BrowserReplayAnnotateOutputSchema,
1208
+ annotations: annotations("Annotate Replay MP4")
929
1209
  },
930
1210
  async (input) => {
931
1211
  const sourceName = input.filename ? `${input.filename}-source` : void 0;
932
1212
  const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
933
- if (!downloaded.ok) return textResult(downloaded.data, true);
1213
+ if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
934
1214
  try {
935
1215
  const sourcePath = String(downloaded.data.file_path);
936
1216
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
@@ -942,7 +1222,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
942
1222
  sourceLeftOffset: input.source_left_offset,
943
1223
  sourceTopOffset: input.source_top_offset
944
1224
  });
945
- return textResult({
1225
+ return structuredResult({
1226
+ ok: true,
1227
+ tool: "browser_replay_annotate",
1228
+ session_id: input.session_id,
946
1229
  replay_id: input.replay_id,
947
1230
  source_file_path: sourcePath,
948
1231
  annotated_file_path: result.filePath,
@@ -953,7 +1236,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
953
1236
  mime_type: "video/mp4"
954
1237
  });
955
1238
  } catch (err) {
956
- return textResult({ error: err instanceof Error ? err.message : String(err) }, true);
1239
+ return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
957
1240
  }
958
1241
  }
959
1242
  );
@@ -961,28 +1244,43 @@ function registerBrowserAgentMcpTools(server2, opts) {
961
1244
  "browser_close",
962
1245
  {
963
1246
  title: "Close Browser Session",
964
- description: "Close and release the browser session when the task is done.",
1247
+ 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
1248
  inputSchema: BrowserSessionInputSchema,
1249
+ outputSchema: BrowserCloseOutputSchema,
966
1250
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
967
1251
  },
968
1252
  async (input) => {
969
1253
  const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
970
- return textResult(res.data, !res.ok);
1254
+ if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
1255
+ return structuredResult({
1256
+ ok: true,
1257
+ tool: "browser_close",
1258
+ session_id: input.session_id,
1259
+ closed: true,
1260
+ raw: res.data
1261
+ });
971
1262
  }
972
1263
  );
973
1264
  server2.registerTool(
974
1265
  "browser_list_sessions",
975
1266
  {
976
1267
  title: "List Browser Sessions",
977
- description: "List your browser sessions and their status, with a watch_url for each.",
1268
+ 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
1269
  inputSchema: BrowserListInputSchema,
1270
+ outputSchema: BrowserListSessionsOutputSchema,
979
1271
  annotations: annotations("List Browser Sessions", true)
980
1272
  },
981
1273
  async (input) => {
982
1274
  const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
983
- if (!res.ok) return textResult(res.data, true);
1275
+ if (!res.ok) return errorResult("browser_list_sessions", res.data);
984
1276
  const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
985
- return textResult({ sessions });
1277
+ return structuredResult({
1278
+ ok: true,
1279
+ tool: "browser_list_sessions",
1280
+ session_id: null,
1281
+ sessions,
1282
+ count: sessions.length
1283
+ });
986
1284
  }
987
1285
  );
988
1286
  }
@@ -1462,7 +1760,7 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
1462
1760
  const full = `# URL Extract: ${url}
1463
1761
  **${title}**
1464
1762
  ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
1465
- const textResult2 = oneBlock(full);
1763
+ const textResult = oneBlock(full);
1466
1764
  const structuredContent = {
1467
1765
  url,
1468
1766
  title: d.title ?? null,
@@ -1477,13 +1775,13 @@ ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSectio
1477
1775
  if (screenshotMeta?.base64) {
1478
1776
  return {
1479
1777
  content: [
1480
- ...textResult2.content,
1778
+ ...textResult.content,
1481
1779
  { type: "image", data: screenshotMeta.base64, mimeType: "image/png" }
1482
1780
  ],
1483
1781
  structuredContent
1484
1782
  };
1485
1783
  }
1486
- return { ...textResult2, structuredContent };
1784
+ return { ...textResult, structuredContent };
1487
1785
  }
1488
1786
  function formatMapSiteUrls(raw, input) {
1489
1787
  const parsed = parseData(raw);
@@ -1612,13 +1910,25 @@ ${videoRows}`,
1612
1910
  }
1613
1911
  };
1614
1912
  }
1913
+ function structuredTranscriptChunks(chunks) {
1914
+ return chunks.map((c) => ({
1915
+ startSec: Number.isFinite(c.timestamp?.[0]) ? c.timestamp[0] : 0,
1916
+ endSec: Number.isFinite(c.timestamp?.[1]) ? c.timestamp[1] : 0,
1917
+ text: c.text
1918
+ }));
1919
+ }
1920
+ function wordCount(text) {
1921
+ return text.trim() ? text.trim().split(/\s+/).length : 0;
1922
+ }
1615
1923
  function formatYoutubeTranscribe(raw, input) {
1616
1924
  const parsed = parseData(raw);
1617
1925
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1618
1926
  const d = parsed.data;
1619
1927
  const text = d.text ?? "";
1620
1928
  const chunks = d.chunks ?? [];
1929
+ const videoId = d.videoId ?? input.videoId ?? null;
1621
1930
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
1931
+ const words = wordCount(text);
1622
1932
  const chunkRows = chunks.slice(0, 50).map((c) => {
1623
1933
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
1624
1934
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -1626,8 +1936,8 @@ function formatYoutubeTranscribe(raw, input) {
1626
1936
  return `| ${mm}:${ss} | ${cell(truncate(c.text, 120))} |`;
1627
1937
  }).join("\n");
1628
1938
  const full = [
1629
- `# YouTube Transcript: \`${input.videoId}\``,
1630
- `**Duration:** ${durSec}s \xB7 **${text.split(" ").length} words**`,
1939
+ `# YouTube Transcript: \`${videoId ?? input.url ?? "video"}\``,
1940
+ `**Duration:** ${durSec}s \xB7 **${words} words**`,
1631
1941
  `
1632
1942
  ## Full Transcript
1633
1943
  ${text}`,
@@ -1640,7 +1950,22 @@ ${chunkRows}` : "",
1640
1950
  ---
1641
1951
  \u{1F4A1} Harvest more from this channel: use \`youtube_harvest\` with \`mode: "channel"\``
1642
1952
  ].filter(Boolean).join("\n");
1643
- return oneBlock(full);
1953
+ return {
1954
+ ...oneBlock(full),
1955
+ structuredContent: {
1956
+ videoId,
1957
+ url: videoId ? `https://www.youtube.com/watch?v=${videoId}` : input.url ?? null,
1958
+ wordCount: words,
1959
+ chunkCount: chunks.length,
1960
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
1961
+ transcriptText: text,
1962
+ chunks: structuredTranscriptChunks(chunks),
1963
+ resolvedInputs: {
1964
+ videoId,
1965
+ url: input.url ?? null
1966
+ }
1967
+ }
1968
+ };
1644
1969
  }
1645
1970
  function formatFacebookPageIntel(raw, input) {
1646
1971
  const parsed = parseData(raw);
@@ -1650,12 +1975,13 @@ function formatFacebookPageIntel(raw, input) {
1650
1975
  const ads = d.ads ?? [];
1651
1976
  const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
1652
1977
  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"}`,
1978
+ `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
1654
1979
  ad.headline ? `**Headline:** ${ad.headline}` : "",
1655
1980
  ad.primaryText ? `**Copy:** ${truncate(ad.primaryText, 200)}` : "",
1656
1981
  ad.cta ? `**CTA:** ${ad.cta}` : "",
1657
- ad.videoUrl ? `**Video URL:** \`${ad.videoUrl}\`` : "",
1658
- ad.variations ? `**Variations:** ${ad.variations}` : ""
1982
+ ad.landingUrl ? `**Landing URL:** ${ad.landingUrl}` : "",
1983
+ ad.videoUrl ?? ad.videoSrc ? `**Video URL:** \`${ad.videoUrl ?? ad.videoSrc}\`` : "",
1984
+ ad.variations ?? ad.clusterCount ? `**Variations:** ${ad.variations ?? ad.clusterCount}` : ""
1659
1985
  ].filter(Boolean).join("\n")).join("\n\n---\n\n");
1660
1986
  const full = [
1661
1987
  `# Facebook Ad Intel: ${advertiser}`,
@@ -1681,11 +2007,16 @@ ${adBlocks}`,
1681
2007
  libraryId: ad.libraryId ?? null,
1682
2008
  status: ad.status ?? null,
1683
2009
  creativeType: ad.creativeType ?? null,
2010
+ primaryText: ad.primaryText ?? null,
1684
2011
  headline: ad.headline ?? null,
1685
2012
  cta: ad.cta ?? null,
1686
- startDate: ad.startDate ?? null,
1687
- videoUrl: ad.videoUrl ?? null,
1688
- variations: typeof ad.variations === "number" ? ad.variations : null
2013
+ startDate: ad.startDate ?? ad.started ?? null,
2014
+ landingUrl: ad.landingUrl ?? null,
2015
+ domain: ad.domain ?? null,
2016
+ videoUrl: ad.videoUrl ?? ad.videoSrc ?? null,
2017
+ imageUrl: ad.imageUrl ?? ad.imageSrc ?? null,
2018
+ videoPoster: ad.videoPoster ?? null,
2019
+ variations: typeof ad.variations === "number" ? ad.variations : typeof ad.clusterCount === "number" ? ad.clusterCount : null
1689
2020
  }))
1690
2021
  }
1691
2022
  };
@@ -1719,8 +2050,11 @@ ${rows}`,
1719
2050
  advertiserCount: advertisers.length,
1720
2051
  advertisers: advertisers.map((a) => ({
1721
2052
  name: a.pageName ?? a.name ?? null,
2053
+ pageId: a.pageId ?? null,
2054
+ pageUrl: a.pageUrl ?? null,
1722
2055
  adCount: typeof a.adCount === "number" ? a.adCount : null,
1723
- libraryId: a.sampleLibraryId ?? a.libraryId ?? null
2056
+ libraryId: a.sampleLibraryId ?? a.libraryId ?? null,
2057
+ sampleLibraryId: a.sampleLibraryId ?? null
1724
2058
  }))
1725
2059
  }
1726
2060
  };
@@ -1818,6 +2152,34 @@ function formatWorkflowSuggest(input) {
1818
2152
  }
1819
2153
  };
1820
2154
  }
2155
+ function workflowStepLines(data) {
2156
+ const step = data.step;
2157
+ const nextStep = data.nextStep;
2158
+ const done = data.done === true;
2159
+ const lines = [];
2160
+ if (step) {
2161
+ const index = Number(step.index ?? 0);
2162
+ const totalSteps = step.totalSteps != null ? Number(step.totalSteps) : void 0;
2163
+ const stepLabel = totalSteps ? `${index + 1}/${totalSteps}` : `${index + 1}`;
2164
+ lines.push(`
2165
+ ## Step ${stepLabel}: ${step.title ?? step.id ?? ""}`);
2166
+ const output = step.output;
2167
+ if (output && Object.keys(output).length) {
2168
+ lines.push(Object.entries(output).map(([k, v]) => `- ${k}: ${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join("\n"));
2169
+ }
2170
+ const warnings = Array.isArray(step.warnings) ? step.warnings : [];
2171
+ if (warnings.length) lines.push(`
2172
+ **Warnings:**
2173
+ ${warnings.map((w) => `- ${w}`).join("\n")}`);
2174
+ }
2175
+ if (done) {
2176
+ lines.push("\n**Done.** All steps complete.");
2177
+ } else if (nextStep && typeof nextStep === "object") {
2178
+ lines.push(`
2179
+ **Next step:** \`${nextStep.id ?? nextStep.index}\` \u2014 call \`workflow_step\` with this run id to continue.`);
2180
+ }
2181
+ return lines;
2182
+ }
1821
2183
  function formatWorkflowRun(raw, input) {
1822
2184
  const parsed = parseData(raw);
1823
2185
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -1831,6 +2193,7 @@ function formatWorkflowRun(raw, input) {
1831
2193
  `**Run ID:** \`${runId || "unknown"}\``,
1832
2194
  `**Status:** ${status}`,
1833
2195
  summary?.title ? `**Title:** ${summary.title}` : "",
2196
+ ...workflowStepLines(parsed.data),
1834
2197
  summary?.summary ? `
1835
2198
  ## Summary
1836
2199
  ${summary.summary}` : "",
@@ -1846,6 +2209,42 @@ ${workflowArtifactRows(artifacts)}` : "",
1846
2209
  input: input.input ?? {},
1847
2210
  run,
1848
2211
  summary,
2212
+ step: parsed.data.step,
2213
+ nextStep: parsed.data.nextStep ?? null,
2214
+ done: parsed.data.done === true,
2215
+ artifacts
2216
+ }
2217
+ };
2218
+ }
2219
+ function formatWorkflowStep(raw, input) {
2220
+ const parsed = parseData(raw);
2221
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
2222
+ const run = parsed.data.run;
2223
+ const summary = parsed.data.summary;
2224
+ const artifacts = workflowArtifactsFrom(run);
2225
+ const done = parsed.data.done === true;
2226
+ const full = [
2227
+ `# Workflow Step`,
2228
+ `**Run ID:** \`${input.runId}\``,
2229
+ `**Status:** ${run?.status ?? (done ? "done" : "running")}`,
2230
+ ...workflowStepLines(parsed.data),
2231
+ done && summary?.summary ? `
2232
+ ## Summary
2233
+ ${summary.summary}` : "",
2234
+ artifacts.length ? `
2235
+ ## Artifacts
2236
+ ${workflowArtifactRows(artifacts)}` : "",
2237
+ done && artifacts.length ? "\nUse `workflow_artifact_read` with the run id and artifact id to pull CSV, JSON, Markdown, or report content into context." : ""
2238
+ ].filter(Boolean).join("\n");
2239
+ return {
2240
+ ...oneBlock(full),
2241
+ structuredContent: {
2242
+ runId: input.runId,
2243
+ run,
2244
+ summary: summary ?? null,
2245
+ step: parsed.data.step,
2246
+ nextStep: parsed.data.nextStep ?? null,
2247
+ done,
1849
2248
  artifacts
1850
2249
  }
1851
2250
  };
@@ -2250,6 +2649,7 @@ function formatFacebookAdTranscribe(raw, input) {
2250
2649
  const text = d.text ?? "";
2251
2650
  const chunks = d.chunks ?? [];
2252
2651
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
2652
+ const words = wordCount(text);
2253
2653
  const chunkRows = chunks.slice(0, 50).map((c) => {
2254
2654
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
2255
2655
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -2258,7 +2658,7 @@ function formatFacebookAdTranscribe(raw, input) {
2258
2658
  }).join("\n");
2259
2659
  const full = [
2260
2660
  `# Facebook Ad Transcript`,
2261
- `**Duration:** ${durSec}s \xB7 **${text.split(" ").length} words**`,
2661
+ `**Duration:** ${durSec}s \xB7 **${words} words**`,
2262
2662
  `
2263
2663
  ## Full Transcript
2264
2664
  ${text}`,
@@ -2271,7 +2671,20 @@ ${chunkRows}` : "",
2271
2671
  ---
2272
2672
  \u{1F4A1} Get more ads from this advertiser: use \`facebook_page_intel\`. For public Facebook reel/post URLs, use \`facebook_video_transcribe\`.`
2273
2673
  ].filter(Boolean).join("\n");
2274
- return oneBlock(full);
2674
+ return {
2675
+ ...oneBlock(full),
2676
+ structuredContent: {
2677
+ videoUrl: input.videoUrl,
2678
+ wordCount: words,
2679
+ chunkCount: chunks.length,
2680
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
2681
+ transcriptText: text,
2682
+ chunks: structuredTranscriptChunks(chunks),
2683
+ resolvedInputs: {
2684
+ videoUrl: input.videoUrl
2685
+ }
2686
+ }
2687
+ };
2275
2688
  }
2276
2689
  function formatFacebookVideoTranscribe(raw, input) {
2277
2690
  const parsed = parseData(raw);
@@ -2279,7 +2692,7 @@ function formatFacebookVideoTranscribe(raw, input) {
2279
2692
  const d = parsed.data;
2280
2693
  const text = d.text ?? "";
2281
2694
  const chunks = d.chunks ?? [];
2282
- const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
2695
+ const wordCount2 = text.trim() ? text.trim().split(/\s+/).length : 0;
2283
2696
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
2284
2697
  const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
2285
2698
  const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
@@ -2292,7 +2705,7 @@ function formatFacebookVideoTranscribe(raw, input) {
2292
2705
  const full = [
2293
2706
  `# ${label} Transcript`,
2294
2707
  d.ownerName ? `**Owner:** ${d.ownerName}` : "",
2295
- `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount} words**`,
2708
+ `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
2296
2709
  d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
2297
2710
  d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
2298
2711
  `
@@ -2318,7 +2731,7 @@ ${chunkRows}` : "",
2318
2731
  bitrate: typeof d.bitrate === "number" ? d.bitrate : null,
2319
2732
  videoDurationSec: typeof d.videoDurationSec === "number" ? d.videoDurationSec : null,
2320
2733
  videoUrl: d.videoUrl ?? "",
2321
- wordCount,
2734
+ wordCount: wordCount2,
2322
2735
  chunkCount: chunks.length,
2323
2736
  transcriptText: text,
2324
2737
  chunks: chunks.map((c) => ({
@@ -2331,451 +2744,612 @@ ${chunkRows}` : "",
2331
2744
  }
2332
2745
 
2333
2746
  // src/mcp/mcp-tool-schemas.ts
2747
+ var import_zod3 = require("zod");
2748
+
2749
+ // src/schemas.ts
2334
2750
  var import_zod2 = require("zod");
2751
+ var DEFAULT_PROXY_MODE = "configured";
2752
+ var DEFAULT_MAPS_PROXY_MODE = "location";
2753
+ var HarvestOptionsSchema = import_zod2.z.object({
2754
+ query: import_zod2.z.string().min(1),
2755
+ location: import_zod2.z.string().optional(),
2756
+ gl: import_zod2.z.string().length(2).default("us"),
2757
+ hl: import_zod2.z.string().length(2).default("en"),
2758
+ device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop"),
2759
+ proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE),
2760
+ proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
2761
+ debug: import_zod2.z.boolean().default(false),
2762
+ depth: import_zod2.z.number().int().min(1).max(30).default(3),
2763
+ maxQuestions: import_zod2.z.number().int().min(1).max(1e3).default(100),
2764
+ headless: import_zod2.z.boolean().default(false),
2765
+ profileDir: import_zod2.z.string().optional(),
2766
+ proxy: import_zod2.z.string().url().optional(),
2767
+ kernelApiKey: import_zod2.z.string().optional(),
2768
+ kernelProxyId: import_zod2.z.string().optional(),
2769
+ kernelProxyResolution: import_zod2.z.unknown().optional(),
2770
+ outputDir: import_zod2.z.string().default("./paa-output"),
2771
+ format: import_zod2.z.enum(["json", "csv", "both"]).default("both"),
2772
+ serpOnly: import_zod2.z.boolean().default(false),
2773
+ pages: import_zod2.z.number().int().min(1).max(2).default(1)
2774
+ });
2775
+ var MapsPlaceOptionsSchema = import_zod2.z.object({
2776
+ businessName: import_zod2.z.string().min(1),
2777
+ location: import_zod2.z.string().min(1),
2778
+ gl: import_zod2.z.string().length(2).default("us"),
2779
+ hl: import_zod2.z.string().length(2).default("en"),
2780
+ includeReviews: import_zod2.z.boolean().default(false),
2781
+ maxReviews: import_zod2.z.number().int().min(1).max(500).default(50),
2782
+ kernelApiKey: import_zod2.z.string().optional(),
2783
+ kernelProxyId: import_zod2.z.string().optional(),
2784
+ headless: import_zod2.z.boolean().default(true)
2785
+ });
2786
+ var MapsSearchOptionsSchema = import_zod2.z.object({
2787
+ query: import_zod2.z.string().min(1),
2788
+ location: import_zod2.z.string().optional(),
2789
+ gl: import_zod2.z.string().length(2).default("us"),
2790
+ hl: import_zod2.z.string().length(2).default("en"),
2791
+ maxResults: import_zod2.z.number().int().min(1).max(50).default(10),
2792
+ proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
2793
+ proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
2794
+ debug: import_zod2.z.boolean().default(false),
2795
+ kernelApiKey: import_zod2.z.string().optional(),
2796
+ kernelProxyId: import_zod2.z.string().optional(),
2797
+ kernelProxyResolution: import_zod2.z.unknown().optional(),
2798
+ headless: import_zod2.z.boolean().default(true)
2799
+ });
2800
+ var RawPAAItemSchema = import_zod2.z.object({
2801
+ question: import_zod2.z.string().min(1),
2802
+ answer: import_zod2.z.string().optional(),
2803
+ sourceTitle: import_zod2.z.string().optional(),
2804
+ sourceSite: import_zod2.z.string().optional(),
2805
+ sourceCite: import_zod2.z.string().optional()
2806
+ });
2807
+ var RawMapsOverviewSchema = import_zod2.z.object({
2808
+ name: import_zod2.z.string().nullable(),
2809
+ rating: import_zod2.z.string().nullable(),
2810
+ reviewCount: import_zod2.z.string().nullable(),
2811
+ category: import_zod2.z.string().nullable(),
2812
+ address: import_zod2.z.string().nullable(),
2813
+ hoursSummary: import_zod2.z.string().nullable(),
2814
+ phone: import_zod2.z.string().nullable(),
2815
+ phoneDisplay: import_zod2.z.string().nullable(),
2816
+ website: import_zod2.z.string().nullable(),
2817
+ plusCode: import_zod2.z.string().nullable(),
2818
+ bookingUrl: import_zod2.z.string().nullable()
2819
+ });
2820
+ var RawMapsHoursRowSchema = import_zod2.z.object({
2821
+ day: import_zod2.z.string(),
2822
+ hours: import_zod2.z.string()
2823
+ });
2824
+ var RawMapsReviewStatsSchema = import_zod2.z.object({
2825
+ reviewHistogram: import_zod2.z.array(import_zod2.z.object({
2826
+ stars: import_zod2.z.number(),
2827
+ count: import_zod2.z.string()
2828
+ })),
2829
+ reviewTopics: import_zod2.z.array(import_zod2.z.object({
2830
+ label: import_zod2.z.string(),
2831
+ count: import_zod2.z.string()
2832
+ }))
2833
+ });
2834
+ var RawMapsReviewCardSchema = import_zod2.z.object({
2835
+ reviewId: import_zod2.z.string(),
2836
+ author: import_zod2.z.string().nullable(),
2837
+ stars: import_zod2.z.string().nullable(),
2838
+ date: import_zod2.z.string().nullable(),
2839
+ text: import_zod2.z.string().nullable(),
2840
+ ownerResponse: import_zod2.z.string().nullable()
2841
+ });
2842
+ var RawMapsAboutAttributeSchema = import_zod2.z.object({
2843
+ section: import_zod2.z.string(),
2844
+ attribute: import_zod2.z.string()
2845
+ });
2846
+
2847
+ // src/mcp/mcp-tool-schemas.ts
2335
2848
  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.")
2849
+ 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.'),
2850
+ 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".'),
2851
+ 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."),
2852
+ 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."),
2853
+ 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."),
2854
+ 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."),
2855
+ 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."),
2856
+ 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."),
2857
+ 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
2858
  };
2346
2859
  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.")
2860
+ url: import_zod3.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
2861
+ 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."),
2862
+ screenshotDevice: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
2863
+ 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."),
2864
+ 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."),
2865
+ 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."),
2866
+ allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
2354
2867
  };
2355
2868
  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.")
2869
+ 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."),
2870
+ 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
2871
  };
2359
2872
  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.")
2873
+ 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."),
2874
+ 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
2875
  };
2363
2876
  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.")
2877
+ 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."),
2878
+ query: import_zod3.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
2879
+ channelHandle: import_zod3.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
2880
+ 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
2881
  };
2369
2882
  var YoutubeTranscribeInputSchema = {
2370
- videoId: import_zod2.z.string().min(1).describe("YouTube video ID, e.g. dQw4w9WgXcQ")
2883
+ 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."),
2884
+ 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
2885
  };
2372
2886
  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")
2887
+ 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."),
2888
+ 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."),
2889
+ 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."),
2890
+ 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."),
2891
+ 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
2892
  };
2379
2893
  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)
2894
+ query: import_zod3.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
2895
+ country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
2896
+ 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
2897
  };
2384
2898
  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.")
2899
+ 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
2900
  };
2387
2901
  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.")
2902
+ 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."),
2903
+ 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
2904
  };
2391
2905
  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)")
2906
+ 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".'),
2907
+ 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.'),
2908
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
2909
+ hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
2910
+ 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."),
2911
+ 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
2912
  };
2399
2913
  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.")
2914
+ 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.'),
2915
+ 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.'),
2916
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
2917
+ hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
2918
+ 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."),
2919
+ 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."),
2920
+ 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."),
2921
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
2408
2922
  };
2409
2923
  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"]);
2924
+ 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."),
2925
+ 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."),
2926
+ 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".'),
2927
+ 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."),
2928
+ maxCities: import_zod3.z.number().int().min(1).max(100).default(25).describe("Maximum number of markets to process after sorting by population descending."),
2929
+ 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."),
2930
+ 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."),
2931
+ 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."),
2932
+ 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."),
2933
+ 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."),
2934
+ 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."),
2935
+ 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."),
2936
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
2937
+ };
2938
+ var RankTrackerModeSchema = import_zod3.z.enum(["maps", "organic", "ai_overview", "paa"]);
2425
2939
  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
2940
+ projectName: import_zod3.z.string().min(1).optional().describe("Optional name for the rank tracker project, client, or campaign."),
2941
+ 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."),
2942
+ 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."),
2943
+ 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."),
2944
+ 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."),
2945
+ 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."),
2946
+ 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."),
2947
+ database: import_zod3.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family the downstream AI should target when generating migrations."),
2948
+ scheduleCadence: import_zod3.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence for the generated cron/heartbeat plan."),
2949
+ customCron: import_zod3.z.string().min(1).optional().describe("Cron expression to use when scheduleCadence is custom."),
2950
+ timezone: import_zod3.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks, e.g. America/Denver."),
2951
+ includeCron: import_zod3.z.boolean().default(true).describe("Include a cron or heartbeat worker plan. Keep true for production rank trackers."),
2952
+ includeDashboard: import_zod3.z.boolean().default(true).describe("Include dashboard/reporting requirements in the generated prompt."),
2953
+ includeAlerts: import_zod3.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature gains/losses."),
2954
+ notes: import_zod3.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
2955
+ };
2956
+ var NullableString2 = import_zod3.z.string().nullable();
2957
+ var MapsSearchAttemptOutput = import_zod3.z.object({
2958
+ attemptNumber: import_zod3.z.number().int().min(1),
2959
+ maxAttempts: import_zod3.z.number().int().min(1),
2960
+ status: import_zod3.z.enum(["ok", "failed"]),
2961
+ outcome: import_zod3.z.string(),
2962
+ willRetry: import_zod3.z.boolean(),
2963
+ durationMs: import_zod3.z.number().int().min(0),
2964
+ resultCount: import_zod3.z.number().int().min(0),
2965
+ error: NullableString2,
2966
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]),
2967
+ proxyResolutionSource: import_zod3.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
2968
+ proxyIdSuffix: NullableString2,
2969
+ proxyTargetLevel: import_zod3.z.enum(["zip", "city", "state"]).nullable(),
2970
+ proxyTargetLocation: NullableString2,
2971
+ proxyTargetZip: NullableString2,
2972
+ browserSessionIdSuffix: NullableString2,
2973
+ observedIp: NullableString2,
2974
+ observedCity: NullableString2,
2975
+ observedRegion: NullableString2
2462
2976
  });
2463
2977
  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())
2978
+ query: import_zod3.z.string(),
2979
+ location: import_zod3.z.string().nullable(),
2980
+ searchQuery: import_zod3.z.string(),
2981
+ searchUrl: import_zod3.z.string().url(),
2982
+ extractedAt: import_zod3.z.string(),
2983
+ requestedMaxResults: import_zod3.z.number().int().min(1).max(50),
2984
+ resultCount: import_zod3.z.number().int().min(0).max(50),
2985
+ results: import_zod3.z.array(import_zod3.z.object({
2986
+ position: import_zod3.z.number().int().min(1),
2987
+ name: import_zod3.z.string(),
2988
+ placeUrl: import_zod3.z.string().url(),
2989
+ cid: NullableString2,
2990
+ cidDecimal: NullableString2,
2991
+ rating: NullableString2,
2992
+ reviewCount: NullableString2,
2993
+ category: NullableString2,
2994
+ address: NullableString2,
2995
+ phone: NullableString2,
2996
+ hoursStatus: NullableString2,
2997
+ websiteUrl: NullableString2,
2998
+ directionsUrl: NullableString2,
2999
+ metadata: import_zod3.z.array(import_zod3.z.string())
2486
3000
  })),
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())
3001
+ attempts: import_zod3.z.array(MapsSearchAttemptOutput),
3002
+ durationMs: import_zod3.z.number().int().min(0)
3003
+ };
3004
+ var DirectoryMapsBusinessOutput = import_zod3.z.object({
3005
+ position: import_zod3.z.number().int().min(1),
3006
+ name: import_zod3.z.string(),
3007
+ placeUrl: import_zod3.z.string().url(),
3008
+ cid: NullableString2,
3009
+ cidDecimal: NullableString2,
3010
+ rating: NullableString2,
3011
+ reviewCount: NullableString2,
3012
+ category: NullableString2,
3013
+ address: NullableString2,
3014
+ phone: NullableString2,
3015
+ hoursStatus: NullableString2,
3016
+ websiteUrl: NullableString2,
3017
+ directionsUrl: NullableString2,
3018
+ metadata: import_zod3.z.array(import_zod3.z.string())
2505
3019
  });
2506
3020
  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)
3021
+ query: import_zod3.z.string(),
3022
+ state: import_zod3.z.string(),
3023
+ minPopulation: import_zod3.z.number().int().min(0),
3024
+ populationYear: import_zod3.z.number().int().min(2020).max(2025),
3025
+ maxResultsPerCity: import_zod3.z.number().int().min(1).max(50),
3026
+ concurrency: import_zod3.z.number().int().min(1).max(5),
3027
+ censusSourceUrl: import_zod3.z.string().url(),
3028
+ usZipsSourcePath: NullableString2,
3029
+ warnings: import_zod3.z.array(import_zod3.z.string()),
3030
+ extractedAt: import_zod3.z.string(),
3031
+ selectedCityCount: import_zod3.z.number().int().min(0),
3032
+ totalResultCount: import_zod3.z.number().int().min(0),
3033
+ csvPath: NullableString2,
3034
+ cities: import_zod3.z.array(import_zod3.z.object({
3035
+ city: import_zod3.z.string(),
3036
+ state: import_zod3.z.string(),
3037
+ location: import_zod3.z.string(),
3038
+ cityKey: import_zod3.z.string(),
3039
+ censusName: import_zod3.z.string(),
3040
+ population: import_zod3.z.number().int().min(0),
3041
+ populationYear: import_zod3.z.number().int().min(2020).max(2025),
3042
+ zips: import_zod3.z.array(import_zod3.z.string()),
3043
+ counties: import_zod3.z.array(import_zod3.z.string()),
3044
+ status: import_zod3.z.enum(["ok", "empty", "failed"]),
3045
+ error: NullableString2,
3046
+ resultCount: import_zod3.z.number().int().min(0),
3047
+ durationMs: import_zod3.z.number().int().min(0),
3048
+ attempts: import_zod3.z.array(MapsSearchAttemptOutput),
3049
+ results: import_zod3.z.array(DirectoryMapsBusinessOutput)
2536
3050
  })),
2537
- durationMs: import_zod2.z.number().int().min(0)
3051
+ durationMs: import_zod3.z.number().int().min(0)
2538
3052
  };
2539
- var RankTrackerToolPlanOutput = import_zod2.z.object({
2540
- tool: import_zod2.z.string(),
2541
- purpose: import_zod2.z.string()
3053
+ var RankTrackerToolPlanOutput = import_zod3.z.object({
3054
+ tool: import_zod3.z.string(),
3055
+ purpose: import_zod3.z.string()
2542
3056
  });
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())
3057
+ var RankTrackerTableOutput = import_zod3.z.object({
3058
+ name: import_zod3.z.string(),
3059
+ purpose: import_zod3.z.string(),
3060
+ keyColumns: import_zod3.z.array(import_zod3.z.string())
2547
3061
  });
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())
3062
+ var RankTrackerCronJobOutput = import_zod3.z.object({
3063
+ name: import_zod3.z.string(),
3064
+ purpose: import_zod3.z.string(),
3065
+ modes: import_zod3.z.array(RankTrackerModeSchema),
3066
+ recommendedTools: import_zod3.z.array(import_zod3.z.string())
2553
3067
  });
2554
3068
  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)
3069
+ projectName: import_zod3.z.string(),
3070
+ targetDomain: NullableString2,
3071
+ targetBusinessName: NullableString2,
3072
+ trackingModes: import_zod3.z.array(RankTrackerModeSchema),
3073
+ database: import_zod3.z.string(),
3074
+ recommendedTools: import_zod3.z.array(RankTrackerToolPlanOutput),
3075
+ tables: import_zod3.z.array(RankTrackerTableOutput),
3076
+ cron: import_zod3.z.object({
3077
+ enabled: import_zod3.z.boolean(),
3078
+ cadence: import_zod3.z.string(),
3079
+ expression: import_zod3.z.string(),
3080
+ timezone: import_zod3.z.string(),
3081
+ jobs: import_zod3.z.array(RankTrackerCronJobOutput)
2568
3082
  }),
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
3083
+ metrics: import_zod3.z.array(import_zod3.z.string()),
3084
+ implementationPrompt: import_zod3.z.string()
3085
+ };
3086
+ var OrganicResultOutput = import_zod3.z.object({
3087
+ position: import_zod3.z.number().int(),
3088
+ title: import_zod3.z.string(),
3089
+ url: import_zod3.z.string(),
3090
+ domain: import_zod3.z.string(),
3091
+ snippet: NullableString2
2578
3092
  });
2579
- var AiOverviewOutput = import_zod2.z.object({
2580
- detected: import_zod2.z.boolean(),
2581
- text: NullableString
3093
+ var AiOverviewOutput = import_zod3.z.object({
3094
+ detected: import_zod3.z.boolean(),
3095
+ text: NullableString2
2582
3096
  }).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())
3097
+ var EntityIdsOutput = import_zod3.z.object({
3098
+ kgIds: import_zod3.z.array(import_zod3.z.string()),
3099
+ cids: import_zod3.z.array(import_zod3.z.string()),
3100
+ gcids: import_zod3.z.array(import_zod3.z.string())
2587
3101
  }).nullable();
2588
3102
  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
3103
+ query: import_zod3.z.string(),
3104
+ location: NullableString2,
3105
+ questionCount: import_zod3.z.number().int().min(0),
3106
+ completionStatus: NullableString2,
3107
+ questions: import_zod3.z.array(import_zod3.z.object({
3108
+ question: import_zod3.z.string(),
3109
+ answer: NullableString2,
3110
+ sourceTitle: NullableString2,
3111
+ sourceSite: NullableString2
2598
3112
  })),
2599
- organicResults: import_zod2.z.array(OrganicResultOutput),
3113
+ organicResults: import_zod3.z.array(OrganicResultOutput),
2600
3114
  aiOverview: AiOverviewOutput,
2601
3115
  entityIds: EntityIdsOutput,
2602
- durationMs: import_zod2.z.number().min(0).nullable()
3116
+ durationMs: import_zod3.z.number().min(0).nullable()
2603
3117
  };
2604
3118
  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
3119
+ query: import_zod3.z.string(),
3120
+ location: NullableString2,
3121
+ organicResults: import_zod3.z.array(OrganicResultOutput),
3122
+ localPack: import_zod3.z.array(import_zod3.z.object({
3123
+ position: import_zod3.z.number().int(),
3124
+ name: import_zod3.z.string(),
3125
+ rating: NullableString2,
3126
+ reviewCount: NullableString2,
3127
+ websiteUrl: NullableString2
2614
3128
  })),
2615
3129
  aiOverview: AiOverviewOutput,
2616
3130
  entityIds: EntityIdsOutput
2617
3131
  };
2618
3132
  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()
3133
+ url: import_zod3.z.string(),
3134
+ title: NullableString2,
3135
+ headings: import_zod3.z.array(import_zod3.z.object({
3136
+ level: import_zod3.z.number().int(),
3137
+ text: import_zod3.z.string()
2624
3138
  })),
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
3139
+ schemaBlockCount: import_zod3.z.number().int().min(0),
3140
+ entityName: NullableString2,
3141
+ entityTypes: import_zod3.z.array(import_zod3.z.string()),
3142
+ napScore: import_zod3.z.number().nullable(),
3143
+ missingSchemaFields: import_zod3.z.array(import_zod3.z.string()),
3144
+ screenshotSaved: NullableString2
2631
3145
  };
2632
3146
  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())
3147
+ url: import_zod3.z.string(),
3148
+ pageCount: import_zod3.z.number().int().min(0),
3149
+ pages: import_zod3.z.array(import_zod3.z.object({
3150
+ url: import_zod3.z.string(),
3151
+ title: NullableString2,
3152
+ schemaTypes: import_zod3.z.array(import_zod3.z.string())
2639
3153
  })),
2640
- durationMs: import_zod2.z.number().min(0)
3154
+ durationMs: import_zod3.z.number().min(0)
2641
3155
  };
2642
3156
  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()
3157
+ name: import_zod3.z.string(),
3158
+ rating: NullableString2,
3159
+ reviewCount: NullableString2,
3160
+ category: NullableString2,
3161
+ address: NullableString2,
3162
+ phone: NullableString2,
3163
+ website: NullableString2,
3164
+ hoursSummary: NullableString2,
3165
+ bookingUrl: NullableString2,
3166
+ kgmid: NullableString2,
3167
+ cidDecimal: NullableString2,
3168
+ cidUrl: NullableString2,
3169
+ lat: import_zod3.z.number().nullable(),
3170
+ lng: import_zod3.z.number().nullable(),
3171
+ reviewsStatus: import_zod3.z.string(),
3172
+ reviewsCollected: import_zod3.z.number().int().min(0),
3173
+ reviewTopics: import_zod3.z.array(import_zod3.z.object({
3174
+ label: import_zod3.z.string(),
3175
+ count: import_zod3.z.string()
2662
3176
  }))
2663
3177
  };
2664
3178
  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
3179
+ balanceCredits: import_zod3.z.number().nullable(),
3180
+ matchedCost: import_zod3.z.object({
3181
+ label: import_zod3.z.string(),
3182
+ credits: import_zod3.z.number(),
3183
+ unit: import_zod3.z.string(),
3184
+ notes: NullableString2
2671
3185
  }).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
3186
+ costs: import_zod3.z.array(import_zod3.z.object({
3187
+ key: import_zod3.z.string(),
3188
+ label: import_zod3.z.string(),
3189
+ credits: import_zod3.z.number(),
3190
+ unit: import_zod3.z.string(),
3191
+ notes: NullableString2
2678
3192
  })),
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
3193
+ ledger: import_zod3.z.array(import_zod3.z.object({
3194
+ createdAt: import_zod3.z.string(),
3195
+ operation: import_zod3.z.string(),
3196
+ credits: import_zod3.z.number(),
3197
+ description: NullableString2
2684
3198
  })),
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()
3199
+ concurrency: import_zod3.z.object({
3200
+ currentExtraSlots: import_zod3.z.number().int().min(0),
3201
+ currentLimit: import_zod3.z.number().int().min(1),
3202
+ hasSubscription: import_zod3.z.boolean(),
3203
+ upgrade: import_zod3.z.object({
3204
+ product: import_zod3.z.string(),
3205
+ priceLabel: import_zod3.z.string(),
3206
+ unitAmountUsd: import_zod3.z.number(),
3207
+ currency: import_zod3.z.string(),
3208
+ interval: import_zod3.z.string(),
3209
+ billingUrl: import_zod3.z.string().url(),
3210
+ terminalCommand: import_zod3.z.string(),
3211
+ terminalCommandWithApiKeyEnv: import_zod3.z.string()
2698
3212
  })
2699
3213
  }).nullable()
2700
3214
  };
2701
3215
  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()
3216
+ startUrl: import_zod3.z.string(),
3217
+ totalFound: import_zod3.z.number().int().min(0),
3218
+ truncated: import_zod3.z.boolean(),
3219
+ okCount: import_zod3.z.number().int().min(0),
3220
+ redirectCount: import_zod3.z.number().int().min(0),
3221
+ brokenCount: import_zod3.z.number().int().min(0),
3222
+ urls: import_zod3.z.array(import_zod3.z.object({
3223
+ url: import_zod3.z.string(),
3224
+ status: import_zod3.z.number().int().nullable()
2711
3225
  })),
2712
- durationMs: import_zod2.z.number().min(0)
3226
+ durationMs: import_zod3.z.number().min(0)
2713
3227
  };
2714
3228
  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
3229
+ mode: import_zod3.z.string(),
3230
+ videoCount: import_zod3.z.number().int().min(0),
3231
+ channel: import_zod3.z.object({
3232
+ title: NullableString2,
3233
+ subscriberCount: NullableString2
2720
3234
  }).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
3235
+ videos: import_zod3.z.array(import_zod3.z.object({
3236
+ videoId: import_zod3.z.string(),
3237
+ title: import_zod3.z.string(),
3238
+ channelName: NullableString2,
3239
+ views: NullableString2,
3240
+ duration: NullableString2,
3241
+ url: NullableString2
2728
3242
  }))
2729
3243
  };
2730
3244
  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
3245
+ query: import_zod3.z.string(),
3246
+ advertiserCount: import_zod3.z.number().int().min(0),
3247
+ advertisers: import_zod3.z.array(import_zod3.z.object({
3248
+ name: NullableString2,
3249
+ pageId: NullableString2,
3250
+ pageUrl: NullableString2,
3251
+ adCount: import_zod3.z.number().int().nullable(),
3252
+ libraryId: NullableString2,
3253
+ sampleLibraryId: NullableString2
2737
3254
  }))
2738
3255
  };
2739
3256
  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()
3257
+ advertiserName: NullableString2,
3258
+ totalAds: import_zod3.z.number().int().min(0),
3259
+ activeCount: import_zod3.z.number().int().min(0),
3260
+ videoCount: import_zod3.z.number().int().min(0),
3261
+ imageCount: import_zod3.z.number().int().min(0),
3262
+ ads: import_zod3.z.array(import_zod3.z.object({
3263
+ libraryId: NullableString2,
3264
+ status: NullableString2,
3265
+ creativeType: NullableString2,
3266
+ primaryText: NullableString2,
3267
+ headline: NullableString2,
3268
+ cta: NullableString2,
3269
+ startDate: NullableString2,
3270
+ landingUrl: NullableString2,
3271
+ domain: NullableString2,
3272
+ videoUrl: NullableString2,
3273
+ imageUrl: NullableString2,
3274
+ videoPoster: NullableString2,
3275
+ variations: import_zod3.z.number().int().nullable()
2754
3276
  }))
2755
3277
  };
2756
3278
  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()
3279
+ sourceUrl: import_zod3.z.string().url(),
3280
+ pageUrl: import_zod3.z.string().url(),
3281
+ videoId: NullableString2,
3282
+ ownerName: NullableString2,
3283
+ selectedQuality: import_zod3.z.string(),
3284
+ bitrate: import_zod3.z.number().int().nullable(),
3285
+ videoDurationSec: import_zod3.z.number().nullable(),
3286
+ videoUrl: import_zod3.z.string().url(),
3287
+ wordCount: import_zod3.z.number().int().min(0),
3288
+ chunkCount: import_zod3.z.number().int().min(0),
3289
+ transcriptText: import_zod3.z.string(),
3290
+ chunks: import_zod3.z.array(import_zod3.z.object({
3291
+ startSec: import_zod3.z.number(),
3292
+ endSec: import_zod3.z.number(),
3293
+ text: import_zod3.z.string()
2772
3294
  }))
2773
3295
  };
3296
+ var TranscriptChunkOutput = import_zod3.z.object({
3297
+ startSec: import_zod3.z.number(),
3298
+ endSec: import_zod3.z.number(),
3299
+ text: import_zod3.z.string()
3300
+ });
3301
+ var YoutubeTranscribeOutputSchema = {
3302
+ videoId: NullableString2,
3303
+ url: NullableString2,
3304
+ wordCount: import_zod3.z.number().int().min(0),
3305
+ chunkCount: import_zod3.z.number().int().min(0),
3306
+ durationMs: import_zod3.z.number().nullable(),
3307
+ transcriptText: import_zod3.z.string(),
3308
+ chunks: import_zod3.z.array(TranscriptChunkOutput),
3309
+ resolvedInputs: import_zod3.z.object({
3310
+ videoId: NullableString2,
3311
+ url: NullableString2
3312
+ })
3313
+ };
3314
+ var FacebookAdTranscribeOutputSchema = {
3315
+ videoUrl: import_zod3.z.string().url(),
3316
+ wordCount: import_zod3.z.number().int().min(0),
3317
+ chunkCount: import_zod3.z.number().int().min(0),
3318
+ durationMs: import_zod3.z.number().nullable(),
3319
+ transcriptText: import_zod3.z.string(),
3320
+ chunks: import_zod3.z.array(TranscriptChunkOutput),
3321
+ resolvedInputs: import_zod3.z.object({
3322
+ videoUrl: import_zod3.z.string().url()
3323
+ })
3324
+ };
3325
+ var CaptureSerpSnapshotOutputSchema = {
3326
+ schemaVersion: import_zod3.z.literal("serp-intelligence.capture.v1"),
3327
+ status: import_zod3.z.string(),
3328
+ query: NullableString2,
3329
+ location: NullableString2,
3330
+ capturedAt: NullableString2,
3331
+ resultCount: import_zod3.z.number().int().min(0).nullable(),
3332
+ snapshotId: NullableString2,
3333
+ resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
3334
+ artifacts: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
3335
+ diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
3336
+ providerPayload: import_zod3.z.record(import_zod3.z.unknown())
3337
+ };
3338
+ var CaptureSerpPageSnapshotsOutputSchema = {
3339
+ schemaVersion: import_zod3.z.literal("serp-intelligence.page-snapshots.v1"),
3340
+ status: import_zod3.z.string(),
3341
+ count: import_zod3.z.number().int().min(0),
3342
+ failedCount: import_zod3.z.number().int().min(0),
3343
+ captures: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
3344
+ resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
3345
+ diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
3346
+ providerPayload: import_zod3.z.record(import_zod3.z.unknown())
3347
+ };
2774
3348
  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")
3349
+ 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"'),
3350
+ includeLedger: import_zod3.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
2777
3351
  };
2778
- var WorkflowIdSchema = import_zod2.z.enum([
3352
+ var WorkflowIdSchema = import_zod3.z.enum([
2779
3353
  "directory",
2780
3354
  "agent-packet",
2781
3355
  "local-competitive-audit",
@@ -2785,114 +3359,129 @@ var WorkflowIdSchema = import_zod2.z.enum([
2785
3359
  "ai-overview-language"
2786
3360
  ]);
2787
3361
  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.")
3362
+ 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
3363
  };
2790
3364
  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.")
3365
+ 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".'),
3366
+ query: import_zod3.z.string().optional().describe("Business category, niche, or Maps query when known."),
3367
+ keyword: import_zod3.z.string().optional().describe("Search keyword, audience problem, or content topic when known."),
3368
+ domain: import_zod3.z.string().optional().describe("Target domain or brand domain when known."),
3369
+ url: import_zod3.z.string().url().optional().describe("Target URL when the workflow should inspect a specific page."),
3370
+ location: import_zod3.z.string().optional().describe("City/region/country for localized research, e.g. Denver, CO."),
3371
+ state: import_zod3.z.string().optional().describe("US state abbreviation or name for state-wide market research."),
3372
+ maxSuggestions: import_zod3.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
2799
3373
  };
2800
3374
  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.")
3375
+ 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."),
3376
+ 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?}."),
3377
+ webhookUrl: import_zod3.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
3378
+ };
3379
+ var WorkflowStepInputSchema = {
3380
+ 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
3381
  };
2805
3382
  var WorkflowStatusInputSchema = {
2806
- runId: import_zod2.z.string().min(1).describe("Workflow run id returned by workflow_run.")
3383
+ 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
3384
  };
2808
3385
  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()
3386
+ 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."),
3387
+ 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."),
3388
+ 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.")
3389
+ };
3390
+ var WorkflowRecipeOutput = import_zod3.z.object({
3391
+ id: import_zod3.z.string(),
3392
+ title: import_zod3.z.string(),
3393
+ description: import_zod3.z.string(),
3394
+ primaryWorkflowId: import_zod3.z.string().nullable(),
3395
+ recommendedTools: import_zod3.z.array(import_zod3.z.string()),
3396
+ requiredInputs: import_zod3.z.array(import_zod3.z.string()),
3397
+ optionalInputs: import_zod3.z.array(import_zod3.z.string()),
3398
+ produces: import_zod3.z.array(import_zod3.z.string()),
3399
+ runHint: import_zod3.z.string()
2823
3400
  });
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()
3401
+ var WorkflowDefinitionOutput = import_zod3.z.object({
3402
+ id: import_zod3.z.string(),
3403
+ title: import_zod3.z.string(),
3404
+ description: import_zod3.z.string()
2828
3405
  });
2829
- var WorkflowArtifactOutput = import_zod2.z.record(import_zod2.z.unknown());
3406
+ var WorkflowArtifactOutput = import_zod3.z.record(import_zod3.z.unknown());
2830
3407
  var WorkflowListOutputSchema = {
2831
- workflows: import_zod2.z.array(WorkflowDefinitionOutput),
2832
- recipes: import_zod2.z.array(WorkflowRecipeOutput)
3408
+ workflows: import_zod3.z.array(WorkflowDefinitionOutput),
3409
+ recipes: import_zod3.z.array(WorkflowRecipeOutput)
2833
3410
  };
2834
3411
  var WorkflowSuggestOutputSchema = {
2835
- goal: import_zod2.z.string(),
2836
- suggestions: import_zod2.z.array(WorkflowRecipeOutput)
3412
+ goal: import_zod3.z.string(),
3413
+ suggestions: import_zod3.z.array(WorkflowRecipeOutput)
2837
3414
  };
2838
3415
  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)
3416
+ workflowId: import_zod3.z.string(),
3417
+ input: import_zod3.z.record(import_zod3.z.unknown()),
3418
+ run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
3419
+ summary: import_zod3.z.record(import_zod3.z.unknown()).optional(),
3420
+ step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
3421
+ nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
3422
+ done: import_zod3.z.boolean().optional(),
3423
+ artifacts: import_zod3.z.array(WorkflowArtifactOutput)
3424
+ };
3425
+ var WorkflowStepOutputSchema = {
3426
+ runId: import_zod3.z.string(),
3427
+ run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
3428
+ summary: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
3429
+ step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
3430
+ nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
3431
+ done: import_zod3.z.boolean(),
3432
+ artifacts: import_zod3.z.array(WorkflowArtifactOutput)
2844
3433
  };
2845
3434
  var WorkflowStatusOutputSchema = {
2846
- run: import_zod2.z.record(import_zod2.z.unknown()).optional(),
2847
- artifacts: import_zod2.z.array(WorkflowArtifactOutput)
3435
+ run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
3436
+ artifacts: import_zod3.z.array(WorkflowArtifactOutput)
2848
3437
  };
2849
3438
  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()
3439
+ runId: import_zod3.z.string(),
3440
+ artifactId: import_zod3.z.string(),
3441
+ contentType: import_zod3.z.string(),
3442
+ bytes: import_zod3.z.number().int().min(0),
3443
+ truncated: import_zod3.z.boolean(),
3444
+ text: import_zod3.z.string()
2856
3445
  };
2857
3446
  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)")
3447
+ 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".'),
3448
+ location: import_zod3.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
3449
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
3450
+ hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from user request."),
3451
+ 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."),
3452
+ 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."),
3453
+ 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."),
3454
+ 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."),
3455
+ pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
2867
3456
  };
2868
3457
  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.")
3458
+ 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."),
3459
+ 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."),
3460
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
3461
+ hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
3462
+ 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."),
3463
+ 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."),
3464
+ 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."),
3465
+ 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."),
3466
+ 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."),
3467
+ includePageSnapshots: import_zod3.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
3468
+ 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
3469
  };
2881
3470
  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.")
3471
+ 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."),
3472
+ 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."),
3473
+ 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
3474
  };
2886
3475
  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.")
3476
+ 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."),
3477
+ targets: import_zod3.z.array(import_zod3.z.object({
3478
+ url: import_zod3.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
3479
+ 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."),
3480
+ sourcePosition: import_zod3.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
2892
3481
  }).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.")
3482
+ 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."),
3483
+ 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."),
3484
+ 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
3485
  };
2897
3486
 
2898
3487
  // src/mcp/rank-tracker-blueprint.ts
@@ -3278,70 +3867,72 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
3278
3867
  if (savesReports) registerSavedReportResources(server2);
3279
3868
  server2.registerTool("harvest_paa", {
3280
3869
  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.'),
3870
+ 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
3871
  inputSchema: HarvestPaaInputSchema,
3283
3872
  outputSchema: HarvestPaaOutputSchema,
3284
3873
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
3285
3874
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
3286
3875
  server2.registerTool("search_serp", {
3287
3876
  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."),
3877
+ 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
3878
  inputSchema: SearchSerpInputSchema,
3290
3879
  outputSchema: SearchSerpOutputSchema,
3291
3880
  annotations: liveWebToolAnnotations("Google SERP Lookup")
3292
3881
  }, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
3293
3882
  server2.registerTool("extract_url", {
3294
3883
  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."),
3884
+ 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
3885
  inputSchema: ExtractUrlInputSchema,
3297
3886
  outputSchema: ExtractUrlOutputSchema,
3298
3887
  annotations: liveWebToolAnnotations("Single URL Extract")
3299
3888
  }, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
3300
3889
  server2.registerTool("map_site_urls", {
3301
3890
  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."),
3891
+ 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
3892
  inputSchema: MapSiteUrlsInputSchema,
3304
3893
  outputSchema: MapSiteUrlsOutputSchema,
3305
3894
  annotations: liveWebToolAnnotations("Site URL Map")
3306
3895
  }, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
3307
3896
  server2.registerTool("extract_site", {
3308
3897
  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."),
3898
+ 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
3899
  inputSchema: ExtractSiteInputSchema,
3311
3900
  outputSchema: ExtractSiteOutputSchema,
3312
3901
  annotations: liveWebToolAnnotations("Multi-Page Site Extract")
3313
3902
  }, async (input) => formatExtractSite(await executor.extractSite(input), input));
3314
3903
  server2.registerTool("youtube_harvest", {
3315
3904
  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.'),
3905
+ 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
3906
  inputSchema: YoutubeHarvestInputSchema,
3318
3907
  outputSchema: YoutubeHarvestOutputSchema,
3319
3908
  annotations: liveWebToolAnnotations("YouTube Video Harvest")
3320
3909
  }, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
3321
3910
  server2.registerTool("youtube_transcribe", {
3322
3911
  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."),
3912
+ 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
3913
  inputSchema: YoutubeTranscribeInputSchema,
3914
+ outputSchema: YoutubeTranscribeOutputSchema,
3325
3915
  annotations: liveWebToolAnnotations("YouTube Transcription")
3326
3916
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
3327
3917
  server2.registerTool("facebook_page_intel", {
3328
3918
  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."),
3919
+ 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
3920
  inputSchema: FacebookPageIntelInputSchema,
3331
3921
  outputSchema: FacebookPageIntelOutputSchema,
3332
3922
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
3333
3923
  }, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
3334
3924
  server2.registerTool("facebook_ad_search", {
3335
3925
  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."),
3926
+ 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
3927
  inputSchema: FacebookAdSearchInputSchema,
3338
3928
  outputSchema: FacebookAdSearchOutputSchema,
3339
3929
  annotations: liveWebToolAnnotations("Facebook Ad Library Search")
3340
3930
  }, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
3341
3931
  server2.registerTool("facebook_ad_transcribe", {
3342
3932
  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.",
3933
+ 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
3934
  inputSchema: FacebookAdTranscribeInputSchema,
3935
+ outputSchema: FacebookAdTranscribeOutputSchema,
3345
3936
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
3346
3937
  }, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
3347
3938
  server2.registerTool("facebook_video_transcribe", {
@@ -3360,14 +3951,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
3360
3951
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
3361
3952
  server2.registerTool("maps_search", {
3362
3953
  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.'),
3954
+ 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
3955
  inputSchema: MapsSearchInputSchema,
3365
3956
  outputSchema: MapsSearchOutputSchema,
3366
3957
  annotations: liveWebToolAnnotations("Google Maps Business Search")
3367
3958
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
3368
3959
  server2.registerTool("directory_workflow", {
3369
3960
  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.'),
3961
+ 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
3962
  inputSchema: DirectoryWorkflowInputSchema,
3372
3963
  outputSchema: DirectoryWorkflowOutputSchema,
3373
3964
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
@@ -3388,21 +3979,28 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
3388
3979
  }, async (input) => formatWorkflowSuggest(input));
3389
3980
  server2.registerTool("workflow_run", {
3390
3981
  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."),
3982
+ 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
3983
  inputSchema: WorkflowRunInputSchema,
3393
3984
  outputSchema: WorkflowRunOutputSchema,
3394
3985
  annotations: liveWebToolAnnotations("Run Workflow")
3395
3986
  }, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
3987
+ server2.registerTool("workflow_step", {
3988
+ title: "Advance Workflow Step",
3989
+ 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."),
3990
+ inputSchema: WorkflowStepInputSchema,
3991
+ outputSchema: WorkflowStepOutputSchema,
3992
+ annotations: liveWebToolAnnotations("Advance Workflow Step")
3993
+ }, async (input) => formatWorkflowStep(await executor.workflowStep(input), input));
3396
3994
  server2.registerTool("workflow_status", {
3397
3995
  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.",
3996
+ 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
3997
  inputSchema: WorkflowStatusInputSchema,
3400
3998
  outputSchema: WorkflowStatusOutputSchema,
3401
3999
  annotations: liveWebToolAnnotations("Workflow Status")
3402
4000
  }, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
3403
4001
  server2.registerTool("workflow_artifact_read", {
3404
4002
  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.",
4003
+ 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
4004
  inputSchema: WorkflowArtifactReadInputSchema,
3407
4005
  outputSchema: WorkflowArtifactReadOutputSchema,
3408
4006
  annotations: liveWebToolAnnotations("Read Workflow Artifact")
@@ -3482,7 +4080,7 @@ function renderInstallTerminal(options) {
3482
4080
  "1/1 install surfaces ready",
3483
4081
  colorize("Newest: MCP workflow tools for market analysis, ICP research, CRO audits, brand briefs, and AI search visibility.", "lime", color),
3484
4082
  "",
3485
- `${colorize("Tools", "cyan", color)} ${colorize("(38 MCP tools)", "muted", color)}`,
4083
+ `${colorize("Tools", "cyan", color)} ${colorize("(39 MCP tools)", "muted", color)}`,
3486
4084
  toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
3487
4085
  toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "directory_workflow"], color),
3488
4086
  toolRow("build", ["rank_tracker_blueprint", "cron plan", "database prompt"], color),
@@ -3490,7 +4088,7 @@ function renderInstallTerminal(options) {
3490
4088
  toolRow("browser", ["browser_open", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
3491
4089
  toolRow("account", ["credits_info", "local reports", "MCP resources"], color),
3492
4090
  `${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),
4091
+ toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_step", "workflow_status", "workflow_artifact_read"], color),
3494
4092
  toolRow("seo", ["directory", "agent-packet", "local audit", "map/serp comparison", "PAA/AIO briefs", "scheduled runs"], color),
3495
4093
  "",
3496
4094
  colorize("Usage tips:", "amber", color),