mcp-scraper 0.2.7 → 0.2.8

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 (42) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/api-server.cjs +364 -162
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +1 -1
  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-combined-stdio-server.cjs +68 -13
  9. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -3
  11. package/dist/bin/mcp-scraper-install.cjs +2 -1
  12. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  13. package/dist/bin/mcp-scraper-install.js +2 -1
  14. package/dist/bin/mcp-scraper-install.js.map +1 -1
  15. package/dist/bin/mcp-stdio-server.cjs +68 -13
  16. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  17. package/dist/bin/mcp-stdio-server.js +2 -2
  18. package/dist/bin/paa-harvest.cjs +42 -6
  19. package/dist/bin/paa-harvest.cjs.map +1 -1
  20. package/dist/bin/paa-harvest.js +1 -1
  21. package/dist/{chunk-3TF6UT2P.js → chunk-5HMOPP76.js} +2 -2
  22. package/dist/{chunk-7SI6XIR3.js → chunk-6NEXSNSA.js} +69 -14
  23. package/dist/chunk-6NEXSNSA.js.map +1 -0
  24. package/dist/{chunk-MY3S7EX7.js → chunk-CQTAKXBN.js} +43 -7
  25. package/dist/chunk-CQTAKXBN.js.map +1 -0
  26. package/dist/chunk-I26QN7WQ.js +7 -0
  27. package/dist/chunk-I26QN7WQ.js.map +1 -0
  28. package/dist/index.cjs +42 -6
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/{server-RIKRBDOI.js → server-BTTDFPSQ.js} +238 -133
  32. package/dist/server-BTTDFPSQ.js.map +1 -0
  33. package/dist/{worker-NAKGTIF5.js → worker-5O44YBF4.js} +2 -2
  34. package/docs/mcp-tool-quality-spec.md +3 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-7SI6XIR3.js.map +0 -1
  37. package/dist/chunk-IZE5UW7Y.js +0 -7
  38. package/dist/chunk-IZE5UW7Y.js.map +0 -1
  39. package/dist/chunk-MY3S7EX7.js.map +0 -1
  40. package/dist/server-RIKRBDOI.js.map +0 -1
  41. /package/dist/{chunk-3TF6UT2P.js.map → chunk-5HMOPP76.js.map} +0 -0
  42. /package/dist/{worker-NAKGTIF5.js.map → worker-5O44YBF4.js.map} +0 -0
@@ -138,7 +138,7 @@ var import_node_os2 = require("os");
138
138
  var import_node_path2 = require("path");
139
139
 
140
140
  // src/version.ts
141
- var PACKAGE_VERSION = "0.2.7";
141
+ var PACKAGE_VERSION = "0.2.8";
142
142
 
143
143
  // src/mcp/browser-agent-tool-schemas.ts
144
144
  var import_zod = require("zod");
@@ -1056,12 +1056,17 @@ function errorAttemptsSection(body) {
1056
1056
  const browser = debug.browser ?? {};
1057
1057
  const kernel = browser.browserRuntime ?? browser.kernel ?? {};
1058
1058
  const proxyResolution = kernel.proxyResolution ?? {};
1059
- const network = browser.networkLocation ?? {};
1059
+ const network = browser.networkLocation ?? {
1060
+ ip: attempt.observedIp ?? attempt.observed_ip,
1061
+ city: attempt.observedCity ?? attempt.observed_city,
1062
+ region: attempt.observedRegion ?? attempt.observed_region
1063
+ };
1060
1064
  const nav = browser.serpNavigation ?? {};
1061
1065
  const geo = [network.ip, network.city, network.region].filter(Boolean).join(" / ") || "geo unknown";
1062
- const sessionId = attempt.browser_session_id ?? attempt.kernel_session_id ?? kernel.sessionId ?? "unknown";
1066
+ const sessionId = attempt.browser_session_id ?? attempt.browserSessionIdSuffix ?? attempt.kernel_session_id ?? kernel.sessionId ?? "unknown";
1063
1067
  const cleanupSucceeded = attempt.session_cleanup_succeeded ?? attempt.kernel_delete_succeeded;
1064
- return `- Attempt ${attempt.attempt_number ?? "?"}: ${attempt.outcome ?? attempt.status ?? "unknown"} \xB7 session ${sessionId} \xB7 proxy ${debug.request?.proxyMode ?? kernel.proxyMode ?? "unknown"}${proxyResolution.source ? `/${proxyResolution.source}` : ""} \xB7 ${geo} \xB7 CAPTCHA ${nav.captchaDetected === true ? "yes" : nav.captchaDetected === false ? "no" : "unknown"} \xB7 cleanup ${cleanupSucceeded === true ? "yes" : cleanupSucceeded === false ? "no" : "unknown"}`;
1068
+ const proxySource = proxyResolution.source ?? attempt.proxyResolutionSource;
1069
+ return `- Attempt ${attempt.attempt_number ?? attempt.attemptNumber ?? "?"}: ${attempt.outcome ?? attempt.status ?? "unknown"} \xB7 session ${sessionId} \xB7 proxy ${debug.request?.proxyMode ?? kernel.proxyMode ?? attempt.proxyMode ?? "unknown"}${proxySource ? `/${proxySource}` : ""} \xB7 ${geo} \xB7 CAPTCHA ${nav.captchaDetected === true ? "yes" : nav.captchaDetected === false ? "no" : "unknown"} \xB7 cleanup ${cleanupSucceeded === true ? "yes" : cleanupSucceeded === false ? "no" : "unknown"}`;
1065
1070
  });
1066
1071
  return `
1067
1072
 
@@ -1522,6 +1527,29 @@ ${rows}`,
1522
1527
  }
1523
1528
  };
1524
1529
  }
1530
+ function normalizeMapsAttempts(value) {
1531
+ const attempts = Array.isArray(value) ? value : [];
1532
+ return attempts.map((attempt, index) => ({
1533
+ attemptNumber: attempt.attemptNumber ?? attempt.attempt_number ?? index + 1,
1534
+ maxAttempts: attempt.maxAttempts ?? attempt.max_attempts ?? attempts.length,
1535
+ status: attempt.status === "ok" ? "ok" : "failed",
1536
+ outcome: attempt.outcome ?? attempt.status ?? "unknown",
1537
+ willRetry: attempt.willRetry ?? attempt.will_retry ?? false,
1538
+ durationMs: attempt.durationMs ?? attempt.duration_ms ?? 0,
1539
+ resultCount: attempt.resultCount ?? attempt.result_count ?? 0,
1540
+ error: attempt.error ? sanitizeVendorText(attempt.error) : null,
1541
+ proxyMode: attempt.proxyMode ?? attempt.proxy_mode ?? "location",
1542
+ proxyResolutionSource: attempt.proxyResolutionSource ?? attempt.proxy_resolution_source ?? null,
1543
+ proxyIdSuffix: attempt.proxyIdSuffix ?? attempt.proxy_id_suffix ?? null,
1544
+ proxyTargetLevel: attempt.proxyTargetLevel ?? attempt.proxy_target_level ?? null,
1545
+ proxyTargetLocation: attempt.proxyTargetLocation ?? attempt.proxy_target_location ?? null,
1546
+ proxyTargetZip: attempt.proxyTargetZip ?? attempt.proxy_target_zip ?? null,
1547
+ browserSessionIdSuffix: attempt.browserSessionIdSuffix ?? attempt.browser_session_id ?? null,
1548
+ observedIp: attempt.observedIp ?? attempt.observed_ip ?? null,
1549
+ observedCity: attempt.observedCity ?? attempt.observed_city ?? null,
1550
+ observedRegion: attempt.observedRegion ?? attempt.observed_region ?? null
1551
+ }));
1552
+ }
1525
1553
  function formatCreditsInfo(raw, input) {
1526
1554
  const parsed = parseData(raw);
1527
1555
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -1594,6 +1622,8 @@ function formatMapsSearch(raw, input) {
1594
1622
  const searchQuery = d.searchQuery ?? [input.query, input.location].filter(Boolean).join(" ");
1595
1623
  const requestedMax = d.requestedMaxResults ?? input.maxResults ?? 10;
1596
1624
  const durationMs = d.durationMs;
1625
+ const attempts = normalizeMapsAttempts(d.attempts);
1626
+ const lastAttempt = attempts.at(-1);
1597
1627
  const rows = results.map((r) => {
1598
1628
  const rating = [r.rating, r.reviewCount ? `(${r.reviewCount})` : null].filter(Boolean).join(" ");
1599
1629
  return `| ${r.position} | ${cell(r.name)} | ${cell(r.category)} | ${cell(rating)} | ${cell(r.address)} | ${r.cidDecimal ? `\`${r.cidDecimal}\`` : "\u2014"} | ${r.websiteUrl ? `[site](${r.websiteUrl})` : "\u2014"} | [maps](${r.placeUrl}) |`;
@@ -1608,6 +1638,7 @@ ${meta}`;
1608
1638
  const full = [
1609
1639
  `# Google Maps Search: "${searchQuery}"`,
1610
1640
  `**Returned:** ${results.length} profile candidate${results.length === 1 ? "" : "s"} \xB7 **Requested max:** ${requestedMax} \xB7 **Limit:** 50`,
1641
+ attempts.length ? `**Attempts:** ${attempts.length}/${lastAttempt?.maxAttempts ?? attempts.length} \xB7 **Proxy:** ${lastAttempt?.proxyMode ?? "unknown"}${lastAttempt?.proxyResolutionSource ? `/${lastAttempt.proxyResolutionSource}` : ""} \xB7 **Observed:** ${[lastAttempt?.observedCity, lastAttempt?.observedRegion].filter(Boolean).join(", ") || "unknown"}` : null,
1611
1642
  `
1612
1643
  ## Results
1613
1644
  | # | Name | Category | Rating | Address | CID | Website | Maps |
@@ -1631,6 +1662,7 @@ ${rows}`,
1631
1662
  requestedMaxResults: requestedMax,
1632
1663
  resultCount: results.length,
1633
1664
  results: normalizedResults,
1665
+ attempts,
1634
1666
  durationMs: durationMs ?? 0
1635
1667
  }
1636
1668
  };
@@ -1641,6 +1673,7 @@ function formatDirectoryWorkflow(raw, input) {
1641
1673
  const d = parsed.data;
1642
1674
  const cities = (d.cities ?? []).map((city) => ({
1643
1675
  ...city,
1676
+ attempts: normalizeMapsAttempts(city.attempts),
1644
1677
  results: city.results.map((result) => ({
1645
1678
  ...result,
1646
1679
  phone: result.phone ?? null,
@@ -1868,7 +1901,7 @@ var HarvestPaaInputSchema = {
1868
1901
  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."),
1869
1902
  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."),
1870
1903
  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."),
1871
- 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 and retries CAPTCHA, proxy tunnel failure, and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
1904
+ 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."),
1872
1905
  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."),
1873
1906
  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.")
1874
1907
  };
@@ -1927,7 +1960,7 @@ var MapsSearchInputSchema = {
1927
1960
  gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location."),
1928
1961
  hl: import_zod2.z.string().length(2).default("en").describe("Language inferred from user request."),
1929
1962
  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."),
1930
- proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state Maps searches; it creates a fresh residential proxy ID when the browser service is available. Use configured for the server proxy ID, and none only for local direct-network debugging."),
1963
+ 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."),
1931
1964
  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."),
1932
1965
  debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
1933
1966
  };
@@ -1942,7 +1975,7 @@ var DirectoryWorkflowInputSchema = {
1942
1975
  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."),
1943
1976
  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."),
1944
1977
  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, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
1945
- 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; it creates fresh residential proxy IDs when the browser service is available. Use configured for the server proxy ID, and none only for local direct-network debugging."),
1978
+ 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."),
1946
1979
  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."),
1947
1980
  debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
1948
1981
  };
@@ -1965,6 +1998,26 @@ var RankTrackerBlueprintInputSchema = {
1965
1998
  notes: import_zod2.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
1966
1999
  };
1967
2000
  var NullableString = import_zod2.z.string().nullable();
2001
+ var MapsSearchAttemptOutput = import_zod2.z.object({
2002
+ attemptNumber: import_zod2.z.number().int().min(1),
2003
+ maxAttempts: import_zod2.z.number().int().min(1),
2004
+ status: import_zod2.z.enum(["ok", "failed"]),
2005
+ outcome: import_zod2.z.string(),
2006
+ willRetry: import_zod2.z.boolean(),
2007
+ durationMs: import_zod2.z.number().int().min(0),
2008
+ resultCount: import_zod2.z.number().int().min(0),
2009
+ error: NullableString,
2010
+ proxyMode: import_zod2.z.enum(["location", "configured", "none"]),
2011
+ proxyResolutionSource: import_zod2.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
2012
+ proxyIdSuffix: NullableString,
2013
+ proxyTargetLevel: import_zod2.z.enum(["zip", "city", "state"]).nullable(),
2014
+ proxyTargetLocation: NullableString,
2015
+ proxyTargetZip: NullableString,
2016
+ browserSessionIdSuffix: NullableString,
2017
+ observedIp: NullableString,
2018
+ observedCity: NullableString,
2019
+ observedRegion: NullableString
2020
+ });
1968
2021
  var MapsSearchOutputSchema = {
1969
2022
  query: import_zod2.z.string(),
1970
2023
  location: import_zod2.z.string().nullable(),
@@ -1989,6 +2042,7 @@ var MapsSearchOutputSchema = {
1989
2042
  directionsUrl: NullableString,
1990
2043
  metadata: import_zod2.z.array(import_zod2.z.string())
1991
2044
  })),
2045
+ attempts: import_zod2.z.array(MapsSearchAttemptOutput),
1992
2046
  durationMs: import_zod2.z.number().int().min(0)
1993
2047
  };
1994
2048
  var DirectoryMapsBusinessOutput = import_zod2.z.object({
@@ -2035,6 +2089,7 @@ var DirectoryWorkflowOutputSchema = {
2035
2089
  error: NullableString,
2036
2090
  resultCount: import_zod2.z.number().int().min(0),
2037
2091
  durationMs: import_zod2.z.number().int().min(0),
2092
+ attempts: import_zod2.z.array(MapsSearchAttemptOutput),
2038
2093
  results: import_zod2.z.array(DirectoryMapsBusinessOutput)
2039
2094
  })),
2040
2095
  durationMs: import_zod2.z.number().int().min(0)
@@ -2251,7 +2306,7 @@ var SearchSerpInputSchema = {
2251
2306
  gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
2252
2307
  hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from user request."),
2253
2308
  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."),
2254
- 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 and retries CAPTCHA, proxy tunnel failure, and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
2309
+ 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."),
2255
2310
  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."),
2256
2311
  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."),
2257
2312
  pages: import_zod2.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
@@ -2262,7 +2317,7 @@ var CaptureSerpSnapshotInputSchema = {
2262
2317
  gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
2263
2318
  hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from the user request."),
2264
2319
  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."),
2265
- 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 and retries CAPTCHA, proxy tunnel failure, and wrong-location evidence before returning. Use configured only for the static residential proxy, and none only for direct-network debugging."),
2320
+ 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."),
2266
2321
  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."),
2267
2322
  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."),
2268
2323
  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."),
@@ -2669,14 +2724,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
2669
2724
  if (savesReports) registerSavedReportResources(server2);
2670
2725
  server2.registerTool("harvest_paa", {
2671
2726
  title: "Google PAA + SERP Harvest",
2672
- 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 and rejects wrong-location evidence instead of returning a bad market. 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.'),
2727
+ 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.'),
2673
2728
  inputSchema: HarvestPaaInputSchema,
2674
2729
  outputSchema: HarvestPaaOutputSchema,
2675
2730
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
2676
2731
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
2677
2732
  server2.registerTool("search_serp", {
2678
2733
  title: "Google SERP Lookup",
2679
- 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 and retries CAPTCHA, proxy tunnel failures, and wrong-location evidence before returning."),
2734
+ 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."),
2680
2735
  inputSchema: SearchSerpInputSchema,
2681
2736
  outputSchema: SearchSerpOutputSchema,
2682
2737
  annotations: liveWebToolAnnotations("Google SERP Lookup")
@@ -2744,14 +2799,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
2744
2799
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
2745
2800
  server2.registerTool("maps_search", {
2746
2801
  title: "Google Maps Business Search",
2747
- 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 can create a fresh residential proxy ID for that market; 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, and profile metadata. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
2802
+ 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.'),
2748
2803
  inputSchema: MapsSearchInputSchema,
2749
2804
  outputSchema: MapsSearchOutputSchema,
2750
2805
  annotations: liveWebToolAnnotations("Google Maps Business Search")
2751
2806
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
2752
2807
  server2.registerTool("directory_workflow", {
2753
2808
  title: "Directory Workflow: Markets + Maps",
2754
- 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 can use a fresh residential proxy ID when the browser service is available; retryable city failures use fresh proxies across attempts. Saved CSV rows include source_location, result_position, business_name, review_stars, category, address, phone, hours_status, website_url, directions_url, place_url, cid, cid_decimal, city population, and ZIP groups. This workflow captures star ratings from Maps list cards, not profile review counts; use maps_place_intel only when a selected profile needs deeper review details. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
2809
+ 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, 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. This workflow captures star ratings from Maps list cards, not profile review counts; use maps_place_intel only when a selected profile needs deeper review details. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
2755
2810
  inputSchema: DirectoryWorkflowInputSchema,
2756
2811
  outputSchema: DirectoryWorkflowOutputSchema,
2757
2812
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")