mcp-scraper 0.31.0 → 0.32.0

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 +1 -1
  2. package/dist/bin/api-server.cjs +55 -11
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/mcp-scraper-cli.cjs +4 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +3 -3
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +9 -6
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +3 -3
  14. package/dist/bin/paa-harvest.cjs +26 -4
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +2 -2
  17. package/dist/{chunk-KAMKTSJQ.js → chunk-55M5CE5L.js} +2 -2
  18. package/dist/chunk-CPQF3PYW.js +7 -0
  19. package/dist/chunk-CPQF3PYW.js.map +1 -0
  20. package/dist/{chunk-ZQSHKWV4.js → chunk-PTHY7NYD.js} +4 -1
  21. package/dist/chunk-PTHY7NYD.js.map +1 -0
  22. package/dist/{chunk-YH5DRFWQ.js → chunk-RDCCU7CQ.js} +25 -6
  23. package/dist/chunk-RDCCU7CQ.js.map +1 -0
  24. package/dist/{chunk-ZOXL3VWO.js → chunk-YOOEAXWP.js} +8 -8
  25. package/dist/chunk-YOOEAXWP.js.map +1 -0
  26. package/dist/index.cjs +26 -4
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +9 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.js +2 -2
  31. package/dist/{server-DS3ZSNNO.js → server-OBOTNFD6.js} +31 -8
  32. package/dist/{server-DS3ZSNNO.js.map → server-OBOTNFD6.js.map} +1 -1
  33. package/dist/{worker-RYP5EOV5.js → worker-7DJYUACV.js} +3 -3
  34. package/docs/mcp-tool-manifest.generated.json +7 -7
  35. package/package.json +1 -1
  36. package/dist/chunk-W3GYPHFL.js +0 -7
  37. package/dist/chunk-W3GYPHFL.js.map +0 -1
  38. package/dist/chunk-YH5DRFWQ.js.map +0 -1
  39. package/dist/chunk-ZOXL3VWO.js.map +0 -1
  40. package/dist/chunk-ZQSHKWV4.js.map +0 -1
  41. /package/dist/{chunk-KAMKTSJQ.js.map → chunk-55M5CE5L.js.map} +0 -0
  42. /package/dist/{worker-RYP5EOV5.js.map → worker-7DJYUACV.js.map} +0 -0
package/README.md CHANGED
@@ -88,7 +88,7 @@ Build the branded one-click bundle:
88
88
  npm run build:mcpb
89
89
  ```
90
90
 
91
- The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.30.0`, SHA-256 `848ebe2ac12ea4fdd08ae95d368ca537eaf8a1bbd32456baff7a8c2c817e457b`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, and API-key configuration field from the bundle manifest.
91
+ The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.32.0`, SHA-256 `f31500fa60ffa96d2bea43feb4f5606a40fdc59b599dd51a11b8a54e5c9bce32`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, and API-key configuration field from the bundle manifest.
92
92
 
93
93
  The MCPB install exposes every tool — web-intelligence plus all `browser_*` tools — through the one `mcp-scraper` server.
94
94
 
@@ -13834,6 +13834,15 @@ function positiveIntFromEnv(name, fallback) {
13834
13834
  function proxyIdSuffix(proxyId) {
13835
13835
  return proxyId ? proxyId.slice(-6) : null;
13836
13836
  }
13837
+ function serpArrivalUrl(googleUrl) {
13838
+ const base = process.env.SERP_REDIRECT_BASE?.trim().replace(/\/+$/, "");
13839
+ if (!base) return googleUrl;
13840
+ return `${base}/g/${Buffer.from(googleUrl, "utf8").toString("base64url")}`;
13841
+ }
13842
+ function isUsCountry(country) {
13843
+ if (!country) return false;
13844
+ return /united states|u\.?s\.?a?\.?|^us$/i.test(country.trim());
13845
+ }
13837
13846
  function errorText(err) {
13838
13847
  return err instanceof Error ? err.message : String(err);
13839
13848
  }
@@ -13988,7 +13997,7 @@ var init_BrowserDriver = __esm({
13988
13997
  this.kernelProxyType = config.kernelProxyResolution?.proxyType ?? null;
13989
13998
  let defaultProxyDisabled = null;
13990
13999
  let defaultProxyDisableError = null;
13991
- if (!config.kernelProxyId) {
14000
+ if (!config.kernelProxyId && !config.keepDefaultProxy) {
13992
14001
  try {
13993
14002
  await withTimeout2(
13994
14003
  this.kernelClient.browsers.update(this.kernelSessionId, { disable_default_proxy: true }),
@@ -14038,9 +14047,15 @@ var init_BrowserDriver = __esm({
14038
14047
  await this.installEsbuildHelperShims(this.context);
14039
14048
  this.page = await this.context.newPage();
14040
14049
  await this.page.setViewportSize(config.viewport);
14041
- if (this.debugEnabled) {
14050
+ if (this.debugEnabled || config.requireUsEgress) {
14042
14051
  this.debugSnapshot.networkLocation = await this.captureBrowserNetworkLocation();
14043
14052
  }
14053
+ if (config.requireUsEgress) {
14054
+ const country = this.debugSnapshot.networkLocation?.country ?? null;
14055
+ if (!isUsCountry(country)) {
14056
+ throw new CaptchaError(`Non-US egress (${country ?? "unknown"}) \u2014 retrying with a fresh session`);
14057
+ }
14058
+ }
14044
14059
  return;
14045
14060
  }
14046
14061
  const launchOpts = {
@@ -14180,6 +14195,7 @@ var init_BrowserDriver = __esm({
14180
14195
  if (options?.num) params.set("num", String(options.num));
14181
14196
  if (uule) params.set("uule", uule);
14182
14197
  const url = "https://www.google.com/search?" + params.toString();
14198
+ const navUrl = serpArrivalUrl(url);
14183
14199
  const navDebug = options?.debug ? {
14184
14200
  requestedUrl: url,
14185
14201
  finalUrl: null,
@@ -14192,7 +14208,7 @@ var init_BrowserDriver = __esm({
14192
14208
  } : null;
14193
14209
  if (navDebug) this.debugSnapshot.serpNavigation = navDebug;
14194
14210
  try {
14195
- await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
14211
+ await this.page.goto(navUrl, { waitUntil: "domcontentloaded", timeout: 45e3 });
14196
14212
  } catch (err) {
14197
14213
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: null, captchaDetected: null });
14198
14214
  const diag = await this.captureDiagnostics(url);
@@ -17339,6 +17355,9 @@ var init_schemas3 = __esm({
17339
17355
  device: import_zod17.z.enum(["desktop", "mobile"]).default("desktop"),
17340
17356
  proxyMode: import_zod17.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE),
17341
17357
  proxyZip: import_zod17.z.string().regex(/^\d{5}$/).optional(),
17358
+ keepDefaultProxy: import_zod17.z.boolean().optional(),
17359
+ requireUsEgress: import_zod17.z.boolean().optional(),
17360
+ maxAttempts: import_zod17.z.number().int().min(1).max(12).optional(),
17342
17361
  debug: import_zod17.z.boolean().default(false),
17343
17362
  depth: import_zod17.z.number().int().min(1).max(30).default(3),
17344
17363
  maxQuestions: import_zod17.z.number().int().min(1).max(1e3).default(100),
@@ -19235,6 +19254,12 @@ function buildGoogleLocalResultsUrl(input) {
19235
19254
  if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
19236
19255
  return `https://www.google.com/search?${params.toString()}`;
19237
19256
  }
19257
+ function buildGoogleOrganicSearchUrl(input) {
19258
+ const query = [input.query, input.location].filter(Boolean).join(" ");
19259
+ const params = new URLSearchParams({ q: query, gl: input.gl, hl: input.hl, pws: "0" });
19260
+ if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
19261
+ return `https://www.google.com/search?${params.toString()}`;
19262
+ }
19238
19263
  var LOCAL_RESULTS_PAGE_LIMIT, LOCAL_RESULTS_WAIT_MS, MapsSearchExtractor;
19239
19264
  var init_MapsSearchExtractor = __esm({
19240
19265
  "src/extractor/MapsSearchExtractor.ts"() {
@@ -19251,13 +19276,14 @@ var init_MapsSearchExtractor = __esm({
19251
19276
  async extract(options) {
19252
19277
  const startMs = Date.now();
19253
19278
  const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
19254
- const searchUrl = buildGoogleLocalResultsUrl(options);
19279
+ const searchUrl = buildGoogleOrganicSearchUrl(options);
19255
19280
  const config = {
19256
19281
  headless: options.headless,
19257
19282
  kernelApiKey: options.kernelApiKey,
19258
19283
  kernelProxyId: options.kernelProxyId,
19259
19284
  kernelProxyResolution: options.kernelProxyResolution,
19260
19285
  proxyMode: options.proxyMode,
19286
+ keepDefaultProxy: options.proxyMode !== "location" && options.proxyMode !== "configured" && !options.kernelProxyId,
19261
19287
  viewport: { width: 1280, height: 900 },
19262
19288
  locale: `${options.hl}-${options.gl.toUpperCase()}`,
19263
19289
  debug: options.debug
@@ -19268,6 +19294,10 @@ var init_MapsSearchExtractor = __esm({
19268
19294
  await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
19269
19295
  await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
19270
19296
  if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
19297
+ const reachedFinder = await this.clickIntoLocalFinder(page);
19298
+ if (!reachedFinder) throw new ExtractionError('Google "More businesses" control was not found on the organic results page');
19299
+ await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
19300
+ if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
19271
19301
  const results = await this.collectResults(page, options);
19272
19302
  return {
19273
19303
  query: options.query,
@@ -19284,6 +19314,17 @@ var init_MapsSearchExtractor = __esm({
19284
19314
  await this.driver.close();
19285
19315
  }
19286
19316
  }
19317
+ async clickIntoLocalFinder(page) {
19318
+ const control = page.getByRole("link", { name: /more (businesses|places|results)/i }).or(page.getByRole("button", { name: /more (businesses|places|results)/i })).first();
19319
+ const visible = await control.isVisible({ timeout: 1e4 }).catch(() => false);
19320
+ if (!visible) return false;
19321
+ await control.click({ timeout: 1e4 }).catch(() => {
19322
+ });
19323
+ await page.waitForTimeout(5e3);
19324
+ return page.evaluate(
19325
+ () => /[?&]udm=1/.test(location.href) || document.querySelectorAll('a[href*="/maps/place/"]').length > 0
19326
+ );
19327
+ }
19287
19328
  async detectBlock(page) {
19288
19329
  return page.evaluate(() => {
19289
19330
  const text2 = document.body.innerText.slice(0, 2e3);
@@ -28145,6 +28186,8 @@ var init_PAAExtractor = __esm({
28145
28186
  kernelProxyId: options.kernelProxyId,
28146
28187
  kernelProxyResolution: options.kernelProxyResolution,
28147
28188
  proxyMode: options.proxyMode,
28189
+ keepDefaultProxy: options.keepDefaultProxy,
28190
+ requireUsEgress: options.requireUsEgress,
28148
28191
  viewport: isMobile ? { width: 390, height: 844 } : { width: 1280, height: 800 },
28149
28192
  locale: `${options.hl}-${options.gl.toUpperCase()}`,
28150
28193
  userAgent: isMobile ? MOBILE_USER_AGENT2 : DESKTOP_USER_AGENT2,
@@ -28645,7 +28688,8 @@ async function harvest(rawOptions) {
28645
28688
  };
28646
28689
  const requestedDebug = typeof raw.debug === "boolean" ? raw.debug : false;
28647
28690
  const needsLocationEvidence = proxyMode === "location" && Boolean(proxyOpts.location);
28648
- const maxAttempts = maxAttemptsForProxyMode(proxyMode) + 1;
28691
+ const requestedMaxAttempts = raw.maxAttempts;
28692
+ const maxAttempts = typeof requestedMaxAttempts === "number" && Number.isInteger(requestedMaxAttempts) && requestedMaxAttempts >= 1 ? requestedMaxAttempts : maxAttemptsForProxyMode(proxyMode) + 1;
28649
28693
  const serializer = new OutputSerializer();
28650
28694
  let lastError = null;
28651
28695
  let effectiveProxyMode = proxyMode;
@@ -29723,7 +29767,7 @@ var PACKAGE_VERSION;
29723
29767
  var init_version = __esm({
29724
29768
  "src/version.ts"() {
29725
29769
  "use strict";
29726
- PACKAGE_VERSION = "0.31.0";
29770
+ PACKAGE_VERSION = "0.32.0";
29727
29771
  }
29728
29772
  });
29729
29773
 
@@ -30355,9 +30399,9 @@ var init_mcp_tool_schemas = __esm({
30355
30399
  gl: import_zod35.z.string().length(2).default("us").describe("Google country code inferred from location."),
30356
30400
  hl: import_zod35.z.string().length(2).default("en").describe("Language inferred from user request."),
30357
30401
  maxResults: import_zod35.z.number().int().min(1).max(50).default(10).describe("Number of candidates to return. Default 10, maximum 50."),
30358
- includeServices: import_zod35.z.boolean().default(false).describe("Open each returned Google Business Profile and include configured services and areas served when available. Does not collect review cards."),
30359
- proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Leave unset for the default direct browser route. Google localization comes from the city in the query plus UULE, gl, and hl. location is an explicit residential-proxy override."),
30360
- proxyZip: import_zod35.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override when proxyMode is location."),
30402
+ includeServices: import_zod35.z.boolean().default(false).describe("Open each returned business profile to include its configured services and areas served when available. Adds a page visit per business; does not collect review cards."),
30403
+ proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Leave unset for the default route (stealth browser on the managed ISP proxy, retried on a fresh session when Google blocks). Localization comes from the city in the query plus gl/hl. location forces an explicit residential proxy \u2014 not recommended for Google."),
30404
+ proxyZip: import_zod35.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override, only used when proxyMode is location."),
30361
30405
  debug: import_zod35.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
30362
30406
  };
30363
30407
  DirectoryWorkflowInputSchema = {
@@ -30371,7 +30415,7 @@ var init_mcp_tool_schemas = __esm({
30371
30415
  includeZipGroups: import_zod35.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available (MCP_SCRAPER_USZIPS_CSV_PATH or usZipsCsvPath)."),
30372
30416
  usZipsCsvPath: import_zod35.z.string().optional().describe("Local/test-only path to a US ZIPS CSV (state_abbr, zipcode, county, city columns). Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead. For ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the server, or pass this in local/test mode."),
30373
30417
  saveCsv: import_zod35.z.boolean().default(true).describe("Save a directory-ready CSV of results to the MCP Scraper output directory and return its path."),
30374
- proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy behavior per city search. Leave unset for the direct localized Google route; location is an explicit residential-proxy override."),
30418
+ proxyMode: import_zod35.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy behavior per city search. Leave unset for the default route (stealth browser on the managed ISP proxy, retried fresh on a Google block). location forces an explicit residential proxy \u2014 not recommended for Google."),
30375
30419
  proxyZip: import_zod35.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting; normally omitted."),
30376
30420
  debug: import_zod35.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
30377
30421
  };
@@ -32191,7 +32235,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
32191
32235
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
32192
32236
  server.registerTool("maps_search", {
32193
32237
  title: "Google Maps Business Search",
32194
- description: "Search Google local results for multiple businesses by category, niche, or local market \u2014 leads, prospects, competitors, or beyond the 3-pack. It paginates the rendered local-results list, opens each exact business card, then closes the profile dialog before continuing. Set includeServices for services and areas served; review cards are never collected by this tool.",
32238
+ description: "Search Google local results for multiple businesses by category, niche, or local market \u2014 leads, prospects, competitors, or beyond the 3-pack. Reaches the local-results list from the organic page and paginates it, returning up to 50 candidates (default 10) with names, place URLs, CIDs, and ratings. Leave proxyMode unset. Set includeServices to also open each business profile for its services and areas served; review cards are never collected by this tool.",
32195
32239
  inputSchema: MapsSearchInputSchema,
32196
32240
  outputSchema: recordOutputSchema("maps_search", MapsSearchOutputSchema),
32197
32241
  annotations: liveWebToolAnnotations("Google Maps Business Search")