mcp-scraper 0.3.24 → 0.3.26

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 (66) hide show
  1. package/dist/bin/api-server.cjs +8220 -5991
  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 +186 -49
  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-GZ4W72WF.js → chunk-2GHI2D6Q.js} +184 -48
  19. package/dist/chunk-2GHI2D6Q.js.map +1 -0
  20. package/dist/{chunk-ZAP7MYVO.js → chunk-3PRO376E.js} +2 -3
  21. package/dist/chunk-3PRO376E.js.map +1 -0
  22. package/dist/{chunk-LFATOGDF.js → chunk-BP27CZ5Q.js} +4 -3
  23. package/dist/chunk-BP27CZ5Q.js.map +1 -0
  24. package/dist/{chunk-DE7WP4UU.js → chunk-DOBQN3EY.js} +3 -3
  25. package/dist/chunk-DOBQN3EY.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-MS2UZ6HR.js +7 -0
  31. package/dist/chunk-MS2UZ6HR.js.map +1 -0
  32. package/dist/{chunk-H2R232HK.js → chunk-NNEIXK5L.js} +307 -62
  33. package/dist/chunk-NNEIXK5L.js.map +1 -0
  34. package/dist/{chunk-3KYRG7O7.js → chunk-TRNSXR46.js} +185 -2
  35. package/dist/chunk-TRNSXR46.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-HIGHKTEV.js} +1563 -275
  46. package/dist/server-HIGHKTEV.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/spec-server-instructions.md +94 -0
  53. package/docs/specs/audit-visual-demo.md +309 -0
  54. package/package.json +5 -2
  55. package/dist/chunk-3KYRG7O7.js.map +0 -1
  56. package/dist/chunk-DE7WP4UU.js.map +0 -1
  57. package/dist/chunk-GZ4W72WF.js.map +0 -1
  58. package/dist/chunk-H2R232HK.js.map +0 -1
  59. package/dist/chunk-H7R4PPMG.js +0 -7
  60. package/dist/chunk-H7R4PPMG.js.map +0 -1
  61. package/dist/chunk-IGOEAKC7.js.map +0 -1
  62. package/dist/chunk-JTWQGPKF.js.map +0 -1
  63. package/dist/chunk-LFATOGDF.js.map +0 -1
  64. package/dist/chunk-ZAP7MYVO.js.map +0 -1
  65. package/dist/server-2SRARAO5.js.map +0 -1
  66. /package/dist/{db-EG5ETPTY.js.map → db-FP2ABUU4.js.map} +0 -0
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  harvest
4
- } from "../chunk-IGOEAKC7.js";
4
+ } from "../chunk-G7PQ64LM.js";
5
5
  import {
6
6
  browserServiceApiKey
7
- } from "../chunk-ZAP7MYVO.js";
8
- import "../chunk-LFATOGDF.js";
7
+ } from "../chunk-3PRO376E.js";
8
+ import "../chunk-K443GQY5.js";
9
+ import "../chunk-BP27CZ5Q.js";
10
+ import "../chunk-TRNSXR46.js";
9
11
 
10
12
  // src/cli.ts
11
13
  import { Command } from "commander";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cli.ts","../../bin/paa-harvest.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { browserServiceApiKey } from './lib/browser-service-env.js'\nimport { harvest } from './harvest.js'\n\nconst program = new Command()\n\nprogram\n .name('paa-harvest')\n .description('Recursively extract Google People Also Ask questions')\n .requiredOption('-q, --query <query>', 'Seed query')\n .option('-l, --location <location>', 'Location name (e.g. \"austin\" or \"Austin,Texas,United States\")')\n .option('--gl <gl>', 'Google country code', 'us')\n .option('--hl <hl>', 'Google language code', 'en')\n .option('-d, --depth <depth>', 'BFS depth (1-30)', '3')\n .option('-m, --max-questions <n>', 'Max questions to harvest', '100')\n .option('-o, --output <dir>', 'Output directory', './paa-output')\n .option('-f, --format <format>', 'Output format: json, csv, or both', 'both')\n .option('--headless', 'Run browser in headless mode', false)\n .option('--profile <dir>', 'Persistent browser profile directory')\n .option('--proxy <url>', 'Proxy server URL')\n .option('--kernel-api-key <key>', 'Browser service API key (or set BROWSER_SERVICE_API_KEY env var)')\n .action(async (opts) => {\n try {\n const result = await harvest({\n query: opts.query,\n location: opts.location,\n gl: opts.gl,\n hl: opts.hl,\n depth: parseInt(opts.depth, 10),\n maxQuestions: parseInt(opts.maxQuestions, 10),\n outputDir: opts.output,\n format: opts.format,\n headless: opts.headless,\n profileDir: opts.profile,\n proxy: opts.proxy,\n kernelApiKey: opts.kernelApiKey ?? browserServiceApiKey(),\n })\n console.log(JSON.stringify({ totalQuestions: result.totalQuestions, outputDir: result.stats.seed }))\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n }\n })\n\nexport async function runCli(): Promise<void> {\n await program.parseAsync()\n}\n","#!/usr/bin/env node\nimport { runCli } from '../src/cli.js'\nrunCli()\n"],"mappings":";;;;;;;;;;AAAA,SAAS,eAAe;AAIxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,aAAa,EAClB,YAAY,sDAAsD,EAClE,eAAe,uBAAuB,YAAY,EAClD,OAAO,6BAA6B,+DAA+D,EACnG,OAAO,aAAa,uBAAuB,IAAI,EAC/C,OAAO,aAAa,wBAAwB,IAAI,EAChD,OAAO,uBAAuB,oBAAoB,GAAG,EACrD,OAAO,2BAA2B,4BAA4B,KAAK,EACnE,OAAO,sBAAsB,oBAAoB,cAAc,EAC/D,OAAO,yBAAyB,qCAAqC,MAAM,EAC3E,OAAO,cAAc,gCAAgC,KAAK,EAC1D,OAAO,mBAAmB,sCAAsC,EAChE,OAAO,iBAAiB,kBAAkB,EAC1C,OAAO,0BAA0B,kEAAkE,EACnG,OAAO,OAAO,SAAS;AACtB,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ;AAAA,MAC3B,OAAO,KAAK;AAAA,MACZ,UAAU,KAAK;AAAA,MACf,IAAI,KAAK;AAAA,MACT,IAAI,KAAK;AAAA,MACT,OAAO,SAAS,KAAK,OAAO,EAAE;AAAA,MAC9B,cAAc,SAAS,KAAK,cAAc,EAAE;AAAA,MAC5C,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,YAAY,KAAK;AAAA,MACjB,OAAO,KAAK;AAAA,MACZ,cAAc,KAAK,gBAAgB,qBAAqB;AAAA,IAC1D,CAAC;AACD,YAAQ,IAAI,KAAK,UAAU,EAAE,gBAAgB,OAAO,gBAAgB,WAAW,OAAO,MAAM,KAAK,CAAC,CAAC;AAAA,EACrG,SAAS,KAAK;AACZ,YAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,eAAsB,SAAwB;AAC5C,QAAM,QAAQ,WAAW;AAC3B;;;AC5CA,OAAO;","names":[]}
1
+ {"version":3,"sources":["../../src/cli.ts","../../bin/paa-harvest.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { browserServiceApiKey } from './lib/browser-service-env.js'\nimport { harvest } from './harvest.js'\n\nconst program = new Command()\n\nprogram\n .name('paa-harvest')\n .description('Recursively extract Google People Also Ask questions')\n .requiredOption('-q, --query <query>', 'Seed query')\n .option('-l, --location <location>', 'Location name (e.g. \"austin\" or \"Austin,Texas,United States\")')\n .option('--gl <gl>', 'Google country code', 'us')\n .option('--hl <hl>', 'Google language code', 'en')\n .option('-d, --depth <depth>', 'BFS depth (1-30)', '3')\n .option('-m, --max-questions <n>', 'Max questions to harvest', '100')\n .option('-o, --output <dir>', 'Output directory', './paa-output')\n .option('-f, --format <format>', 'Output format: json, csv, or both', 'both')\n .option('--headless', 'Run browser in headless mode', false)\n .option('--profile <dir>', 'Persistent browser profile directory')\n .option('--proxy <url>', 'Proxy server URL')\n .option('--kernel-api-key <key>', 'Browser service API key (or set BROWSER_SERVICE_API_KEY env var)')\n .action(async (opts) => {\n try {\n const result = await harvest({\n query: opts.query,\n location: opts.location,\n gl: opts.gl,\n hl: opts.hl,\n depth: parseInt(opts.depth, 10),\n maxQuestions: parseInt(opts.maxQuestions, 10),\n outputDir: opts.output,\n format: opts.format,\n headless: opts.headless,\n profileDir: opts.profile,\n proxy: opts.proxy,\n kernelApiKey: opts.kernelApiKey ?? browserServiceApiKey(),\n })\n console.log(JSON.stringify({ totalQuestions: result.totalQuestions, outputDir: result.stats.seed }))\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n }\n })\n\nexport async function runCli(): Promise<void> {\n await program.parseAsync()\n}\n","#!/usr/bin/env node\nimport { runCli } from '../src/cli.js'\nrunCli()\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,eAAe;AAIxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,aAAa,EAClB,YAAY,sDAAsD,EAClE,eAAe,uBAAuB,YAAY,EAClD,OAAO,6BAA6B,+DAA+D,EACnG,OAAO,aAAa,uBAAuB,IAAI,EAC/C,OAAO,aAAa,wBAAwB,IAAI,EAChD,OAAO,uBAAuB,oBAAoB,GAAG,EACrD,OAAO,2BAA2B,4BAA4B,KAAK,EACnE,OAAO,sBAAsB,oBAAoB,cAAc,EAC/D,OAAO,yBAAyB,qCAAqC,MAAM,EAC3E,OAAO,cAAc,gCAAgC,KAAK,EAC1D,OAAO,mBAAmB,sCAAsC,EAChE,OAAO,iBAAiB,kBAAkB,EAC1C,OAAO,0BAA0B,kEAAkE,EACnG,OAAO,OAAO,SAAS;AACtB,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ;AAAA,MAC3B,OAAO,KAAK;AAAA,MACZ,UAAU,KAAK;AAAA,MACf,IAAI,KAAK;AAAA,MACT,IAAI,KAAK;AAAA,MACT,OAAO,SAAS,KAAK,OAAO,EAAE;AAAA,MAC9B,cAAc,SAAS,KAAK,cAAc,EAAE;AAAA,MAC5C,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,YAAY,KAAK;AAAA,MACjB,OAAO,KAAK;AAAA,MACZ,cAAc,KAAK,gBAAgB,qBAAqB;AAAA,IAC1D,CAAC;AACD,YAAQ,IAAI,KAAK,UAAU,EAAE,gBAAgB,OAAO,gBAAgB,WAAW,OAAO,MAAM,KAAK,CAAC,CAAC;AAAA,EACrG,SAAS,KAAK;AACZ,YAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC9D,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,eAAsB,SAAwB;AAC5C,QAAM,QAAQ,WAAW;AAC3B;;;AC5CA,OAAO;","names":[]}
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  PACKAGE_VERSION
3
- } from "./chunk-H7R4PPMG.js";
3
+ } from "./chunk-MS2UZ6HR.js";
4
4
  import {
5
5
  browserServiceProfileName,
6
6
  browserServiceProfileSaveChanges,
7
7
  sanitizeVendorName
8
- } from "./chunk-ZAP7MYVO.js";
8
+ } from "./chunk-3PRO376E.js";
9
9
  import {
10
10
  DEFAULT_MAPS_PROXY_MODE,
11
11
  DEFAULT_PROXY_MODE
12
- } from "./chunk-LFATOGDF.js";
12
+ } from "./chunk-BP27CZ5Q.js";
13
13
 
14
14
  // src/harvest-timeout.ts
15
15
  var VERCEL_FUNCTION_MAX_MS = 3e5;
@@ -17,10 +17,10 @@ var CLIENT_OVER_SERVER_MARGIN_MS = 15e3;
17
17
  function harvestTimeoutBudget(maxQuestions, serpOnly = false) {
18
18
  const requested = Number.isFinite(maxQuestions) && maxQuestions > 0 ? Math.trunc(maxQuestions) : 30;
19
19
  let serverMs;
20
- if (serpOnly || requested <= 50) serverMs = 11e4;
21
- else if (requested <= 100) serverMs = 18e4;
22
- else if (requested <= 150) serverMs = 24e4;
23
- else serverMs = 28e4;
20
+ if (serpOnly) serverMs = 11e4;
21
+ else if (requested <= 50) serverMs = 17e4;
22
+ else if (requested <= 100) serverMs = 23e4;
23
+ else serverMs = 285e3;
24
24
  const clientMs = Math.min(serverMs + CLIENT_OVER_SERVER_MARGIN_MS, VERCEL_FUNCTION_MAX_MS - 5e3);
25
25
  return { serverMs, clientMs };
26
26
  }
@@ -92,33 +92,73 @@ function sanitizeHarvestResult(result) {
92
92
 
93
93
  // src/mcp/server-instructions.ts
94
94
  var SERVER_INSTRUCTIONS = `
95
- This server scrapes and analyzes web, social, search, maps, and site data. Pick a tool by NAME using
96
- this routing map, then load its schema before calling.
95
+ # MCP Scraper
97
96
 
98
- ROUTING
99
- - One web page -> extract_url. Whole site (crawl + SEO report) -> extract_site. Just the URL list -> map_site_urls.
100
- - Web search (organic SERP) -> search_serp. Full SERP + People-Also-Ask / AI-Overview detail -> harvest_paa.
101
- - Google Maps: find places -> maps_search; one place deep-dive + reviews -> maps_place_intel.
102
- - YouTube: find or list videos -> youtube_harvest; transcribe one video -> youtube_transcribe.
103
- - Facebook: find ads -> facebook_ad_search; transcribe an ad -> facebook_ad_transcribe; transcribe a
104
- video -> facebook_video_transcribe; page profile/intel -> facebook_page_intel.
105
- - Instagram: profile inventory -> instagram_profile_content; one post or reel -> instagram_media_download.
106
- - Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
107
- AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
108
- - Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
109
- (browser_open, then navigate/read).
110
- - Logged-in sites (ChatGPT, Claude, Reddit, any account): save the login first -> browser_profile_connect
111
- (returns an mcpscraper.dev sign-in link for the user; one profile holds MANY logins, call it again with
112
- the same profile + a new domain to add accounts) -> poll browser_profile_list until AUTHENTICATED ->
113
- browser_open with that profile. browser_profile_list also shows what a profile is connected to.
97
+ Scrapes and analyzes web, search, maps, social, and site data. **Pick a tool by NAME from the map
98
+ below, then load its schema before calling.** Where a tool needs a value another tool produces, the
99
+ seam is noted so you can chain them.
114
100
 
115
- NOTES
116
- - Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
117
- It discovers URLs and returns a saved folder/artifact plus a summary, not the full content inline.
118
- - Tools that open a browser session or transcribe media cost credits and take longer. Prefer the
119
- cheapest tool that answers the question (plain search/extract before browser agents).
120
- - Large results are saved to disk or an artifact and returned as a summary plus a path; read the path
121
- for full detail rather than expecting the whole payload inline.
101
+ ## Search & research
102
+ - Organic Google results, rankings, local pack -> **search_serp**.
103
+ - Full SERP plus People-Also-Ask and AI-Overview detail -> **harvest_paa**.
104
+ - \`search_serp\` returns \`organicResults[].url\` (often including reddit.com threads) \u2014 feed those into
105
+ \`extract_url\` or \`reddit_thread\`.
106
+
107
+ ## Pages & sites
108
+ - One page -> **extract_url** (takes a url).
109
+ - Whole site, crawl + SEO report -> **extract_site** (takes a url).
110
+ - Just the URL list/inventory -> **map_site_urls** (takes a url).
111
+ - \`map_site_urls\` returns urls you can feed straight into \`extract_url\`.
112
+
113
+ ## Google Maps
114
+ - Find multiple places/competitors/prospects -> **maps_search** (returns \`results[]\` with name,
115
+ placeUrl, cid).
116
+ - One business deep-dive + reviews -> **maps_place_intel** (takes businessName + location, NOT an id \u2014
117
+ call it directly with a name from a \`maps_search\` result or from the user).
118
+
119
+ ## YouTube
120
+ - Find or list videos -> **youtube_harvest** (returns \`videos[].videoId\`).
121
+ - Transcribe one video -> **youtube_transcribe** (takes a videoId from \`youtube_harvest\`, or a url).
122
+
123
+ ## Facebook
124
+ - Find advertisers -> **facebook_ad_search** (returns \`advertisers[]\` with pageId, libraryId).
125
+ - One advertiser's ads -> **facebook_page_intel** (takes pageId, libraryId, or query; returns
126
+ \`ads[].videoUrl\`).
127
+ - Transcribe an ad video -> **facebook_ad_transcribe** (takes a videoUrl from \`facebook_page_intel\`).
128
+ - Transcribe an organic reel/video/post -> **facebook_video_transcribe** (takes the Facebook url directly).
129
+
130
+ ## Instagram
131
+ - Profile inventory -> **instagram_profile_content** (returns post/reel/tv urls).
132
+ - One post or reel -> **instagram_media_download** (takes a url from \`instagram_profile_content\`, or a
133
+ url the user gives).
134
+
135
+ ## Reddit
136
+ - A reddit.com thread/post URL -> **reddit_thread**. Returns the post plus its comment tree and handles
137
+ Reddit's bot wall itself (no login needed). Find threads first with \`search_serp\`.
138
+
139
+ ## Other sites & logins (browser agent)
140
+ For an arbitrary site or a logged-in dashboard with no dedicated tool, use the browser_* agent. **First
141
+ decide whether the site needs a login:**
142
+ - **Needs a login** (ChatGPT, Claude, any account) -> save it first: **browser_profile_connect** returns
143
+ an mcpscraper.dev sign-in link for the user; one profile holds MANY logins (call it again with the same
144
+ profile + a new domain to add accounts). Poll **browser_profile_list** until AUTHENTICATED, then
145
+ **browser_open** with that profile.
146
+ - **No login** -> **browser_open**, then navigate/read.
147
+
148
+ ## Workflows
149
+ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when the whole job is one of these:
150
+ - Build a local business directory for a niche across a state/region -> **directory_workflow**.
151
+ - Stand up a rank-tracking blueprint (database + cron + ingestion plan) -> **rank_tracker_workflow**.
152
+ - Find which sources an AI answer cites for a query (AEO) -> **query_fanout_workflow** (open a browser
153
+ session on chatgpt.com or claude.ai FIRST, then run it against that session).
154
+
155
+ ## Notes
156
+ - Bulk / full-site crawls: call \`extract_site\` with \`rotateProxies:true\` for blocked or rate-limited
157
+ sites. It returns a saved folder/artifact plus a summary, not the full content inline.
158
+ - Browser sessions and media transcription cost credits and take longer \u2014 prefer the cheapest tool that
159
+ answers the question (plain search/extract before browser agents).
160
+ - Large results are saved to disk or an artifact and returned as a summary plus a path; read the path for
161
+ full detail rather than expecting the whole payload inline.
122
162
  `.trim();
123
163
 
124
164
  // src/mcp/paa-mcp-server.ts
@@ -144,6 +184,17 @@ var WORKFLOW_RECIPES = [
144
184
  produces: ["city summary", "competitor CSV", "review insight CSV", "market difficulty/opportunity notes", "HTML report"],
145
185
  runHint: "Use workflow_run with workflowId local-competitive-audit for state-wide market batches, or map-comparison for one city/location comparison."
146
186
  },
187
+ {
188
+ id: "lead_generation",
189
+ title: "Lead Generation",
190
+ 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).",
191
+ primaryWorkflowId: "get-leads",
192
+ recommendedTools: ["workflow_run", "maps_search", "maps_place_intel", "extract_url"],
193
+ requiredInputs: ["query", "location"],
194
+ optionalInputs: ["maxResults", "enrichWebsites", "hydrateReviewCounts", "concurrency", "proxyMode"],
195
+ produces: ["leads CSV (name, phone, website, email, socials, review count, booking URL)", "evidence JSON", "HTML report", "outreach task list"],
196
+ 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.'
197
+ },
147
198
  {
148
199
  id: "icp_research",
149
200
  title: "ICP Research",
@@ -1092,6 +1143,16 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
1092
1143
  const bodySectionFull = bodyMd ? `
1093
1144
  ## Page Content
1094
1145
  ${bodyMd}` : "";
1146
+ const mem = d.memory ?? null;
1147
+ const memSection = mem?.deposited ? `
1148
+ ## \u2705 Saved to memory
1149
+ 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 ? `
1150
+ ## \u{1F4C4} Saved as a temporary file
1151
+ Memory vault was unavailable${mem.error ? ` (${mem.error})` : ""}, so the full content was saved to a download link instead:
1152
+ - **Link:** ${mem.fileUrl}
1153
+ - **Expires:** ${mem.fileExpiresAt ?? "in 24 hours"} (auto-deleted)` : mem ? `
1154
+ ## \u26A0\uFE0F Memory deposit skipped
1155
+ ${mem.error ?? "unknown error"} \u2014 the page content is still in the truncated preview above.` : "";
1095
1156
  const screenshotSection = screenshotMeta ? `
1096
1157
  ## Screenshot
1097
1158
  - **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
@@ -1121,10 +1182,10 @@ ${bodyMd}` : "";
1121
1182
  - ${schemaCount} JSON-LD schema block(s) detected`;
1122
1183
  const full = `# URL Extract: ${url}
1123
1184
  **${title}**
1124
- ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
1185
+ ${headingSection}${kpoSection}${brandingSection}${bodySection}${memSection}${screenshotSection}${mediaSection}${tips}`;
1125
1186
  const diskReport = `# URL Extract: ${url}
1126
1187
  **${title}**
1127
- ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
1188
+ ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${memSection}${screenshotSection}${mediaSection}${tips}`;
1128
1189
  const textResult = oneBlock(full, diskReport);
1129
1190
  const structuredContent = {
1130
1191
  url,
@@ -1137,7 +1198,8 @@ ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSe
1137
1198
  missingSchemaFields: kpo?.missingFields ?? [],
1138
1199
  screenshotSaved: screenshotPath ?? null,
1139
1200
  branding: branding ?? null,
1140
- mediaAssets: media?.assets ?? null
1201
+ mediaAssets: media?.assets ?? null,
1202
+ memory: mem ?? void 0
1141
1203
  };
1142
1204
  if (screenshotMeta?.base64) {
1143
1205
  return {
@@ -1523,6 +1585,38 @@ ${adBlocks}`,
1523
1585
  }
1524
1586
  };
1525
1587
  }
1588
+ function formatRedditThread(raw, input) {
1589
+ const parsed = parseData(raw);
1590
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1591
+ const d = parsed.data;
1592
+ const comments = d.comments ?? [];
1593
+ const commentMd = comments.map((cm) => {
1594
+ const indent = " ".repeat(Math.min(Number(cm.depth ?? 0), 6));
1595
+ return `${indent}- **u/${cm.author || "[unknown]"}** (${cm.score || "\u2014"}): ${(cm.body || "").replace(/\s+/g, " ").trim()}`;
1596
+ }).join("\n");
1597
+ const full = [
1598
+ `# ${d.title || "Reddit thread"}`,
1599
+ `**u/${d.author || "[unknown]"}** \xB7 ${d.score || "\u2014"} \xB7 ${comments.length} comments captured`,
1600
+ d.postBody ? `
1601
+ ${d.postBody}` : "",
1602
+ `
1603
+ ## Comments
1604
+ ${commentMd || "_No comments captured._"}`
1605
+ ].join("\n");
1606
+ return {
1607
+ ...oneBlock(full),
1608
+ structuredContent: {
1609
+ sourceUrl: d.sourceUrl ?? input.url ?? null,
1610
+ oldRedditUrl: d.oldRedditUrl ?? null,
1611
+ title: d.title ?? null,
1612
+ author: d.author ?? null,
1613
+ score: d.score ?? null,
1614
+ postBody: d.postBody ?? null,
1615
+ numComments: comments.length,
1616
+ comments: comments.map((cm) => ({ author: cm.author ?? null, score: cm.score ?? null, depth: Number(cm.depth ?? 0), body: cm.body ?? "" }))
1617
+ }
1618
+ };
1619
+ }
1526
1620
  function formatFacebookAdSearch(raw, input) {
1527
1621
  const parsed = parseData(raw);
1528
1622
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -2551,7 +2645,7 @@ var HarvestPaaInputSchema = {
2551
2645
  gl: 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."),
2552
2646
  hl: 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."),
2553
2647
  device: z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
2554
- proxyMode: 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."),
2648
+ proxyMode: 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.'),
2555
2649
  proxyZip: 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."),
2556
2650
  debug: z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
2557
2651
  };
@@ -2562,7 +2656,9 @@ var ExtractUrlInputSchema = {
2562
2656
  extractBranding: 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."),
2563
2657
  downloadMedia: 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."),
2564
2658
  mediaTypes: z.array(z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
2565
- allowLocal: z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
2659
+ allowLocal: z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only."),
2660
+ depositToVault: 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."),
2661
+ vaultName: z.string().trim().min(1).max(120).optional().describe("Optional vault to deposit into. Defaults to the user's personal vault.")
2566
2662
  };
2567
2663
  var MapSiteUrlsInputSchema = {
2568
2664
  url: 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."),
@@ -2603,6 +2699,10 @@ var FacebookAdSearchInputSchema = {
2603
2699
  country: z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
2604
2700
  maxResults: 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.")
2605
2701
  };
2702
+ var RedditThreadInputSchema = {
2703
+ url: 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."),
2704
+ maxComments: z.number().int().min(1).max(2e3).optional().describe("Optional cap on comments returned. Omit to return all captured comments.")
2705
+ };
2606
2706
  var FacebookAdTranscribeInputSchema = {
2607
2707
  videoUrl: 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.")
2608
2708
  };
@@ -2869,7 +2969,17 @@ var ExtractUrlOutputSchema = {
2869
2969
  entityTypes: z.array(z.string()),
2870
2970
  napScore: z.number().nullable(),
2871
2971
  missingSchemaFields: z.array(z.string()),
2872
- screenshotSaved: NullableString
2972
+ screenshotSaved: NullableString,
2973
+ memory: z.object({
2974
+ deposited: z.boolean(),
2975
+ vault: z.string().optional(),
2976
+ noteId: z.string().optional(),
2977
+ path: z.string().optional(),
2978
+ chunks: z.number().int().optional(),
2979
+ fileUrl: z.string().optional(),
2980
+ fileExpiresAt: z.string().optional(),
2981
+ error: z.string().optional()
2982
+ }).optional()
2873
2983
  };
2874
2984
  var ExtractSiteOutputSchema = {
2875
2985
  url: z.string(),
@@ -3001,6 +3111,21 @@ var FacebookAdSearchOutputSchema = {
3001
3111
  sampleLibraryId: NullableString
3002
3112
  }))
3003
3113
  };
3114
+ var RedditThreadOutputSchema = {
3115
+ sourceUrl: NullableString,
3116
+ oldRedditUrl: NullableString,
3117
+ title: NullableString,
3118
+ author: NullableString,
3119
+ score: NullableString,
3120
+ postBody: NullableString,
3121
+ numComments: z.number().int().min(0),
3122
+ comments: z.array(z.object({
3123
+ author: NullableString,
3124
+ score: NullableString,
3125
+ depth: z.number().int().min(0),
3126
+ body: z.string()
3127
+ }))
3128
+ };
3004
3129
  var FacebookPageIntelOutputSchema = {
3005
3130
  advertiserName: NullableString,
3006
3131
  totalAds: z.number().int().min(0),
@@ -3194,6 +3319,7 @@ var CreditsInfoInputSchema = {
3194
3319
  };
3195
3320
  var WorkflowIdSchema = z.enum([
3196
3321
  "directory",
3322
+ "get-leads",
3197
3323
  "agent-packet",
3198
3324
  "local-competitive-audit",
3199
3325
  "map-comparison",
@@ -3215,8 +3341,8 @@ var WorkflowSuggestInputSchema = {
3215
3341
  maxSuggestions: z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
3216
3342
  };
3217
3343
  var WorkflowRunInputSchema = {
3218
- 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."),
3219
- input: z.record(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?}."),
3344
+ 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."),
3345
+ input: z.record(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?}."),
3220
3346
  webhookUrl: z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
3221
3347
  };
3222
3348
  var WorkflowStepInputSchema = {
@@ -3292,7 +3418,7 @@ var SearchSerpInputSchema = {
3292
3418
  gl: z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
3293
3419
  hl: z.string().default("en").describe("Google interface/content language inferred from user request."),
3294
3420
  device: z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
3295
- proxyMode: 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."),
3421
+ proxyMode: z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic. Set "location" only to force local-pack targeting.'),
3296
3422
  proxyZip: 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."),
3297
3423
  debug: z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
3298
3424
  pages: z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
@@ -3303,7 +3429,7 @@ var CaptureSerpSnapshotInputSchema = {
3303
3429
  gl: z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
3304
3430
  hl: z.string().default("en").describe("Google interface/content language inferred from the user request."),
3305
3431
  device: z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
3306
- proxyMode: 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."),
3432
+ proxyMode: z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic. Set "location" only to force city/ZIP targeting.'),
3307
3433
  proxyZip: 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."),
3308
3434
  pages: 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."),
3309
3435
  debug: z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
@@ -3715,21 +3841,21 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
3715
3841
  if (savesReports) registerSavedReportResources(server);
3716
3842
  server.registerTool("harvest_paa", {
3717
3843
  title: "Google PAA + SERP Harvest",
3718
- 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.'),
3844
+ 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.'),
3719
3845
  inputSchema: HarvestPaaInputSchema,
3720
3846
  outputSchema: HarvestPaaOutputSchema,
3721
3847
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
3722
3848
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
3723
3849
  server.registerTool("search_serp", {
3724
3850
  title: "Google SERP Lookup",
3725
- 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."),
3851
+ 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."),
3726
3852
  inputSchema: SearchSerpInputSchema,
3727
3853
  outputSchema: SearchSerpOutputSchema,
3728
3854
  annotations: liveWebToolAnnotations("Google SERP Lookup")
3729
3855
  }, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
3730
3856
  server.registerTool("extract_url", {
3731
3857
  title: "Single URL Extract",
3732
- 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."),
3858
+ 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."),
3733
3859
  inputSchema: ExtractUrlInputSchema,
3734
3860
  outputSchema: ExtractUrlOutputSchema,
3735
3861
  annotations: liveWebToolAnnotations("Single URL Extract")
@@ -3783,6 +3909,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
3783
3909
  outputSchema: FacebookAdSearchOutputSchema,
3784
3910
  annotations: liveWebToolAnnotations("Facebook Ad Library Search")
3785
3911
  }, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
3912
+ server.registerTool("reddit_thread", {
3913
+ title: "Reddit Thread + Comments",
3914
+ 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."),
3915
+ inputSchema: RedditThreadInputSchema,
3916
+ outputSchema: RedditThreadOutputSchema,
3917
+ annotations: liveWebToolAnnotations("Reddit Thread + Comments")
3918
+ }, async (input) => formatRedditThread(await executor.redditThread(input), input));
3786
3919
  server.registerTool("facebook_ad_transcribe", {
3787
3920
  title: "Facebook Ad Transcription",
3788
3921
  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.",
@@ -3820,14 +3953,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
3820
3953
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
3821
3954
  server.registerTool("maps_search", {
3822
3955
  title: "Google Maps Business Search",
3823
- 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.'),
3956
+ 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.'),
3824
3957
  inputSchema: MapsSearchInputSchema,
3825
3958
  outputSchema: MapsSearchOutputSchema,
3826
3959
  annotations: liveWebToolAnnotations("Google Maps Business Search")
3827
3960
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
3828
3961
  server.registerTool("directory_workflow", {
3829
3962
  title: "Directory Workflow: Markets + Maps",
3830
- 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.'),
3963
+ 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.'),
3831
3964
  inputSchema: DirectoryWorkflowInputSchema,
3832
3965
  outputSchema: DirectoryWorkflowOutputSchema,
3833
3966
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
@@ -4100,6 +4233,9 @@ var HttpMcpToolExecutor = class {
4100
4233
  facebookAdSearch(input) {
4101
4234
  return this.call("/facebook/search", input);
4102
4235
  }
4236
+ redditThread(input) {
4237
+ return this.call("/reddit/thread", input, this.httpTimeoutOverrideMs ?? 24e4);
4238
+ }
4103
4239
  facebookAdTranscribe(input) {
4104
4240
  return this.call("/facebook/transcribe", input);
4105
4241
  }
@@ -4315,4 +4451,4 @@ export {
4315
4451
  HttpMcpToolExecutor,
4316
4452
  exportFanout
4317
4453
  };
4318
- //# sourceMappingURL=chunk-GZ4W72WF.js.map
4454
+ //# sourceMappingURL=chunk-2GHI2D6Q.js.map