mcp-scraper 0.32.0 → 0.32.2

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +61 -34
  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 +3 -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 +3 -1
  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 +4 -2
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +2 -2
  17. package/dist/{chunk-RDCCU7CQ.js → chunk-AEMABLEL.js} +5 -4
  18. package/dist/chunk-AEMABLEL.js.map +1 -0
  19. package/dist/{chunk-55M5CE5L.js → chunk-MKZIL4EA.js} +2 -2
  20. package/dist/{chunk-PTHY7NYD.js → chunk-V73MPRU6.js} +3 -1
  21. package/dist/chunk-V73MPRU6.js.map +1 -0
  22. package/dist/{chunk-YOOEAXWP.js → chunk-XEED325T.js} +3 -3
  23. package/dist/chunk-XLO5CZ6T.js +7 -0
  24. package/dist/chunk-XLO5CZ6T.js.map +1 -0
  25. package/dist/index.cjs +4 -2
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/{server-OBOTNFD6.js → server-LTZDJHFG.js} +63 -38
  29. package/dist/{server-OBOTNFD6.js.map → server-LTZDJHFG.js.map} +1 -1
  30. package/dist/{worker-7DJYUACV.js → worker-ZZCZNY3R.js} +3 -3
  31. package/docs/mcp-tool-manifest.generated.json +2 -2
  32. package/package.json +1 -1
  33. package/dist/chunk-CPQF3PYW.js +0 -7
  34. package/dist/chunk-CPQF3PYW.js.map +0 -1
  35. package/dist/chunk-PTHY7NYD.js.map +0 -1
  36. package/dist/chunk-RDCCU7CQ.js.map +0 -1
  37. /package/dist/{chunk-55M5CE5L.js.map → chunk-MKZIL4EA.js.map} +0 -0
  38. /package/dist/{chunk-YOOEAXWP.js.map → chunk-XEED325T.js.map} +0 -0
  39. /package/dist/{worker-7DJYUACV.js.map → worker-ZZCZNY3R.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.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.
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.2`, SHA-256 `e4bcf7d1e722ab821eaceebb5acba77e19e3e7bba1326c3d41f77cb22a70f793`). 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,8 +13834,8 @@ 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(/\/+$/, "");
13837
+ function serpArrivalUrl(googleUrl, baseOverride) {
13838
+ const base = (baseOverride ?? process.env.SERP_REDIRECT_BASE)?.trim().replace(/\/+$/, "");
13839
13839
  if (!base) return googleUrl;
13840
13840
  return `${base}/g/${Buffer.from(googleUrl, "utf8").toString("base64url")}`;
13841
13841
  }
@@ -17394,6 +17394,8 @@ var init_schemas3 = __esm({
17394
17394
  includeServices: import_zod17.z.boolean().default(false),
17395
17395
  proxyMode: import_zod17.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
17396
17396
  proxyZip: import_zod17.z.string().regex(/^\d{5}$/).optional(),
17397
+ serpRedirect: import_zod17.z.boolean().optional(),
17398
+ forceDirectEgress: import_zod17.z.boolean().optional(),
17397
17399
  debug: import_zod17.z.boolean().default(false),
17398
17400
  kernelApiKey: import_zod17.z.string().optional(),
17399
17401
  kernelProxyId: import_zod17.z.string().optional(),
@@ -19260,14 +19262,16 @@ function buildGoogleOrganicSearchUrl(input) {
19260
19262
  if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
19261
19263
  return `https://www.google.com/search?${params.toString()}`;
19262
19264
  }
19263
- var LOCAL_RESULTS_PAGE_LIMIT, LOCAL_RESULTS_WAIT_MS, MapsSearchExtractor;
19265
+ var LOCAL_RESULTS_PAGE_LIMIT, LOCAL_RESULTS_WAIT_MS, MAPS_REDIRECT_BASE, MapsSearchExtractor;
19264
19266
  var init_MapsSearchExtractor = __esm({
19265
19267
  "src/extractor/MapsSearchExtractor.ts"() {
19266
19268
  "use strict";
19267
19269
  init_errors();
19270
+ init_BrowserDriver();
19268
19271
  init_uule();
19269
19272
  LOCAL_RESULTS_PAGE_LIMIT = 8;
19270
19273
  LOCAL_RESULTS_WAIT_MS = 1e3;
19274
+ MAPS_REDIRECT_BASE = "https://serp-redirector.vercel.app";
19271
19275
  MapsSearchExtractor = class {
19272
19276
  constructor(driver) {
19273
19277
  this.driver = driver;
@@ -19277,21 +19281,23 @@ var init_MapsSearchExtractor = __esm({
19277
19281
  const startMs = Date.now();
19278
19282
  const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
19279
19283
  const searchUrl = buildGoogleOrganicSearchUrl(options);
19284
+ const keepDefaultProxy = !options.forceDirectEgress && options.proxyMode !== "location" && options.proxyMode !== "configured" && !options.kernelProxyId;
19280
19285
  const config = {
19281
19286
  headless: options.headless,
19282
19287
  kernelApiKey: options.kernelApiKey,
19283
19288
  kernelProxyId: options.kernelProxyId,
19284
19289
  kernelProxyResolution: options.kernelProxyResolution,
19285
19290
  proxyMode: options.proxyMode,
19286
- keepDefaultProxy: options.proxyMode !== "location" && options.proxyMode !== "configured" && !options.kernelProxyId,
19291
+ keepDefaultProxy,
19287
19292
  viewport: { width: 1280, height: 900 },
19288
19293
  locale: `${options.hl}-${options.gl.toUpperCase()}`,
19289
19294
  debug: options.debug
19290
19295
  };
19296
+ const navUrl = options.serpRedirect ? serpArrivalUrl(searchUrl, process.env.SERP_REDIRECT_BASE?.trim() || MAPS_REDIRECT_BASE) : searchUrl;
19291
19297
  try {
19292
19298
  await this.driver.launch(config);
19293
19299
  const page = this.driver.getPage();
19294
- await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
19300
+ await page.goto(navUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
19295
19301
  await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
19296
19302
  if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
19297
19303
  const reachedFinder = await this.clickIntoLocalFinder(page);
@@ -19378,29 +19384,38 @@ var init_MapsSearchExtractor = __esm({
19378
19384
  };
19379
19385
  const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
19380
19386
  const addressPattern = /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i;
19381
- const cards = Array.from(document.querySelectorAll("button")).filter((button) => {
19382
- const name = normalize4(button.querySelector("h3")?.textContent);
19383
- return Boolean(name) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText);
19387
+ const headings = Array.from(document.querySelectorAll('div[role="heading"]')).filter((heading) => {
19388
+ const text2 = normalize4(heading.textContent);
19389
+ return Boolean(text2) && !/^(?:businesses|places|sponsored|people also ask|discussions and forums|more businesses|more places|custom date range|choose what you)/i.test(text2 ?? "");
19384
19390
  });
19385
19391
  const seen = /* @__PURE__ */ new Set();
19386
19392
  const out = [];
19387
- for (const [cardIndex, card] of cards.entries()) {
19388
- const name = normalize4(card.querySelector("h3")?.textContent);
19393
+ let cardIndex = -1;
19394
+ for (const heading of headings) {
19395
+ let card = heading;
19396
+ for (let hop = 0; hop < 8; hop++) {
19397
+ const parent = card.parentElement;
19398
+ if (!parent || parent.querySelectorAll('div[role="heading"]').length > 1) break;
19399
+ card = parent;
19400
+ }
19401
+ if (card.closest('[role="dialog"], dialog')) continue;
19402
+ const name = normalize4(heading.textContent);
19389
19403
  if (!name) continue;
19390
- const text2 = normalize4(card.innerText) ?? name;
19391
- const key = `${name.toLowerCase()}|${text2.toLowerCase()}`;
19404
+ const lines = (card.innerText ?? "").split("\n").map((line) => normalize4(line)).filter((line) => Boolean(line));
19405
+ if (/^sponsored$/i.test(lines[0] ?? "")) continue;
19406
+ const ratingLine = lines.find((line) => /\d(?:\.\d)?\s*\(/.test(line)) ?? "";
19407
+ const rating = ratingLine.match(/(\d(?:\.\d)?)\s*\(/)?.[1] ?? null;
19408
+ const reviewCount = ratingLine.match(/\((\d[\d,]*)\)/)?.[1]?.replace(/,/g, "") ?? null;
19409
+ const category = normalize4(ratingLine.split("\xB7").slice(1).join("\xB7"));
19410
+ const phone = lines.map((line) => line.match(phonePattern)?.[0]).find((value) => Boolean(value)) ?? null;
19411
+ const hoursStatus = normalize4((lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? "").split("\xB7")[0] ?? "");
19412
+ const address = lines.find((line) => addressPattern.test(line)) ?? null;
19413
+ const websiteUrl = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href).find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
19414
+ if (!rating && !(phone && websiteUrl)) continue;
19415
+ const key = `${name.toLowerCase()}|${lines.join(" ").toLowerCase()}`;
19392
19416
  if (seen.has(key)) continue;
19393
19417
  seen.add(key);
19394
- const lines = text2.split(/\n+/).map((line) => normalize4(line)).filter((line) => Boolean(line));
19395
- const aria = normalize4(card.getAttribute("aria-label"));
19396
- const rating = (aria ?? text2).match(/(?:rated\s+)?(\d(?:\.\d)?)\s*(?:out of 5)?/i)?.[1] ?? null;
19397
- const reviewCount = (aria ?? text2).match(/(?:\(|·\s*)([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
19398
- const phone = lines.find((line) => phonePattern.test(line)) ?? null;
19399
- const address = lines.find((line) => addressPattern.test(line)) ?? null;
19400
- const hoursStatus = lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? null;
19401
- const category = lines.find(
19402
- (line) => line !== name && line !== phone && line !== address && line !== hoursStatus && !/^(?:rated|\d[\d.,]*\s*(?:out of 5|reviews?)?)\b/i.test(line) && !/sponsored|website|directions|call|save|share|more/i.test(line)
19403
- ) ?? null;
19418
+ cardIndex += 1;
19404
19419
  const mapsSearchUrl = `https://www.google.com/maps/search/${encodeURIComponent([name, locationHint].filter(Boolean).join(" "))}`;
19405
19420
  out.push({
19406
19421
  cardIndex,
@@ -19415,7 +19430,7 @@ var init_MapsSearchExtractor = __esm({
19415
19430
  address,
19416
19431
  phone,
19417
19432
  hoursStatus,
19418
- websiteUrl: null,
19433
+ websiteUrl,
19419
19434
  directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
19420
19435
  metadata: lines.slice(0, 20)
19421
19436
  });
@@ -19424,17 +19439,27 @@ var init_MapsSearchExtractor = __esm({
19424
19439
  }, fallbackLocation);
19425
19440
  }
19426
19441
  async openCardAndExtractDialog(page, card, includeServices) {
19442
+ if (!includeServices) return null;
19427
19443
  const clicked = await page.evaluate((input) => {
19428
19444
  const normalize4 = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
19429
- const cards = Array.from(document.querySelectorAll("button")).filter(
19430
- (button) => Boolean(button.querySelector("h3")) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText)
19431
- );
19432
- const exact = cards.find((button) => `${normalize4(button.querySelector("h3")?.textContent)}|${normalize4(button.innerText)}` === input.cardKey);
19433
- const target = exact ?? cards[input.cardIndex];
19434
- if (!target) return false;
19435
- target.click();
19436
- return true;
19437
- }, { cardIndex: card.cardIndex, cardKey: card.cardKey });
19445
+ const headings = Array.from(document.querySelectorAll('div[role="heading"]'));
19446
+ for (const heading of headings) {
19447
+ let target = heading;
19448
+ for (let hop = 0; hop < 8; hop++) {
19449
+ const parent = target.parentElement;
19450
+ if (!parent || parent.querySelectorAll('div[role="heading"]').length > 1) break;
19451
+ target = parent;
19452
+ }
19453
+ if (target.closest('[role="dialog"], dialog')) continue;
19454
+ const name = normalize4(heading.textContent);
19455
+ if (!name) continue;
19456
+ const lines = (target.innerText ?? "").split("\n").map((line) => normalize4(line)).filter(Boolean);
19457
+ if (`${name}|${lines.join(" ")}` !== input.cardKey) continue;
19458
+ target.click();
19459
+ return true;
19460
+ }
19461
+ return false;
19462
+ }, { cardKey: card.cardKey });
19438
19463
  if (!clicked) return null;
19439
19464
  const dialog = page.locator('dialog, [role="dialog"]').filter({ has: page.locator('h1, h2, [role="heading"]') }).last();
19440
19465
  await dialog.waitFor({ state: "visible", timeout: 6e3 }).catch(() => {
@@ -20145,7 +20170,9 @@ async function runMapsSearchWithRotation(options) {
20145
20170
  ...options,
20146
20171
  proxyZip,
20147
20172
  kernelProxyId: options.proxyMode === "none" ? void 0 : resolution2.kernelProxyId,
20148
- kernelProxyResolution: resolution2.resolution
20173
+ kernelProxyResolution: resolution2.resolution,
20174
+ serpRedirect: options.proxyMode === "none" && attemptIndex >= 1 && attemptIndex < maxAttempts - 1,
20175
+ forceDirectEgress: options.proxyMode === "none" && attemptIndex >= 1
20149
20176
  };
20150
20177
  const isHeadfulEscalation = attemptIndex === maxAttempts - 1;
20151
20178
  const baseCtx = currentCostContext();
@@ -29767,7 +29794,7 @@ var PACKAGE_VERSION;
29767
29794
  var init_version = __esm({
29768
29795
  "src/version.ts"() {
29769
29796
  "use strict";
29770
- PACKAGE_VERSION = "0.32.0";
29797
+ PACKAGE_VERSION = "0.32.2";
29771
29798
  }
29772
29799
  });
29773
29800