mcp-scraper 0.31.0 → 0.32.1

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 +101 -38
  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-ZOXL3VWO.js → chunk-2FY4VBAY.js} +8 -8
  18. package/dist/chunk-2FY4VBAY.js.map +1 -0
  19. package/dist/{chunk-KAMKTSJQ.js → chunk-55M5CE5L.js} +2 -2
  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-VJBS6RPB.js +7 -0
  25. package/dist/chunk-VJBS6RPB.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-GXTYKIAX.js} +77 -35
  32. package/dist/{server-DS3ZSNNO.js.map → server-GXTYKIAX.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/dist/index.d.cts CHANGED
@@ -8,6 +8,9 @@ declare const HarvestOptionsSchema: z.ZodObject<{
8
8
  device: z.ZodDefault<z.ZodEnum<["desktop", "mobile"]>>;
9
9
  proxyMode: z.ZodDefault<z.ZodEnum<["location", "configured", "none"]>>;
10
10
  proxyZip: z.ZodOptional<z.ZodString>;
11
+ keepDefaultProxy: z.ZodOptional<z.ZodBoolean>;
12
+ requireUsEgress: z.ZodOptional<z.ZodBoolean>;
13
+ maxAttempts: z.ZodOptional<z.ZodNumber>;
11
14
  debug: z.ZodDefault<z.ZodBoolean>;
12
15
  depth: z.ZodDefault<z.ZodNumber>;
13
16
  maxQuestions: z.ZodDefault<z.ZodNumber>;
@@ -38,6 +41,9 @@ declare const HarvestOptionsSchema: z.ZodObject<{
38
41
  pages: number;
39
42
  location?: string | undefined;
40
43
  proxyZip?: string | undefined;
44
+ keepDefaultProxy?: boolean | undefined;
45
+ requireUsEgress?: boolean | undefined;
46
+ maxAttempts?: number | undefined;
41
47
  profileDir?: string | undefined;
42
48
  proxy?: string | undefined;
43
49
  kernelApiKey?: string | undefined;
@@ -52,6 +58,9 @@ declare const HarvestOptionsSchema: z.ZodObject<{
52
58
  device?: "desktop" | "mobile" | undefined;
53
59
  proxyMode?: "none" | "location" | "configured" | undefined;
54
60
  proxyZip?: string | undefined;
61
+ keepDefaultProxy?: boolean | undefined;
62
+ requireUsEgress?: boolean | undefined;
63
+ maxAttempts?: number | undefined;
55
64
  debug?: boolean | undefined;
56
65
  depth?: number | undefined;
57
66
  maxQuestions?: number | undefined;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,9 @@ declare const HarvestOptionsSchema: z.ZodObject<{
8
8
  device: z.ZodDefault<z.ZodEnum<["desktop", "mobile"]>>;
9
9
  proxyMode: z.ZodDefault<z.ZodEnum<["location", "configured", "none"]>>;
10
10
  proxyZip: z.ZodOptional<z.ZodString>;
11
+ keepDefaultProxy: z.ZodOptional<z.ZodBoolean>;
12
+ requireUsEgress: z.ZodOptional<z.ZodBoolean>;
13
+ maxAttempts: z.ZodOptional<z.ZodNumber>;
11
14
  debug: z.ZodDefault<z.ZodBoolean>;
12
15
  depth: z.ZodDefault<z.ZodNumber>;
13
16
  maxQuestions: z.ZodDefault<z.ZodNumber>;
@@ -38,6 +41,9 @@ declare const HarvestOptionsSchema: z.ZodObject<{
38
41
  pages: number;
39
42
  location?: string | undefined;
40
43
  proxyZip?: string | undefined;
44
+ keepDefaultProxy?: boolean | undefined;
45
+ requireUsEgress?: boolean | undefined;
46
+ maxAttempts?: number | undefined;
41
47
  profileDir?: string | undefined;
42
48
  proxy?: string | undefined;
43
49
  kernelApiKey?: string | undefined;
@@ -52,6 +58,9 @@ declare const HarvestOptionsSchema: z.ZodObject<{
52
58
  device?: "desktop" | "mobile" | undefined;
53
59
  proxyMode?: "none" | "location" | "configured" | undefined;
54
60
  proxyZip?: string | undefined;
61
+ keepDefaultProxy?: boolean | undefined;
62
+ requireUsEgress?: boolean | undefined;
63
+ maxAttempts?: number | undefined;
55
64
  debug?: boolean | undefined;
56
65
  depth?: number | undefined;
57
66
  maxQuestions?: number | undefined;
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  harvest
3
- } from "./chunk-YH5DRFWQ.js";
3
+ } from "./chunk-RDCCU7CQ.js";
4
4
  import {
5
5
  recordVendorUsage
6
6
  } from "./chunk-MTSBI7ZH.js";
7
7
  import "./chunk-K443GQY5.js";
8
- import "./chunk-ZQSHKWV4.js";
8
+ import "./chunk-PTHY7NYD.js";
9
9
  import "./chunk-M2S27J6Z.js";
10
10
  import "./chunk-ICT7DDHL.js";
11
11
 
@@ -16,7 +16,7 @@ import {
16
16
  harvest,
17
17
  normalizeLocation,
18
18
  resolveKernelProxyId
19
- } from "./chunk-YH5DRFWQ.js";
19
+ } from "./chunk-RDCCU7CQ.js";
20
20
  import {
21
21
  HttpMcpToolExecutor,
22
22
  assessTranscriptSignal,
@@ -36,7 +36,7 @@ import {
36
36
  sanitizeAttempts,
37
37
  sanitizeHarvestResult,
38
38
  transcribeMediaUrl
39
- } from "./chunk-ZOXL3VWO.js";
39
+ } from "./chunk-2FY4VBAY.js";
40
40
  import {
41
41
  auditImages,
42
42
  buildLinkReport,
@@ -65,7 +65,7 @@ import {
65
65
  workflowOutputBaseDir,
66
66
  workflowStepCount,
67
67
  workflowSupportsSteps
68
- } from "./chunk-KAMKTSJQ.js";
68
+ } from "./chunk-55M5CE5L.js";
69
69
  import "./chunk-K443GQY5.js";
70
70
  import {
71
71
  DEFAULT_MAPS_PROXY_MODE,
@@ -76,8 +76,8 @@ import {
76
76
  RawMapsHoursRowSchema,
77
77
  RawMapsOverviewSchema,
78
78
  RawMapsReviewStatsSchema
79
- } from "./chunk-ZQSHKWV4.js";
80
- import "./chunk-W3GYPHFL.js";
79
+ } from "./chunk-PTHY7NYD.js";
80
+ import "./chunk-VJBS6RPB.js";
81
81
  import {
82
82
  completeExtractJob,
83
83
  countSuccessfulPages,
@@ -129,6 +129,7 @@ import {
129
129
  } from "./chunk-4767BA2O.js";
130
130
  import {
131
131
  CaptchaError,
132
+ ExtractionError,
132
133
  RECAPTCHA_INSTRUCTIONS,
133
134
  sanitizeVendorName
134
135
  } from "./chunk-M2S27J6Z.js";
@@ -13088,6 +13089,12 @@ function buildGoogleLocalResultsUrl(input) {
13088
13089
  if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
13089
13090
  return `https://www.google.com/search?${params.toString()}`;
13090
13091
  }
13092
+ function buildGoogleOrganicSearchUrl(input) {
13093
+ const query = [input.query, input.location].filter(Boolean).join(" ");
13094
+ const params = new URLSearchParams({ q: query, gl: input.gl, hl: input.hl, pws: "0" });
13095
+ if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
13096
+ return `https://www.google.com/search?${params.toString()}`;
13097
+ }
13091
13098
  var MapsSearchExtractor = class {
13092
13099
  constructor(driver) {
13093
13100
  this.driver = driver;
@@ -13096,13 +13103,14 @@ var MapsSearchExtractor = class {
13096
13103
  async extract(options) {
13097
13104
  const startMs = Date.now();
13098
13105
  const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
13099
- const searchUrl = buildGoogleLocalResultsUrl(options);
13106
+ const searchUrl = buildGoogleOrganicSearchUrl(options);
13100
13107
  const config = {
13101
13108
  headless: options.headless,
13102
13109
  kernelApiKey: options.kernelApiKey,
13103
13110
  kernelProxyId: options.kernelProxyId,
13104
13111
  kernelProxyResolution: options.kernelProxyResolution,
13105
13112
  proxyMode: options.proxyMode,
13113
+ keepDefaultProxy: options.proxyMode !== "location" && options.proxyMode !== "configured" && !options.kernelProxyId,
13106
13114
  viewport: { width: 1280, height: 900 },
13107
13115
  locale: `${options.hl}-${options.gl.toUpperCase()}`,
13108
13116
  debug: options.debug
@@ -13113,6 +13121,10 @@ var MapsSearchExtractor = class {
13113
13121
  await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
13114
13122
  await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
13115
13123
  if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
13124
+ const reachedFinder = await this.clickIntoLocalFinder(page);
13125
+ if (!reachedFinder) throw new ExtractionError('Google "More businesses" control was not found on the organic results page');
13126
+ await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
13127
+ if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
13116
13128
  const results = await this.collectResults(page, options);
13117
13129
  return {
13118
13130
  query: options.query,
@@ -13129,6 +13141,17 @@ var MapsSearchExtractor = class {
13129
13141
  await this.driver.close();
13130
13142
  }
13131
13143
  }
13144
+ async clickIntoLocalFinder(page) {
13145
+ const control = page.getByRole("link", { name: /more (businesses|places|results)/i }).or(page.getByRole("button", { name: /more (businesses|places|results)/i })).first();
13146
+ const visible = await control.isVisible({ timeout: 1e4 }).catch(() => false);
13147
+ if (!visible) return false;
13148
+ await control.click({ timeout: 1e4 }).catch(() => {
13149
+ });
13150
+ await page.waitForTimeout(5e3);
13151
+ return page.evaluate(
13152
+ () => /[?&]udm=1/.test(location.href) || document.querySelectorAll('a[href*="/maps/place/"]').length > 0
13153
+ );
13154
+ }
13132
13155
  async detectBlock(page) {
13133
13156
  return page.evaluate(() => {
13134
13157
  const text2 = document.body.innerText.slice(0, 2e3);
@@ -13182,29 +13205,38 @@ var MapsSearchExtractor = class {
13182
13205
  };
13183
13206
  const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
13184
13207
  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;
13185
- const cards = Array.from(document.querySelectorAll("button")).filter((button) => {
13186
- const name = normalize(button.querySelector("h3")?.textContent);
13187
- return Boolean(name) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText);
13208
+ const headings = Array.from(document.querySelectorAll('div[role="heading"]')).filter((heading) => {
13209
+ const text2 = normalize(heading.textContent);
13210
+ 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 ?? "");
13188
13211
  });
13189
13212
  const seen = /* @__PURE__ */ new Set();
13190
13213
  const out = [];
13191
- for (const [cardIndex, card] of cards.entries()) {
13192
- const name = normalize(card.querySelector("h3")?.textContent);
13214
+ let cardIndex = -1;
13215
+ for (const heading of headings) {
13216
+ let card = heading;
13217
+ for (let hop = 0; hop < 8; hop++) {
13218
+ const parent = card.parentElement;
13219
+ if (!parent || parent.querySelectorAll('div[role="heading"]').length > 1) break;
13220
+ card = parent;
13221
+ }
13222
+ if (card.closest('[role="dialog"], dialog')) continue;
13223
+ const name = normalize(heading.textContent);
13193
13224
  if (!name) continue;
13194
- const text2 = normalize(card.innerText) ?? name;
13195
- const key = `${name.toLowerCase()}|${text2.toLowerCase()}`;
13225
+ const lines = (card.innerText ?? "").split("\n").map((line) => normalize(line)).filter((line) => Boolean(line));
13226
+ if (/^sponsored$/i.test(lines[0] ?? "")) continue;
13227
+ const ratingLine = lines.find((line) => /\d(?:\.\d)?\s*\(/.test(line)) ?? "";
13228
+ const rating = ratingLine.match(/(\d(?:\.\d)?)\s*\(/)?.[1] ?? null;
13229
+ const reviewCount = ratingLine.match(/\((\d[\d,]*)\)/)?.[1]?.replace(/,/g, "") ?? null;
13230
+ const category = normalize(ratingLine.split("\xB7").slice(1).join("\xB7"));
13231
+ const phone = lines.map((line) => line.match(phonePattern)?.[0]).find((value) => Boolean(value)) ?? null;
13232
+ const hoursStatus = normalize((lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? "").split("\xB7")[0] ?? "");
13233
+ const address = lines.find((line) => addressPattern.test(line)) ?? null;
13234
+ const websiteUrl = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href).find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
13235
+ if (!rating && !(phone && websiteUrl)) continue;
13236
+ const key = `${name.toLowerCase()}|${lines.join(" ").toLowerCase()}`;
13196
13237
  if (seen.has(key)) continue;
13197
13238
  seen.add(key);
13198
- const lines = text2.split(/\n+/).map((line) => normalize(line)).filter((line) => Boolean(line));
13199
- const aria = normalize(card.getAttribute("aria-label"));
13200
- const rating = (aria ?? text2).match(/(?:rated\s+)?(\d(?:\.\d)?)\s*(?:out of 5)?/i)?.[1] ?? null;
13201
- const reviewCount = (aria ?? text2).match(/(?:\(|·\s*)([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
13202
- const phone = lines.find((line) => phonePattern.test(line)) ?? null;
13203
- const address = lines.find((line) => addressPattern.test(line)) ?? null;
13204
- const hoursStatus = lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? null;
13205
- const category = lines.find(
13206
- (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)
13207
- ) ?? null;
13239
+ cardIndex += 1;
13208
13240
  const mapsSearchUrl = `https://www.google.com/maps/search/${encodeURIComponent([name, locationHint].filter(Boolean).join(" "))}`;
13209
13241
  out.push({
13210
13242
  cardIndex,
@@ -13219,7 +13251,7 @@ var MapsSearchExtractor = class {
13219
13251
  address,
13220
13252
  phone,
13221
13253
  hoursStatus,
13222
- websiteUrl: null,
13254
+ websiteUrl,
13223
13255
  directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
13224
13256
  metadata: lines.slice(0, 20)
13225
13257
  });
@@ -13228,17 +13260,27 @@ var MapsSearchExtractor = class {
13228
13260
  }, fallbackLocation);
13229
13261
  }
13230
13262
  async openCardAndExtractDialog(page, card, includeServices) {
13263
+ if (!includeServices) return null;
13231
13264
  const clicked = await page.evaluate((input) => {
13232
13265
  const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
13233
- const cards = Array.from(document.querySelectorAll("button")).filter(
13234
- (button) => Boolean(button.querySelector("h3")) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText)
13235
- );
13236
- const exact = cards.find((button) => `${normalize(button.querySelector("h3")?.textContent)}|${normalize(button.innerText)}` === input.cardKey);
13237
- const target = exact ?? cards[input.cardIndex];
13238
- if (!target) return false;
13239
- target.click();
13240
- return true;
13241
- }, { cardIndex: card.cardIndex, cardKey: card.cardKey });
13266
+ const headings = Array.from(document.querySelectorAll('div[role="heading"]'));
13267
+ for (const heading of headings) {
13268
+ let target = heading;
13269
+ for (let hop = 0; hop < 8; hop++) {
13270
+ const parent = target.parentElement;
13271
+ if (!parent || parent.querySelectorAll('div[role="heading"]').length > 1) break;
13272
+ target = parent;
13273
+ }
13274
+ if (target.closest('[role="dialog"], dialog')) continue;
13275
+ const name = normalize(heading.textContent);
13276
+ if (!name) continue;
13277
+ const lines = (target.innerText ?? "").split("\n").map((line) => normalize(line)).filter(Boolean);
13278
+ if (`${name}|${lines.join(" ")}` !== input.cardKey) continue;
13279
+ target.click();
13280
+ return true;
13281
+ }
13282
+ return false;
13283
+ }, { cardKey: card.cardKey });
13242
13284
  if (!clicked) return null;
13243
13285
  const dialog = page.locator('dialog, [role="dialog"]').filter({ has: page.locator('h1, h2, [role="heading"]') }).last();
13244
13286
  await dialog.waitFor({ state: "visible", timeout: 6e3 }).catch(() => {
@@ -27382,7 +27424,7 @@ app.get("/cron/tick", async (c) => {
27382
27424
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
27383
27425
  return c.json({ error: "Unauthorized" }, 401);
27384
27426
  }
27385
- const { drainQueue } = await import("./worker-RYP5EOV5.js");
27427
+ const { drainQueue } = await import("./worker-7DJYUACV.js");
27386
27428
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
27387
27429
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
27388
27430
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -27585,4 +27627,4 @@ app.get("/blog/:slug/", (c) => {
27585
27627
  export {
27586
27628
  app
27587
27629
  };
27588
- //# sourceMappingURL=server-DS3ZSNNO.js.map
27630
+ //# sourceMappingURL=server-GXTYKIAX.js.map