mcp-scraper 0.19.0 → 0.20.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 (51) hide show
  1. package/dist/bin/api-server.cjs +189 -37
  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 +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +100 -14
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +4 -4
  13. package/dist/bin/paa-harvest.cjs.map +1 -1
  14. package/dist/bin/paa-harvest.js +3 -3
  15. package/dist/{chunk-GL4BW4CP.js → chunk-22VEGGTW.js} +21 -9
  16. package/dist/{chunk-GL4BW4CP.js.map → chunk-22VEGGTW.js.map} +1 -1
  17. package/dist/{chunk-RUGJE5EB.js → chunk-EJK25QOW.js} +2 -2
  18. package/dist/chunk-JWIE5NCR.js +284 -0
  19. package/dist/chunk-JWIE5NCR.js.map +1 -0
  20. package/dist/{chunk-O2S5TOCG.js → chunk-KE7KE2Q2.js} +102 -16
  21. package/dist/chunk-KE7KE2Q2.js.map +1 -0
  22. package/dist/chunk-PZB3TJWK.js +7 -0
  23. package/dist/chunk-PZB3TJWK.js.map +1 -0
  24. package/dist/{chunk-D7ZT27HY.js → chunk-UN7VMHZL.js} +2 -2
  25. package/dist/{chunk-2HDMYW4B.js → chunk-XDFSLSSH.js} +2 -2
  26. package/dist/{chunk-HE2LQPJ2.js → chunk-ZRKFW5FB.js} +2 -2
  27. package/dist/{db-LIOTIWVN.js → db-YHZYG7D2.js} +2 -2
  28. package/dist/{extract-bundle-U4D5LW5W.js → extract-bundle-OSUPAHCE.js} +58 -5
  29. package/dist/extract-bundle-OSUPAHCE.js.map +1 -0
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/{server-VWXDE64Y.js → server-VDWIPV7F.js} +37 -310
  33. package/dist/server-VDWIPV7F.js.map +1 -0
  34. package/dist/{site-extract-repository-NVSZH35Y.js → site-extract-repository-GWKGK46Z.js} +3 -3
  35. package/dist/{worker-AM2DHUWG.js → worker-O5PZTTPY.js} +5 -5
  36. package/docs/mcp-tool-manifest.generated.json +166 -23
  37. package/docs/specs/meta-ad-creative-media-resolution-spec.md +3 -3
  38. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +1007 -0
  39. package/package.json +1 -1
  40. package/dist/chunk-O2S5TOCG.js.map +0 -1
  41. package/dist/chunk-OQHYDW4Q.js +0 -7
  42. package/dist/chunk-OQHYDW4Q.js.map +0 -1
  43. package/dist/extract-bundle-U4D5LW5W.js.map +0 -1
  44. package/dist/server-VWXDE64Y.js.map +0 -1
  45. /package/dist/{chunk-RUGJE5EB.js.map → chunk-EJK25QOW.js.map} +0 -0
  46. /package/dist/{chunk-D7ZT27HY.js.map → chunk-UN7VMHZL.js.map} +0 -0
  47. /package/dist/{chunk-2HDMYW4B.js.map → chunk-XDFSLSSH.js.map} +0 -0
  48. /package/dist/{chunk-HE2LQPJ2.js.map → chunk-ZRKFW5FB.js.map} +0 -0
  49. /package/dist/{db-LIOTIWVN.js.map → db-YHZYG7D2.js.map} +0 -0
  50. /package/dist/{site-extract-repository-NVSZH35Y.js.map → site-extract-repository-GWKGK46Z.js.map} +0 -0
  51. /package/dist/{worker-AM2DHUWG.js.map → worker-O5PZTTPY.js.map} +0 -0
@@ -5271,18 +5271,30 @@ async function ledgerExistsForStripePI(stripePI) {
5271
5271
  });
5272
5272
  return res.rows.length > 0;
5273
5273
  }
5274
- async function reconcileBalanceMc(userId) {
5274
+ async function reconcileBalanceMc(userId, knownBalanceMc) {
5275
5275
  const db = getDb();
5276
- await ensureMigrated(Number(userId));
5277
5276
  const res = await db.execute({
5278
- sql: `SELECT COALESCE(SUM(remaining_mc), 0) AS balance_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')`,
5277
+ sql: `SELECT
5278
+ COUNT(*) AS lot_count,
5279
+ COALESCE(SUM(CASE WHEN remaining_mc > 0 AND expires_at > datetime('now') THEN remaining_mc ELSE 0 END), 0) AS balance_mc
5280
+ FROM credit_lots WHERE user_id = ?`,
5279
5281
  args: [userId]
5280
5282
  });
5281
- const balanceMc = Number(res.rows[0]?.balance_mc ?? 0);
5282
- await db.execute({
5283
- sql: "UPDATE users SET balance_mc = ? WHERE id = ? AND balance_mc != ?",
5284
- args: [balanceMc, userId, balanceMc]
5285
- });
5283
+ let balanceMc = Number(res.rows[0]?.balance_mc ?? 0);
5284
+ if (Number(res.rows[0]?.lot_count ?? 0) === 0) {
5285
+ await ensureMigrated(Number(userId));
5286
+ const migrated = await db.execute({
5287
+ sql: `SELECT COALESCE(SUM(remaining_mc), 0) AS balance_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')`,
5288
+ args: [userId]
5289
+ });
5290
+ balanceMc = Number(migrated.rows[0]?.balance_mc ?? 0);
5291
+ }
5292
+ if (knownBalanceMc !== balanceMc) {
5293
+ await db.execute({
5294
+ sql: "UPDATE users SET balance_mc = ? WHERE id = ? AND balance_mc != ?",
5295
+ args: [balanceMc, userId, balanceMc]
5296
+ });
5297
+ }
5286
5298
  return balanceMc;
5287
5299
  }
5288
5300
  var import_http, import_node_crypto, import_zod, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, CREDIT_LOT_TTL, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
@@ -6372,8 +6384,8 @@ async function downloadAsset(url, destDir, filename) {
6372
6384
  }
6373
6385
  const writer = (0, import_node_fs.createWriteStream)(dest);
6374
6386
  await (0, import_promises2.pipeline)(import_node_stream.Readable.fromWeb(res.body), writer);
6375
- const { statSync: statSync2 } = await import("fs");
6376
- const sizeBytes = statSync2(dest).size;
6387
+ const { statSync: statSync3 } = await import("fs");
6388
+ const sizeBytes = statSync3(dest).size;
6377
6389
  return { savedPath: dest, sizeBytes, mimeType };
6378
6390
  }
6379
6391
  async function harvestPageMedia(html, pageUrl, options = {}) {
@@ -10746,6 +10758,15 @@ function registrableDomain2(host) {
10746
10758
  const parts = h.split(".");
10747
10759
  return parts.length <= 2 ? h : parts.slice(-2).join(".");
10748
10760
  }
10761
+ function safeImageFilename(url, index) {
10762
+ try {
10763
+ const u = new URL(url);
10764
+ const base = (0, import_node_path3.basename)(u.pathname).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
10765
+ return base || `image-${index}`;
10766
+ } catch {
10767
+ return `image-${index}`;
10768
+ }
10769
+ }
10749
10770
  function slugFactory() {
10750
10771
  const counts = /* @__PURE__ */ new Map();
10751
10772
  return (url) => {
@@ -10768,7 +10789,9 @@ async function* pageChunks(jobId) {
10768
10789
  }
10769
10790
  async function assembleExtractArtifacts(job, extras = {}) {
10770
10791
  const dir = (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), `extract-bundle-${job.id}-${Date.now()}`);
10792
+ const downloadImages = job.options.downloadImages === true;
10771
10793
  (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(dir, "pages"), { recursive: true });
10794
+ if (downloadImages) (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(dir, "images"), { recursive: true });
10772
10795
  try {
10773
10796
  const metas = [];
10774
10797
  const statusByUrl = /* @__PURE__ */ new Map();
@@ -10802,6 +10825,10 @@ async function assembleExtractArtifacts(job, extras = {}) {
10802
10825
  } catch {
10803
10826
  siteReg = "";
10804
10827
  }
10828
+ const imageLimit = (0, import_p_limit3.default)(IMAGE_DOWNLOAD_CONCURRENCY);
10829
+ const imageDownloads = [];
10830
+ let imagesQueued = 0;
10831
+ let imagesFailed = 0;
10805
10832
  for await (const chunk of pageChunks(job.id)) {
10806
10833
  for (const p of chunk) {
10807
10834
  if (p.bodyMarkdown) {
@@ -10819,6 +10846,20 @@ ${p.bodyMarkdown}
10819
10846
  `
10820
10847
  );
10821
10848
  }
10849
+ if (downloadImages && p.imageLinks?.length && imagesQueued < MAX_IMAGES_PER_SITE) {
10850
+ const pageDir = (0, import_node_path3.join)(dir, "images", slug(p.url));
10851
+ (0, import_node_fs3.mkdirSync)(pageDir, { recursive: true });
10852
+ for (const [idx, imgUrl] of p.imageLinks.slice(0, MAX_IMAGES_PER_PAGE).entries()) {
10853
+ if (imagesQueued >= MAX_IMAGES_PER_SITE) break;
10854
+ imagesQueued++;
10855
+ const filename = safeImageFilename(imgUrl, idx);
10856
+ imageDownloads.push(
10857
+ imageLimit(() => downloadAsset(imgUrl, pageDir, filename)).then(() => void 0, () => {
10858
+ imagesFailed++;
10859
+ })
10860
+ );
10861
+ }
10862
+ }
10822
10863
  const from = normalize2(p.url);
10823
10864
  const oc = { int: 0, ext: 0 };
10824
10865
  for (const l of p.outlinks ?? []) {
@@ -10866,6 +10907,7 @@ ${p.bodyMarkdown}
10866
10907
  }
10867
10908
  gz.end();
10868
10909
  await (0, import_node_events.once)(linksOut, "finish");
10910
+ await Promise.all(imageDownloads);
10869
10911
  const depth = /* @__PURE__ */ new Map();
10870
10912
  const start = normalize2(job.startUrl);
10871
10913
  depth.set(start, 0);
@@ -10957,6 +10999,15 @@ ${renderImageSection(extras.imageAudit)}`;
10957
10999
  }
10958
11000
  if (extras.seoAudit) (0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(dir, "seo-audit.json"), JSON.stringify(extras.seoAudit, null, 2));
10959
11001
  if (extras.branding) (0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(dir, "branding.json"), JSON.stringify(extras.branding, null, 2));
11002
+ if (downloadImages) {
11003
+ (0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(dir, "images-download-summary.json"), JSON.stringify({
11004
+ queued: imagesQueued,
11005
+ downloaded: imagesQueued - imagesFailed,
11006
+ failed: imagesFailed,
11007
+ perPageCap: MAX_IMAGES_PER_PAGE,
11008
+ perSiteCap: MAX_IMAGES_PER_SITE
11009
+ }, null, 2));
11010
+ }
10960
11011
  const artifactFiles = [
10961
11012
  { name: "report.md", type: "text/markdown" },
10962
11013
  { name: "issues.json", type: "application/json" },
@@ -10973,13 +11024,20 @@ ${renderImageSection(extras.imageAudit)}`;
10973
11024
  }
10974
11025
  if (extras.seoAudit) artifactFiles.push({ name: "seo-audit.json", type: "application/json" });
10975
11026
  if (extras.branding) artifactFiles.push({ name: "branding.json", type: "application/json" });
11027
+ if (downloadImages) artifactFiles.push({ name: "images-download-summary.json", type: "application/json" });
10976
11028
  const zip = new import_yazl.ZipFile();
10977
11029
  const zipOut = (0, import_node_fs3.createWriteStream)((0, import_node_path3.join)(dir, "bundle.zip"));
10978
11030
  zip.outputStream.pipe(zipOut);
10979
11031
  for (const f of artifactFiles) zip.addFile((0, import_node_path3.join)(dir, f.name), f.name);
10980
11032
  if (pagesWithBody > 0) {
10981
- const { readdirSync: readdirSync2 } = await import("fs");
10982
- for (const f of readdirSync2((0, import_node_path3.join)(dir, "pages"))) zip.addFile((0, import_node_path3.join)(dir, "pages", f), `pages/${f}`);
11033
+ for (const f of (0, import_node_fs3.readdirSync)((0, import_node_path3.join)(dir, "pages"))) zip.addFile((0, import_node_path3.join)(dir, "pages", f), `pages/${f}`);
11034
+ }
11035
+ if (downloadImages && imagesQueued > imagesFailed) {
11036
+ const imagesDir = (0, import_node_path3.join)(dir, "images");
11037
+ for (const rel of (0, import_node_fs3.readdirSync)(imagesDir, { recursive: true })) {
11038
+ const full = (0, import_node_path3.join)(imagesDir, rel);
11039
+ if ((0, import_node_fs3.statSync)(full).isFile()) zip.addFile(full, `images/${rel.split("\\").join("/")}`);
11040
+ }
10983
11041
  }
10984
11042
  zip.end();
10985
11043
  await (0, import_node_events.once)(zipOut, "finish");
@@ -10993,7 +11051,7 @@ ${renderImageSection(extras.imageAudit)}`;
10993
11051
  (0, import_node_fs3.rmSync)(dir, { recursive: true, force: true });
10994
11052
  }
10995
11053
  }
10996
- var import_node_fs3, import_node_path3, import_node_os3, import_node_zlib, import_node_events, import_yazl, CHUNK;
11054
+ var import_node_fs3, import_node_path3, import_node_os3, import_node_zlib, import_node_events, import_yazl, import_p_limit3, CHUNK, MAX_IMAGES_PER_PAGE, MAX_IMAGES_PER_SITE, IMAGE_DOWNLOAD_CONCURRENCY;
10997
11055
  var init_extract_bundle = __esm({
10998
11056
  "src/api/extract-bundle.ts"() {
10999
11057
  "use strict";
@@ -11003,12 +11061,17 @@ var init_extract_bundle = __esm({
11003
11061
  import_node_zlib = require("zlib");
11004
11062
  import_node_events = require("events");
11005
11063
  import_yazl = require("yazl");
11064
+ import_p_limit3 = __toESM(require("p-limit"), 1);
11006
11065
  init_db();
11007
11066
  init_blob_store();
11008
11067
  init_seo_issues();
11009
11068
  init_seo_link_report();
11010
11069
  init_image_audit();
11070
+ init_media_extractor();
11011
11071
  CHUNK = 400;
11072
+ MAX_IMAGES_PER_PAGE = 20;
11073
+ MAX_IMAGES_PER_SITE = 500;
11074
+ IMAGE_DOWNLOAD_CONCURRENCY = 8;
11012
11075
  }
11013
11076
  });
11014
11077
 
@@ -14049,6 +14112,7 @@ var init_server_schemas = __esm({
14049
14112
  rotateProxies: import_zod13.z.boolean().optional(),
14050
14113
  rotateProxyEvery: import_zod13.z.number().int().min(1).max(100).optional(),
14051
14114
  background: import_zod13.z.boolean().optional(),
14115
+ downloadImages: import_zod13.z.boolean().optional(),
14052
14116
  formats: import_zod13.z.array(import_zod13.z.enum(["markdown", "links", "json", "images", "branding", "issues"])).optional()
14053
14117
  });
14054
14118
  YoutubeHarvestBodySchema = import_zod13.z.object({
@@ -20035,9 +20099,24 @@ function buildSeoExport(siteUrl, pages, branding) {
20035
20099
  branding: branding ?? void 0
20036
20100
  };
20037
20101
  }
20102
+ function formatBackgroundJobStarted(toolLabel, data) {
20103
+ if (data.status !== "pending" || typeof data.jobId !== "string") return null;
20104
+ const jobId = data.jobId;
20105
+ const full = [
20106
+ `# ${toolLabel} \u2014 export started`,
20107
+ `**Job ID:** \`${jobId}\``,
20108
+ `
20109
+ Running in the background \u2014 this can take a while for large sites.`,
20110
+ `
20111
+ Poll \`check_site_export\` with this jobId to get the download link once it's ready.`
20112
+ ].join("\n");
20113
+ return { content: [{ type: "text", text: full }], structuredContent: { jobId, status: "pending" } };
20114
+ }
20038
20115
  async function formatExtractSite(raw, input, ctx) {
20039
20116
  const parsed = parseData(raw);
20040
20117
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
20118
+ const started = formatBackgroundJobStarted("Multi-Page Site Content Crawl", parsed.data);
20119
+ if (started) return started;
20041
20120
  const d = parsed.data;
20042
20121
  const pages = d.pages ?? [];
20043
20122
  const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
@@ -20155,6 +20234,8 @@ ${pageDetails}${tips}`;
20155
20234
  async function formatAuditSite(raw, input, ctx) {
20156
20235
  const parsed = parseData(raw);
20157
20236
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
20237
+ const started = formatBackgroundJobStarted("Technical SEO Audit", parsed.data);
20238
+ if (started) return started;
20158
20239
  const d = parsed.data;
20159
20240
  const pages = d.pages ?? [];
20160
20241
  const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
@@ -20226,6 +20307,42 @@ ${imgLine}`
20226
20307
  ].join("\n");
20227
20308
  return { content: [{ type: "text", text: full }], structuredContent };
20228
20309
  }
20310
+ function formatCheckSiteExport(raw, input) {
20311
+ const parsed = parseData(raw);
20312
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
20313
+ const d = parsed.data;
20314
+ const bundle = (d.artifacts ?? []).find((a) => a.key.endsWith("bundle.zip")) ?? null;
20315
+ const progress = d.totalUrls ? `${d.doneUrls ?? 0}/${d.totalUrls} pages` : "starting";
20316
+ const body = d.status === "complete" && bundle ? `
20317
+ ## \u2705 Ready
20318
+ **Download:** ${bundle.url}
20319
+ **Size:** ${(bundle.bytes / 1e6).toFixed(1)} MB` : d.status === "complete" ? `
20320
+ ## \u26A0\uFE0F Complete, but no bundle was produced
20321
+ Check the job's artifacts \u2014 nothing matched \`bundle.zip\`.` : d.status === "failed" ? `
20322
+ ## \u274C Failed
20323
+ ${d.error ?? "no error message recorded"}` : `
20324
+ ## \u23F3 Not ready yet
20325
+ Status: ${d.status} (${progress}). Poll again shortly.`;
20326
+ const full = [
20327
+ `# Site Export: ${d.startUrl ?? input.jobId}`,
20328
+ `**Job ID:** \`${d.jobId}\``,
20329
+ `**Status:** ${d.status}`,
20330
+ body
20331
+ ].join("\n");
20332
+ return {
20333
+ ...oneBlock(full),
20334
+ structuredContent: {
20335
+ jobId: d.jobId,
20336
+ status: d.status,
20337
+ startUrl: d.startUrl,
20338
+ totalUrls: d.totalUrls,
20339
+ doneUrls: d.doneUrls,
20340
+ bundleUrl: bundle?.url ?? null,
20341
+ bundleBytes: bundle?.bytes ?? null,
20342
+ error: d.error ?? null
20343
+ }
20344
+ };
20345
+ }
20229
20346
  function formatYoutubeHarvest(raw, input) {
20230
20347
  const parsed = parseData(raw);
20231
20348
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -25043,7 +25160,7 @@ async function capturePageSnapshot(target, options = {}) {
25043
25160
  async function capturePageSnapshots(targets, options = {}) {
25044
25161
  const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
25045
25162
  const maxConcurrency = normalizeMaxConcurrency(options.maxConcurrency);
25046
- const limit = (0, import_p_limit3.default)(maxConcurrency);
25163
+ const limit = (0, import_p_limit4.default)(maxConcurrency);
25047
25164
  const pageSnapshotArtifacts = await Promise.all(
25048
25165
  targets.map((target) => limit(() => capturePageSnapshot(target, { ...options, timeoutMs })))
25049
25166
  );
@@ -25068,12 +25185,12 @@ async function capturePageSnapshots(targets, options = {}) {
25068
25185
  }
25069
25186
  };
25070
25187
  }
25071
- var import_node_crypto10, import_p_limit3, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_CONTENT_CHARS;
25188
+ var import_node_crypto10, import_p_limit4, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_CONTENT_CHARS;
25072
25189
  var init_page_snapshot_extractor = __esm({
25073
25190
  "src/serp-intelligence/page-snapshot-extractor.ts"() {
25074
25191
  "use strict";
25075
25192
  import_node_crypto10 = require("crypto");
25076
- import_p_limit3 = __toESM(require("p-limit"), 1);
25193
+ import_p_limit4 = __toESM(require("p-limit"), 1);
25077
25194
  init_kpo_extractor();
25078
25195
  init_url_utils();
25079
25196
  DEFAULT_TIMEOUT_MS = 15e3;
@@ -27740,7 +27857,7 @@ var PACKAGE_VERSION;
27740
27857
  var init_version = __esm({
27741
27858
  "src/version.ts"() {
27742
27859
  "use strict";
27743
- PACKAGE_VERSION = "0.19.0";
27860
+ PACKAGE_VERSION = "0.20.0";
27744
27861
  }
27745
27862
  });
27746
27863
 
@@ -28127,7 +28244,7 @@ var init_meta_ad_creative_media = __esm({
28127
28244
  });
28128
28245
 
28129
28246
  // src/mcp/mcp-tool-schemas.ts
28130
- var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, TranscriptSignalOutput, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema, ListServiceConnectionsInputSchema, ListServiceConnectionsOutputSchema, ReadServiceConnectionInputSchema, ReadServiceConnectionOutputSchema, MetaAdCreativeMediaInputSchema, MetaAdCreativeMediaOutputSchema, ImportServiceConnectionToMemoryInputSchema, ImportServiceConnectionToMemoryOutputSchema, DescribeServiceConnectionToolInputSchema, DescribeServiceConnectionToolOutputSchema, ConnectedDataContinuationSchema, ExportConnectedServiceDataInputSchema, ConnectedDataArtifactSchema, ExportConnectedServiceDataOutputSchema, RenewConnectedDataExportDownloadInputSchema, RenewConnectedDataExportDownloadOutputSchema, CallServiceConnectionActionInputSchema, CallServiceConnectionActionOutputSchema, SetScheduledActionConnectionsInputSchema, SetScheduledActionConnectionsOutputSchema, SlackSendMessageInputSchema, SlackSendMessageOutputSchema, GmailSendMessageInputSchema, GmailSendMessageOutputSchema, GoogleCalendarCreateEventInputSchema, GoogleCalendarCreateEventOutputSchema, ZoomCreateMeetingInputSchema, ZoomCreateMeetingOutputSchema;
28247
+ var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, CheckSiteExportInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, CheckSiteExportOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, TranscriptSignalOutput, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema, ListServiceConnectionsInputSchema, ListServiceConnectionsOutputSchema, ReadServiceConnectionInputSchema, ReadServiceConnectionOutputSchema, MetaAdCreativeMediaInputSchema, MetaAdCreativeMediaOutputSchema, ImportServiceConnectionToMemoryInputSchema, ImportServiceConnectionToMemoryOutputSchema, DescribeServiceConnectionToolInputSchema, DescribeServiceConnectionToolOutputSchema, ConnectedDataContinuationSchema, ExportConnectedServiceDataInputSchema, ConnectedDataArtifactSchema, ExportConnectedServiceDataOutputSchema, RenewConnectedDataExportDownloadInputSchema, RenewConnectedDataExportDownloadOutputSchema, CallServiceConnectionActionInputSchema, CallServiceConnectionActionOutputSchema, SetScheduledActionConnectionsInputSchema, SetScheduledActionConnectionsOutputSchema, SlackSendMessageInputSchema, SlackSendMessageOutputSchema, GmailSendMessageInputSchema, GmailSendMessageOutputSchema, GoogleCalendarCreateEventInputSchema, GoogleCalendarCreateEventOutputSchema, ZoomCreateMeetingInputSchema, ZoomCreateMeetingOutputSchema;
28131
28248
  var init_mcp_tool_schemas = __esm({
28132
28249
  "src/mcp/mcp-tool-schemas.ts"() {
28133
28250
  "use strict";
@@ -28169,13 +28286,20 @@ var init_mcp_tool_schemas = __esm({
28169
28286
  maxPages: import_zod33.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to extract. Bulk crawls (over 25 pages) switch to folder mode: each page saved as its own Markdown file, with a summary plus folder path returned instead of inlining content."),
28170
28287
  rotateProxies: import_zod33.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks (403/429). Slower and pricier \u2014 use only when a site blocks normal crawling."),
28171
28288
  rotateProxyEvery: import_zod33.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
28172
- formats: import_zod33.z.array(import_zod33.z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Per-page output formats: markdown, links, json, images are captured cheaply from HTML; branding (site-level logo/colors/fonts) requires a browser and adds time. Defaults to markdown+links.")
28289
+ formats: import_zod33.z.array(import_zod33.z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Per-page output formats: markdown, links, json, images are captured cheaply from HTML; branding (site-level logo/colors/fonts) requires a browser and adds time. Defaults to markdown+links."),
28290
+ background: import_zod33.z.boolean().default(false).describe("Run the crawl as a background job instead of blocking this call, returning a jobId immediately \u2014 poll it with check_site_export to get a downloadable zip (all page content, plus real image files if downloadImages is set) once ready. Use for large sites where a synchronous call would be slow."),
28291
+ downloadImages: import_zod33.z.boolean().default(false).describe("Download every discovered image as a real file into the export bundle (not just image URLs/stats). OFF by default \u2014 must be explicitly set true. Implies background regardless of the background flag, since downloading a whole site's images is too slow to run synchronously. Capped at 20 images/page and 500 images/site.")
28173
28292
  };
28174
28293
  AuditSiteInputSchema = {
28175
28294
  url: import_zod33.z.string().url().describe("Public website URL or domain for a full technical SEO audit (issues, link graph, indexability, headings, images). For plain content use extract_site instead."),
28176
28295
  maxPages: import_zod33.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to crawl and audit. Always writes a folder of analysis files plus per-page content, returning a summary plus the folder path."),
28177
28296
  rotateProxies: import_zod33.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks. Slower/pricier \u2014 use only when a site blocks normal crawling."),
28178
- rotateProxyEvery: import_zod33.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30.")
28297
+ rotateProxyEvery: import_zod33.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
28298
+ background: import_zod33.z.boolean().default(false).describe("Run the audit as a background job instead of blocking this call, returning a jobId immediately \u2014 poll it with check_site_export to get a downloadable zip (full audit report, all page content, plus real image files if downloadImages is set) once ready. Use for large sites where a synchronous call would be slow."),
28299
+ downloadImages: import_zod33.z.boolean().default(false).describe("Download every discovered image as a real file into the export bundle (not just image URLs/stats). OFF by default \u2014 must be explicitly set true. Implies background regardless of the background flag, since downloading a whole site's images is too slow to run synchronously. Capped at 20 images/page and 500 images/site.")
28300
+ };
28301
+ CheckSiteExportInputSchema = {
28302
+ jobId: import_zod33.z.string().min(1).describe('The jobId returned by extract_site or audit_site when called with background (or downloadImages) set \u2014 poll this until status is "complete" (or "failed").')
28179
28303
  };
28180
28304
  YoutubeHarvestInputSchema = {
28181
28305
  mode: import_zod33.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
@@ -28578,36 +28702,52 @@ var init_mcp_tool_schemas = __esm({
28578
28702
  };
28579
28703
  ExtractSiteOutputSchema = {
28580
28704
  url: import_zod33.z.string(),
28581
- pageCount: import_zod33.z.number().int().min(0),
28705
+ pageCount: import_zod33.z.number().int().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
28582
28706
  pages: import_zod33.z.array(import_zod33.z.object({
28583
28707
  url: import_zod33.z.string(),
28584
28708
  title: NullableString,
28585
28709
  schemaTypes: import_zod33.z.array(import_zod33.z.string())
28586
- })),
28587
- durationMs: import_zod33.z.number().min(0),
28710
+ })).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
28711
+ durationMs: import_zod33.z.number().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
28588
28712
  truncatedCount: import_zod33.z.number().int().min(0).optional(),
28589
- artifact: ArtifactPointerOutputSchema.optional()
28713
+ artifact: ArtifactPointerOutputSchema.optional(),
28714
+ jobId: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 poll with check_site_export."),
28715
+ status: import_zod33.z.enum(["pending"]).optional().describe("Present when background (or downloadImages) was set."),
28716
+ statusUrl: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 informational; use check_site_export with jobId, not this URL directly.")
28590
28717
  };
28591
28718
  AuditSiteOutputSchema = {
28592
28719
  url: import_zod33.z.string(),
28593
- pageCount: import_zod33.z.number().int().min(0),
28594
- durationMs: import_zod33.z.number().min(0),
28595
- bulkFolder: import_zod33.z.string().nullable(),
28596
- issues: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.number()),
28720
+ pageCount: import_zod33.z.number().int().min(0).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28721
+ durationMs: import_zod33.z.number().min(0).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28722
+ bulkFolder: import_zod33.z.string().nullable().optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28723
+ issues: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.number()).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28597
28724
  images: import_zod33.z.object({
28598
28725
  unique: import_zod33.z.number().int().min(0),
28599
28726
  totalBytes: import_zod33.z.number().min(0),
28600
28727
  over100kb: import_zod33.z.number().int().min(0),
28601
28728
  legacyFormat: import_zod33.z.number().int().min(0)
28602
- }),
28729
+ }).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28603
28730
  links: import_zod33.z.object({
28604
28731
  internal: import_zod33.z.number().int().min(0),
28605
28732
  external: import_zod33.z.number().int().min(0),
28606
28733
  orphans: import_zod33.z.number().int().min(0),
28607
28734
  brokenInternal: import_zod33.z.number().int().min(0),
28608
28735
  externalDomains: import_zod33.z.number().int().min(0)
28609
- }),
28610
- artifact: ArtifactPointerOutputSchema.optional()
28736
+ }).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28737
+ artifact: ArtifactPointerOutputSchema.optional(),
28738
+ jobId: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 poll with check_site_export."),
28739
+ status: import_zod33.z.enum(["pending"]).optional().describe("Present when background (or downloadImages) was set."),
28740
+ statusUrl: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 informational; use check_site_export with jobId, not this URL directly.")
28741
+ };
28742
+ CheckSiteExportOutputSchema = {
28743
+ jobId: import_zod33.z.string(),
28744
+ status: import_zod33.z.enum(["pending", "running", "complete", "failed"]),
28745
+ startUrl: import_zod33.z.string().optional(),
28746
+ totalUrls: import_zod33.z.number().int().min(0).optional(),
28747
+ doneUrls: import_zod33.z.number().int().min(0).optional(),
28748
+ bundleUrl: import_zod33.z.string().nullable().describe("Downloadable zip URL once status is complete; null otherwise."),
28749
+ bundleBytes: import_zod33.z.number().int().min(0).nullable().describe("Zip size in bytes once status is complete; null otherwise."),
28750
+ error: import_zod33.z.string().nullable().optional().describe("Present with a message when status is failed.")
28611
28751
  };
28612
28752
  MapsPlaceIntelOutputSchema = {
28613
28753
  name: import_zod33.z.string(),
@@ -29919,6 +30059,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29919
30059
  outputSchema: recordOutputSchema("audit_site", AuditSiteOutputSchema),
29920
30060
  annotations: liveWebToolAnnotations("Technical SEO Audit")
29921
30061
  }, async (input) => formatAuditSite(await executor.auditSite(input), input, ctx));
30062
+ server.registerTool("check_site_export", {
30063
+ title: "Check Site Export",
30064
+ description: "Poll the status of a background extract_site or audit_site job (one started with background or downloadImages set). Returns a downloadable zip URL (all page content, plus real image files if downloadImages was set) once status is complete.",
30065
+ inputSchema: CheckSiteExportInputSchema,
30066
+ outputSchema: recordOutputSchema("check_site_export", CheckSiteExportOutputSchema),
30067
+ annotations: liveWebToolAnnotations("Check Site Export")
30068
+ }, async (input) => formatCheckSiteExport(await executor.checkSiteExport(input), input));
29922
30069
  server.registerTool("youtube_harvest", {
29923
30070
  title: "YouTube Video Harvest",
29924
30071
  description: 'Harvest YouTube video metadata by topic search or channel library. Use mode "search" for keyword/topic requests, mode "channel" for @handles/channel IDs/URLs. Returns titles, views, durations, and videoIds.',
@@ -30436,6 +30583,9 @@ var init_http_mcp_tool_executor = __esm({
30436
30583
  auditSite(input) {
30437
30584
  return this.call("/extract-site", input);
30438
30585
  }
30586
+ checkSiteExport(input) {
30587
+ return this.getJson(`/extract-site/status/${encodeURIComponent(input.jobId)}`);
30588
+ }
30439
30589
  youtubeHarvest(input) {
30440
30590
  return this.call("/youtube/harvest", input);
30441
30591
  }
@@ -41906,7 +42056,7 @@ var init_server = __esm({
41906
42056
  const user = await getUserByApiKey(key);
41907
42057
  if (!user) return c.json({ error: "Invalid or inactive API key" }, 401);
41908
42058
  const refreshed = await applyMonthlyFreeRefresh(user);
41909
- const balanceMc = await reconcileBalanceMc(refreshed.id);
42059
+ const balanceMc = await reconcileBalanceMc(refreshed.id, refreshed.balance_mc);
41910
42060
  c.set("user", { ...refreshed, balance_mc: balanceMc });
41911
42061
  return next();
41912
42062
  });
@@ -41925,7 +42075,7 @@ var init_server = __esm({
41925
42075
  const user = await getUserById(userId);
41926
42076
  if (!user) return c.json({ error: "User not found" }, 401);
41927
42077
  const refreshed = await applyMonthlyFreeRefresh(user);
41928
- const balanceMc = await reconcileBalanceMc(refreshed.id);
42078
+ const balanceMc = await reconcileBalanceMc(refreshed.id, refreshed.balance_mc);
41929
42079
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
41930
42080
  return next();
41931
42081
  });
@@ -42115,7 +42265,7 @@ var init_server = __esm({
42115
42265
  }
42116
42266
  const refreshed = await applyMonthlyFreeRefresh(foundUser);
42117
42267
  const [balanceMc, stats] = await Promise.all([
42118
- reconcileBalanceMc(refreshed.id),
42268
+ reconcileBalanceMc(refreshed.id, refreshed.balance_mc),
42119
42269
  getUserStats(refreshed.id)
42120
42270
  ]);
42121
42271
  const user = { ...refreshed, balance_mc: balanceMc };
@@ -43383,7 +43533,8 @@ var init_server = __esm({
43383
43533
  if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
43384
43534
  const parsed = checked.parsed;
43385
43535
  const user = c.get("user");
43386
- if (body.background === true) {
43536
+ const downloadImages = body.downloadImages === true;
43537
+ if (body.background === true || downloadImages) {
43387
43538
  const affordablePages = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
43388
43539
  if (affordablePages < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
43389
43540
  const maxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 1e4)), affordablePages);
@@ -43398,6 +43549,7 @@ var init_server = __esm({
43398
43549
  concurrency: concurrencyLimitForUser(user),
43399
43550
  urlsPerBrowser: body.rotateProxyEvery ?? 10,
43400
43551
  formats: body.formats,
43552
+ downloadImages,
43401
43553
  heldMc
43402
43554
  });
43403
43555
  jobCreated = true;
@@ -43706,7 +43858,7 @@ var init_server = __esm({
43706
43858
  });
43707
43859
  app.get("/billing/balance", auth2, async (c) => {
43708
43860
  const user = c.get("user");
43709
- const balanceMc = await reconcileBalanceMc(user.id);
43861
+ const balanceMc = user.balance_mc;
43710
43862
  const ledger = await getLedger(user.id, 20);
43711
43863
  const freeCredits = await getFreeCreditBreakdown(user.id);
43712
43864
  return c.json({
@@ -43718,7 +43870,7 @@ var init_server = __esm({
43718
43870
  });
43719
43871
  app.post("/billing/credits", auth2, async (c) => {
43720
43872
  const user = c.get("user");
43721
- const balanceMc = await reconcileBalanceMc(user.id);
43873
+ const balanceMc = user.balance_mc;
43722
43874
  const body = await c.req.json().catch(() => ({}));
43723
43875
  const query = body.item?.trim().toLowerCase();
43724
43876
  const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => cost);