mcp-scraper 0.3.24 → 0.3.25

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 (65) hide show
  1. package/dist/bin/api-server.cjs +8140 -5941
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +320 -58
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +4 -3
  7. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +121 -24
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +284 -10
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +5 -3
  17. package/dist/bin/paa-harvest.js.map +1 -1
  18. package/dist/{chunk-ZAP7MYVO.js → chunk-3PRO376E.js} +2 -3
  19. package/dist/chunk-3PRO376E.js.map +1 -0
  20. package/dist/{chunk-LFATOGDF.js → chunk-BP27CZ5Q.js} +4 -3
  21. package/dist/chunk-BP27CZ5Q.js.map +1 -0
  22. package/dist/{chunk-DE7WP4UU.js → chunk-DOBQN3EY.js} +3 -3
  23. package/dist/chunk-DOBQN3EY.js.map +1 -0
  24. package/dist/{chunk-GZ4W72WF.js → chunk-FE2WC4JR.js} +119 -23
  25. package/dist/chunk-FE2WC4JR.js.map +1 -0
  26. package/dist/{chunk-IGOEAKC7.js → chunk-G7PQ64LM.js} +274 -9
  27. package/dist/chunk-G7PQ64LM.js.map +1 -0
  28. package/dist/chunk-K443GQY5.js +24 -0
  29. package/dist/chunk-K443GQY5.js.map +1 -0
  30. package/dist/{chunk-H2R232HK.js → chunk-NNEIXK5L.js} +307 -62
  31. package/dist/chunk-NNEIXK5L.js.map +1 -0
  32. package/dist/{chunk-3KYRG7O7.js → chunk-TRNSXR46.js} +185 -2
  33. package/dist/chunk-TRNSXR46.js.map +1 -0
  34. package/dist/chunk-WPHAMNT7.js +7 -0
  35. package/dist/chunk-WPHAMNT7.js.map +1 -0
  36. package/dist/{chunk-JTWQGPKF.js → chunk-WYCER2HW.js} +33 -10
  37. package/dist/chunk-WYCER2HW.js.map +1 -0
  38. package/dist/{db-EG5ETPTY.js → db-FP2ABUU4.js} +32 -2
  39. package/dist/index.cjs +284 -10
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -3
  42. package/dist/index.d.ts +7 -3
  43. package/dist/index.js +5 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/{server-2SRARAO5.js → server-RRCKVBHG.js} +1563 -265
  46. package/dist/server-RRCKVBHG.js.map +1 -0
  47. package/dist/{worker-UM5UM6O5.js → worker-UC6D2756.js} +7 -6
  48. package/dist/{worker-UM5UM6O5.js.map → worker-UC6D2756.js.map} +1 -1
  49. package/docs/kernel-proxy-future-enhancements.md +80 -0
  50. package/docs/spec-browser-routing.md +108 -0
  51. package/docs/spec-mcp-tools.md +146 -0
  52. package/docs/specs/audit-visual-demo.md +309 -0
  53. package/package.json +5 -2
  54. package/dist/chunk-3KYRG7O7.js.map +0 -1
  55. package/dist/chunk-DE7WP4UU.js.map +0 -1
  56. package/dist/chunk-GZ4W72WF.js.map +0 -1
  57. package/dist/chunk-H2R232HK.js.map +0 -1
  58. package/dist/chunk-H7R4PPMG.js +0 -7
  59. package/dist/chunk-H7R4PPMG.js.map +0 -1
  60. package/dist/chunk-IGOEAKC7.js.map +0 -1
  61. package/dist/chunk-JTWQGPKF.js.map +0 -1
  62. package/dist/chunk-LFATOGDF.js.map +0 -1
  63. package/dist/chunk-ZAP7MYVO.js.map +0 -1
  64. package/dist/server-2SRARAO5.js.map +0 -1
  65. /package/dist/{db-EG5ETPTY.js.map → db-FP2ABUU4.js.map} +0 -0
@@ -36,10 +36,10 @@ var CLIENT_OVER_SERVER_MARGIN_MS = 15e3;
36
36
  function harvestTimeoutBudget(maxQuestions, serpOnly = false) {
37
37
  const requested = Number.isFinite(maxQuestions) && maxQuestions > 0 ? Math.trunc(maxQuestions) : 30;
38
38
  let serverMs;
39
- if (serpOnly || requested <= 50) serverMs = 11e4;
40
- else if (requested <= 100) serverMs = 18e4;
41
- else if (requested <= 150) serverMs = 24e4;
42
- else serverMs = 28e4;
39
+ if (serpOnly) serverMs = 11e4;
40
+ else if (requested <= 50) serverMs = 17e4;
41
+ else if (requested <= 100) serverMs = 23e4;
42
+ else serverMs = 285e3;
43
43
  const clientMs = Math.min(serverMs + CLIENT_OVER_SERVER_MARGIN_MS, VERCEL_FUNCTION_MAX_MS - 5e3);
44
44
  return { serverMs, clientMs };
45
45
  }
@@ -261,6 +261,9 @@ var HttpMcpToolExecutor = class {
261
261
  facebookAdSearch(input) {
262
262
  return this.call("/facebook/search", input);
263
263
  }
264
+ redditThread(input) {
265
+ return this.call("/reddit/thread", input, this.httpTimeoutOverrideMs ?? 24e4);
266
+ }
264
267
  facebookAdTranscribe(input) {
265
268
  return this.call("/facebook/transcribe", input);
266
269
  }
@@ -456,7 +459,7 @@ render();
456
459
  }
457
460
 
458
461
  // src/version.ts
459
- var PACKAGE_VERSION = "0.3.24";
462
+ var PACKAGE_VERSION = "0.3.25";
460
463
 
461
464
  // src/mcp/browser-agent-tool-schemas.ts
462
465
  var import_zod = require("zod");
@@ -1872,6 +1875,17 @@ var WORKFLOW_RECIPES = [
1872
1875
  produces: ["city summary", "competitor CSV", "review insight CSV", "market difficulty/opportunity notes", "HTML report"],
1873
1876
  runHint: "Use workflow_run with workflowId local-competitive-audit for state-wide market batches, or map-comparison for one city/location comparison."
1874
1877
  },
1878
+ {
1879
+ id: "lead_generation",
1880
+ title: "Lead Generation",
1881
+ description: "Build an outreach-ready local lead list for a niche in one market: Google Maps businesses with confirmed review counts and booking URLs, enriched with email and social links scraped from each business website (proxy/browser-backed so blocked sites still resolve).",
1882
+ primaryWorkflowId: "get-leads",
1883
+ recommendedTools: ["workflow_run", "maps_search", "maps_place_intel", "extract_url"],
1884
+ requiredInputs: ["query", "location"],
1885
+ optionalInputs: ["maxResults", "enrichWebsites", "hydrateReviewCounts", "concurrency", "proxyMode"],
1886
+ produces: ["leads CSV (name, phone, website, email, socials, review count, booking URL)", "evidence JSON", "HTML report", "outreach task list"],
1887
+ runHint: 'Use workflow_run with workflowId get-leads. Put the niche in query (e.g. "roofers") and the market in location (e.g. "Houston, TX"); do not combine them.'
1888
+ },
1875
1889
  {
1876
1890
  id: "icp_research",
1877
1891
  title: "ICP Research",
@@ -2817,6 +2831,16 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
2817
2831
  const bodySectionFull = bodyMd ? `
2818
2832
  ## Page Content
2819
2833
  ${bodyMd}` : "";
2834
+ const mem = d.memory ?? null;
2835
+ const memSection = mem?.deposited ? `
2836
+ ## \u2705 Saved to memory
2837
+ Full content stored in vault \`${mem.vault ?? "\u2014"}\` as note \`${mem.noteId ?? "\u2014"}\`${mem.chunks !== void 0 ? ` (${mem.chunks} chunks indexed)` : ""}. Recall it later with memory search \u2014 no need to re-scrape.` : mem?.fileUrl ? `
2838
+ ## \u{1F4C4} Saved as a temporary file
2839
+ Memory vault was unavailable${mem.error ? ` (${mem.error})` : ""}, so the full content was saved to a download link instead:
2840
+ - **Link:** ${mem.fileUrl}
2841
+ - **Expires:** ${mem.fileExpiresAt ?? "in 24 hours"} (auto-deleted)` : mem ? `
2842
+ ## \u26A0\uFE0F Memory deposit skipped
2843
+ ${mem.error ?? "unknown error"} \u2014 the page content is still in the truncated preview above.` : "";
2820
2844
  const screenshotSection = screenshotMeta ? `
2821
2845
  ## Screenshot
2822
2846
  - **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
@@ -2846,10 +2870,10 @@ ${bodyMd}` : "";
2846
2870
  - ${schemaCount} JSON-LD schema block(s) detected`;
2847
2871
  const full = `# URL Extract: ${url}
2848
2872
  **${title}**
2849
- ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
2873
+ ${headingSection}${kpoSection}${brandingSection}${bodySection}${memSection}${screenshotSection}${mediaSection}${tips}`;
2850
2874
  const diskReport = `# URL Extract: ${url}
2851
2875
  **${title}**
2852
- ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
2876
+ ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${memSection}${screenshotSection}${mediaSection}${tips}`;
2853
2877
  const textResult = oneBlock(full, diskReport);
2854
2878
  const structuredContent = {
2855
2879
  url,
@@ -2862,7 +2886,8 @@ ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSe
2862
2886
  missingSchemaFields: kpo?.missingFields ?? [],
2863
2887
  screenshotSaved: screenshotPath ?? null,
2864
2888
  branding: branding ?? null,
2865
- mediaAssets: media?.assets ?? null
2889
+ mediaAssets: media?.assets ?? null,
2890
+ memory: mem ?? void 0
2866
2891
  };
2867
2892
  if (screenshotMeta?.base64) {
2868
2893
  return {
@@ -3248,6 +3273,38 @@ ${adBlocks}`,
3248
3273
  }
3249
3274
  };
3250
3275
  }
3276
+ function formatRedditThread(raw, input) {
3277
+ const parsed = parseData(raw);
3278
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
3279
+ const d = parsed.data;
3280
+ const comments = d.comments ?? [];
3281
+ const commentMd = comments.map((cm) => {
3282
+ const indent = " ".repeat(Math.min(Number(cm.depth ?? 0), 6));
3283
+ return `${indent}- **u/${cm.author || "[unknown]"}** (${cm.score || "\u2014"}): ${(cm.body || "").replace(/\s+/g, " ").trim()}`;
3284
+ }).join("\n");
3285
+ const full = [
3286
+ `# ${d.title || "Reddit thread"}`,
3287
+ `**u/${d.author || "[unknown]"}** \xB7 ${d.score || "\u2014"} \xB7 ${comments.length} comments captured`,
3288
+ d.postBody ? `
3289
+ ${d.postBody}` : "",
3290
+ `
3291
+ ## Comments
3292
+ ${commentMd || "_No comments captured._"}`
3293
+ ].join("\n");
3294
+ return {
3295
+ ...oneBlock(full),
3296
+ structuredContent: {
3297
+ sourceUrl: d.sourceUrl ?? input.url ?? null,
3298
+ oldRedditUrl: d.oldRedditUrl ?? null,
3299
+ title: d.title ?? null,
3300
+ author: d.author ?? null,
3301
+ score: d.score ?? null,
3302
+ postBody: d.postBody ?? null,
3303
+ numComments: comments.length,
3304
+ comments: comments.map((cm) => ({ author: cm.author ?? null, score: cm.score ?? null, depth: Number(cm.depth ?? 0), body: cm.body ?? "" }))
3305
+ }
3306
+ };
3307
+ }
3251
3308
  function formatFacebookAdSearch(raw, input) {
3252
3309
  const parsed = parseData(raw);
3253
3310
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -4213,7 +4270,7 @@ var import_zod3 = require("zod");
4213
4270
 
4214
4271
  // src/schemas.ts
4215
4272
  var import_zod2 = require("zod");
4216
- var DEFAULT_PROXY_MODE = "configured";
4273
+ var DEFAULT_PROXY_MODE = "none";
4217
4274
  var DEFAULT_MAPS_PROXY_MODE = "location";
4218
4275
  var HarvestOptionsSchema = import_zod2.z.object({
4219
4276
  query: import_zod2.z.string().min(1),
@@ -4235,7 +4292,8 @@ var HarvestOptionsSchema = import_zod2.z.object({
4235
4292
  outputDir: import_zod2.z.string().default("./paa-output"),
4236
4293
  format: import_zod2.z.enum(["json", "csv", "both"]).default("both"),
4237
4294
  serpOnly: import_zod2.z.boolean().default(false),
4238
- pages: import_zod2.z.number().int().min(1).max(2).default(1)
4295
+ pages: import_zod2.z.number().int().min(1).max(2).default(1),
4296
+ softDeadlineMs: import_zod2.z.number().optional()
4239
4297
  });
4240
4298
  var MapsPlaceOptionsSchema = import_zod2.z.object({
4241
4299
  businessName: import_zod2.z.string().min(1),
@@ -4317,7 +4375,7 @@ var HarvestPaaInputSchema = {
4317
4375
  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."),
4318
4376
  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."),
4319
4377
  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."),
4320
- 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."),
4378
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic (clean egress by default; residential only when a location is given, with fallback). Set "location" only to force local-pack targeting.'),
4321
4379
  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."),
4322
4380
  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.")
4323
4381
  };
@@ -4328,7 +4386,9 @@ var ExtractUrlInputSchema = {
4328
4386
  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."),
4329
4387
  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."),
4330
4388
  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."),
4331
- allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
4389
+ allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only."),
4390
+ depositToVault: import_zod3.z.boolean().default(false).describe("Also save the full page content into the user's MCP Memory Library vault, server-side. The complete body is stored and embedded for semantic recall WITHOUT passing through this conversation \u2014 the tool result only confirms the saved note. Use when the user asks to save/remember/store a page, or to build a knowledge base from scrapes."),
4391
+ vaultName: import_zod3.z.string().trim().min(1).max(120).optional().describe("Optional vault to deposit into. Defaults to the user's personal vault.")
4332
4392
  };
4333
4393
  var MapSiteUrlsInputSchema = {
4334
4394
  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."),
@@ -4369,6 +4429,10 @@ var FacebookAdSearchInputSchema = {
4369
4429
  country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
4370
4430
  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.")
4371
4431
  };
4432
+ var RedditThreadInputSchema = {
4433
+ url: import_zod3.z.string().min(1).describe("A reddit.com thread/post URL (www, old, or new Reddit, or a redd.it link). The service fetches it via old.reddit through a residential proxy and returns the post plus its comment tree."),
4434
+ maxComments: import_zod3.z.number().int().min(1).max(2e3).optional().describe("Optional cap on comments returned. Omit to return all captured comments.")
4435
+ };
4372
4436
  var FacebookAdTranscribeInputSchema = {
4373
4437
  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.")
4374
4438
  };
@@ -4635,7 +4699,17 @@ var ExtractUrlOutputSchema = {
4635
4699
  entityTypes: import_zod3.z.array(import_zod3.z.string()),
4636
4700
  napScore: import_zod3.z.number().nullable(),
4637
4701
  missingSchemaFields: import_zod3.z.array(import_zod3.z.string()),
4638
- screenshotSaved: NullableString2
4702
+ screenshotSaved: NullableString2,
4703
+ memory: import_zod3.z.object({
4704
+ deposited: import_zod3.z.boolean(),
4705
+ vault: import_zod3.z.string().optional(),
4706
+ noteId: import_zod3.z.string().optional(),
4707
+ path: import_zod3.z.string().optional(),
4708
+ chunks: import_zod3.z.number().int().optional(),
4709
+ fileUrl: import_zod3.z.string().optional(),
4710
+ fileExpiresAt: import_zod3.z.string().optional(),
4711
+ error: import_zod3.z.string().optional()
4712
+ }).optional()
4639
4713
  };
4640
4714
  var ExtractSiteOutputSchema = {
4641
4715
  url: import_zod3.z.string(),
@@ -4767,6 +4841,21 @@ var FacebookAdSearchOutputSchema = {
4767
4841
  sampleLibraryId: NullableString2
4768
4842
  }))
4769
4843
  };
4844
+ var RedditThreadOutputSchema = {
4845
+ sourceUrl: NullableString2,
4846
+ oldRedditUrl: NullableString2,
4847
+ title: NullableString2,
4848
+ author: NullableString2,
4849
+ score: NullableString2,
4850
+ postBody: NullableString2,
4851
+ numComments: import_zod3.z.number().int().min(0),
4852
+ comments: import_zod3.z.array(import_zod3.z.object({
4853
+ author: NullableString2,
4854
+ score: NullableString2,
4855
+ depth: import_zod3.z.number().int().min(0),
4856
+ body: import_zod3.z.string()
4857
+ }))
4858
+ };
4770
4859
  var FacebookPageIntelOutputSchema = {
4771
4860
  advertiserName: NullableString2,
4772
4861
  totalAds: import_zod3.z.number().int().min(0),
@@ -4960,6 +5049,7 @@ var CreditsInfoInputSchema = {
4960
5049
  };
4961
5050
  var WorkflowIdSchema = import_zod3.z.enum([
4962
5051
  "directory",
5052
+ "get-leads",
4963
5053
  "agent-packet",
4964
5054
  "local-competitive-audit",
4965
5055
  "map-comparison",
@@ -4981,8 +5071,8 @@ var WorkflowSuggestInputSchema = {
4981
5071
  maxSuggestions: import_zod3.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
4982
5072
  };
4983
5073
  var WorkflowRunInputSchema = {
4984
- 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."),
4985
- 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?}."),
5074
+ workflowId: WorkflowIdSchema.describe("Workflow to run: directory, get-leads, 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."),
5075
+ input: import_zod3.z.record(import_zod3.z.unknown()).default({}).describe("Workflow-specific input object. Examples: get-leads uses {query, location, maxResults?, enrichWebsites?, hydrateReviewCounts?}; 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?}."),
4986
5076
  webhookUrl: import_zod3.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
4987
5077
  };
4988
5078
  var WorkflowStepInputSchema = {
@@ -5058,7 +5148,7 @@ var SearchSerpInputSchema = {
5058
5148
  gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
5059
5149
  hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from user request."),
5060
5150
  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."),
5061
- 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."),
5151
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic. Set "location" only to force local-pack targeting.'),
5062
5152
  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."),
5063
5153
  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."),
5064
5154
  pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
@@ -5069,7 +5159,7 @@ var CaptureSerpSnapshotInputSchema = {
5069
5159
  gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
5070
5160
  hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
5071
5161
  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."),
5072
- 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."),
5162
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic. Set "location" only to force city/ZIP targeting.'),
5073
5163
  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."),
5074
5164
  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."),
5075
5165
  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."),
@@ -5476,21 +5566,21 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
5476
5566
  if (savesReports) registerSavedReportResources(server2);
5477
5567
  server2.registerTool("harvest_paa", {
5478
5568
  title: "Google PAA + SERP Harvest",
5479
- 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.'),
5569
+ 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"). Routing is automatic \u2014 leave proxyMode unset. If a deep harvest exceeds the time budget it returns the questions gathered so far (partial), billed per returned question. 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.'),
5480
5570
  inputSchema: HarvestPaaInputSchema,
5481
5571
  outputSchema: HarvestPaaOutputSchema,
5482
5572
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
5483
5573
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
5484
5574
  server2.registerTool("search_serp", {
5485
5575
  title: "Google SERP Lookup",
5486
- 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."),
5576
+ 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. Routing is automatic \u2014 leave proxyMode unset; set location only to force local-pack targeting."),
5487
5577
  inputSchema: SearchSerpInputSchema,
5488
5578
  outputSchema: SearchSerpOutputSchema,
5489
5579
  annotations: liveWebToolAnnotations("Google SERP Lookup")
5490
5580
  }, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
5491
5581
  server2.registerTool("extract_url", {
5492
5582
  title: "Single URL Extract",
5493
- 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."),
5583
+ 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. Set depositToVault:true to also save the full page into the user's MCP Memory vault server-side (the full body is NOT returned to chat). Use map_site_urls before extracting a site inventory; use extract_site for multi-page crawling."),
5494
5584
  inputSchema: ExtractUrlInputSchema,
5495
5585
  outputSchema: ExtractUrlOutputSchema,
5496
5586
  annotations: liveWebToolAnnotations("Single URL Extract")
@@ -5544,6 +5634,13 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
5544
5634
  outputSchema: FacebookAdSearchOutputSchema,
5545
5635
  annotations: liveWebToolAnnotations("Facebook Ad Library Search")
5546
5636
  }, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
5637
+ server2.registerTool("reddit_thread", {
5638
+ title: "Reddit Thread + Comments",
5639
+ description: withReportNote("Capture a Reddit post and its comment tree from any reddit.com thread URL. Use this when the user wants Reddit comments, opinions, discussion, or audience voice from a specific thread (find threads first with search_serp, which surfaces reddit.com/comments links). Returns the post title, author, score, body, and a list of comments with author, score, nesting depth, and text. Handles Reddit's bot protection automatically (fetches via old.reddit through a residential proxy with retries); pass maxComments to cap the list."),
5640
+ inputSchema: RedditThreadInputSchema,
5641
+ outputSchema: RedditThreadOutputSchema,
5642
+ annotations: liveWebToolAnnotations("Reddit Thread + Comments")
5643
+ }, async (input) => formatRedditThread(await executor.redditThread(input), input));
5547
5644
  server2.registerTool("facebook_ad_transcribe", {
5548
5645
  title: "Facebook Ad Transcription",
5549
5646
  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.",
@@ -5581,14 +5678,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
5581
5678
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
5582
5679
  server2.registerTool("maps_search", {
5583
5680
  title: "Google Maps Business Search",
5584
- 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.'),
5681
+ 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." Routing is automatic and location-aware \u2014 leave proxyMode unset; pass proxyZip only to force a specific ZIP. 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.'),
5585
5682
  inputSchema: MapsSearchInputSchema,
5586
5683
  outputSchema: MapsSearchOutputSchema,
5587
5684
  annotations: liveWebToolAnnotations("Google Maps Business Search")
5588
5685
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
5589
5686
  server2.registerTool("directory_workflow", {
5590
5687
  title: "Directory Workflow: Markets + Maps",
5591
- 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.'),
5688
+ 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. Routing is automatic per city. 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.'),
5592
5689
  inputSchema: DirectoryWorkflowInputSchema,
5593
5690
  outputSchema: DirectoryWorkflowOutputSchema,
5594
5691
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
@@ -5709,11 +5806,11 @@ function renderInstallTerminal(options) {
5709
5806
  "1/1 install surfaces ready",
5710
5807
  colorize("Newest: saved browser profiles that hold MANY site logins. Share the watch_url, user signs in, then verify with browser_profile_list. Browser sessions are direct/no-proxy by default.", "lime", color),
5711
5808
  "",
5712
- `${colorize("Tools", "cyan", color)} ${colorize("(45 MCP tools)", "muted", color)}`,
5809
+ `${colorize("Tools", "cyan", color)} ${colorize("(46 MCP tools)", "muted", color)}`,
5713
5810
  toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
5714
5811
  toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "audit_site", "directory_workflow"], color),
5715
5812
  toolRow("build", ["rank_tracker_workflow", "cron plan", "database prompt"], color),
5716
- toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe", "instagram_profile_content", "instagram_media_download"], color),
5813
+ toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe", "instagram_profile_content", "instagram_media_download", "reddit_thread"], color),
5717
5814
  toolRow("browser", ["browser_open", "browser_profile_connect", "browser_profile_list", "browser_close", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
5718
5815
  toolRow("account", ["credits_info", "reports", "MCP resources"], color),
5719
5816
  `${colorize("Workflows", "cyan", color)} ${colorize("(MCP + CLI + API)", "muted", color)}`,