mcp-scraper 0.3.13 → 0.3.15
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.
- package/dist/bin/api-server.cjs +1991 -508
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +9 -9
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +428 -47
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +6 -5
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +419 -38
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/chunk-2MX5WOII.js +7 -0
- package/dist/chunk-2MX5WOII.js.map +1 -0
- package/dist/{chunk-DBQDG7EH.js → chunk-5H22TOXQ.js} +28 -1
- package/dist/chunk-5H22TOXQ.js.map +1 -0
- package/dist/{chunk-KPXMPAJ3.js → chunk-7Y6JDBML.js} +2 -2
- package/dist/chunk-7Y6JDBML.js.map +1 -0
- package/dist/{chunk-QAOIH5U2.js → chunk-WJ2LWHPA.js} +10 -10
- package/dist/chunk-WJ2LWHPA.js.map +1 -0
- package/dist/{chunk-YGV2XZDT.js → chunk-WQAIOY2D.js} +424 -39
- package/dist/chunk-WQAIOY2D.js.map +1 -0
- package/dist/{chunk-AZ5PKU4F.js → chunk-ZAUMSBV3.js} +2 -2
- package/dist/{db-BE4JVB3V.js → db-P76GVIFN.js} +2 -2
- package/dist/{server-LX65PB7D.js → server-UOQ2JIUQ.js} +1106 -93
- package/dist/server-UOQ2JIUQ.js.map +1 -0
- package/dist/{worker-FG7ZWEGA.js → worker-2WVKKCC7.js} +3 -3
- package/docs/final-tooling-spec.md +206 -0
- package/docs/mcp-tool-craft-lint.generated.md +1 -1
- package/docs/mcp-tool-design-guide.md +225 -0
- package/docs/mcp-tool-manifest.generated.json +13 -13
- package/docs/seo-crawl-report-spec.md +287 -0
- package/docs/tool-catalog-spec.md +388 -0
- package/package.json +2 -1
- package/dist/chunk-BEZJ4G32.js +0 -7
- package/dist/chunk-BEZJ4G32.js.map +0 -1
- package/dist/chunk-DBQDG7EH.js.map +0 -1
- package/dist/chunk-KPXMPAJ3.js.map +0 -1
- package/dist/chunk-QAOIH5U2.js.map +0 -1
- package/dist/chunk-YGV2XZDT.js.map +0 -1
- package/dist/server-LX65PB7D.js.map +0 -1
- /package/dist/{chunk-AZ5PKU4F.js.map → chunk-ZAUMSBV3.js.map} +0 -0
- /package/dist/{db-BE4JVB3V.js.map → db-P76GVIFN.js.map} +0 -0
- /package/dist/{worker-FG7ZWEGA.js.map → worker-2WVKKCC7.js.map} +0 -0
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./chunk-LFATOGDF.js";
|
|
12
12
|
import {
|
|
13
13
|
PACKAGE_VERSION
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-2MX5WOII.js";
|
|
15
15
|
|
|
16
16
|
// src/harvest-timeout.ts
|
|
17
17
|
var VERCEL_FUNCTION_MAX_MS = 3e5;
|
|
@@ -27,6 +27,33 @@ function harvestTimeoutBudget(maxQuestions, serpOnly = false) {
|
|
|
27
27
|
return { serverMs, clientMs };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// src/mcp/server-instructions.ts
|
|
31
|
+
var SERVER_INSTRUCTIONS = `
|
|
32
|
+
This server scrapes and analyzes web, social, search, maps, and site data. Pick a tool by NAME using
|
|
33
|
+
this routing map, then load its schema before calling.
|
|
34
|
+
|
|
35
|
+
ROUTING
|
|
36
|
+
- One web page -> extract_url. Whole site (crawl + SEO report) -> extract_site. Just the URL list -> map_site_urls.
|
|
37
|
+
- Web search (organic SERP) -> search_serp. Full SERP + People-Also-Ask / AI-Overview detail -> harvest_paa.
|
|
38
|
+
- Google Maps: find places -> maps_search; one place deep-dive + reviews -> maps_place_intel.
|
|
39
|
+
- YouTube: find or list videos -> youtube_harvest; transcribe one video -> youtube_transcribe.
|
|
40
|
+
- Facebook: find ads -> facebook_ad_search; transcribe an ad -> facebook_ad_transcribe; transcribe a
|
|
41
|
+
video -> facebook_video_transcribe; page profile/intel -> facebook_page_intel.
|
|
42
|
+
- Instagram: profile inventory -> instagram_profile_content; one post or reel -> instagram_media_download.
|
|
43
|
+
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
44
|
+
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
45
|
+
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
46
|
+
(browser_open, then navigate/read); save/reuse logins via browser_profile_*.
|
|
47
|
+
|
|
48
|
+
NOTES
|
|
49
|
+
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
50
|
+
It discovers URLs and returns a saved folder/artifact plus a summary, not the full content inline.
|
|
51
|
+
- Tools that open a browser session or transcribe media cost credits and take longer. Prefer the
|
|
52
|
+
cheapest tool that answers the question (plain search/extract before browser agents).
|
|
53
|
+
- Large results are saved to disk or an artifact and returned as a summary plus a path; read the path
|
|
54
|
+
for full detail rather than expecting the whole payload inline.
|
|
55
|
+
`.trim();
|
|
56
|
+
|
|
30
57
|
// src/mcp/paa-mcp-server.ts
|
|
31
58
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
32
59
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
@@ -152,6 +179,191 @@ function suggestWorkflowRecipes(goal, limit = 3) {
|
|
|
152
179
|
return scored.sort((a, b) => b.score - a.score || a.recipe.title.localeCompare(b.recipe.title)).slice(0, Math.max(1, limit)).map((item) => item.recipe);
|
|
153
180
|
}
|
|
154
181
|
|
|
182
|
+
// src/api/seo-link-graph.ts
|
|
183
|
+
function normalize2(u) {
|
|
184
|
+
return u.split("#")[0].replace(/\/$/, "");
|
|
185
|
+
}
|
|
186
|
+
function buildLinkGraph(pages, startUrl) {
|
|
187
|
+
const statusByUrl = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const p of pages) statusByUrl.set(normalize2(p.url), p.status);
|
|
189
|
+
const edges = [];
|
|
190
|
+
for (const p of pages) {
|
|
191
|
+
for (const l of p.outlinks ?? []) {
|
|
192
|
+
const nofollow = (l.rel ?? "").split(/\s+/).includes("nofollow");
|
|
193
|
+
edges.push({
|
|
194
|
+
from: p.url,
|
|
195
|
+
to: l.href,
|
|
196
|
+
anchor: l.anchor,
|
|
197
|
+
rel: l.rel,
|
|
198
|
+
internal: l.internal,
|
|
199
|
+
nofollow,
|
|
200
|
+
targetStatus: statusByUrl.get(normalize2(l.href)) ?? null
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const inboundFrom = /* @__PURE__ */ new Map();
|
|
205
|
+
const inboundAnchors = /* @__PURE__ */ new Map();
|
|
206
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const e of edges) {
|
|
208
|
+
if (!e.internal) continue;
|
|
209
|
+
const to = normalize2(e.to);
|
|
210
|
+
const from = normalize2(e.from);
|
|
211
|
+
if (!inboundFrom.has(to)) inboundFrom.set(to, /* @__PURE__ */ new Set());
|
|
212
|
+
inboundFrom.get(to).add(from);
|
|
213
|
+
if (e.anchor) {
|
|
214
|
+
if (!inboundAnchors.has(to)) inboundAnchors.set(to, []);
|
|
215
|
+
inboundAnchors.get(to).push(e.anchor);
|
|
216
|
+
}
|
|
217
|
+
if (!e.nofollow && (statusByUrl.get(to) === 200 || !statusByUrl.has(to))) {
|
|
218
|
+
if (!adjacency.has(from)) adjacency.set(from, /* @__PURE__ */ new Set());
|
|
219
|
+
adjacency.get(from).add(to);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const depth = /* @__PURE__ */ new Map();
|
|
223
|
+
const start = normalize2(startUrl);
|
|
224
|
+
const queue = [start];
|
|
225
|
+
depth.set(start, 0);
|
|
226
|
+
while (queue.length > 0) {
|
|
227
|
+
const cur = queue.shift();
|
|
228
|
+
const d = depth.get(cur);
|
|
229
|
+
for (const next of adjacency.get(cur) ?? []) {
|
|
230
|
+
if (!depth.has(next)) {
|
|
231
|
+
depth.set(next, d + 1);
|
|
232
|
+
queue.push(next);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const topAnchorsFor = (url) => {
|
|
237
|
+
const counts = /* @__PURE__ */ new Map();
|
|
238
|
+
for (const a of inboundAnchors.get(url) ?? []) counts.set(a, (counts.get(a) ?? 0) + 1);
|
|
239
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map((e) => e[0]);
|
|
240
|
+
};
|
|
241
|
+
const metrics = /* @__PURE__ */ new Map();
|
|
242
|
+
for (const p of pages) {
|
|
243
|
+
const key = normalize2(p.url);
|
|
244
|
+
const inbound = inboundFrom.get(key) ?? /* @__PURE__ */ new Set();
|
|
245
|
+
metrics.set(p.url, {
|
|
246
|
+
url: p.url,
|
|
247
|
+
inlinks: inbound.size,
|
|
248
|
+
uniqueInlinks: inbound.size,
|
|
249
|
+
outlinksInternal: (p.outlinks ?? []).filter((l) => l.internal).length,
|
|
250
|
+
outlinksExternal: (p.outlinks ?? []).filter((l) => !l.internal).length,
|
|
251
|
+
crawlDepth: depth.has(key) ? depth.get(key) : null,
|
|
252
|
+
orphan: inbound.size === 0 && key !== start,
|
|
253
|
+
topAnchors: topAnchorsFor(key)
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return { edges, metrics };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/api/seo-issues.ts
|
|
260
|
+
var THIN_WORDS = 200;
|
|
261
|
+
var TITLE_MAX = 60;
|
|
262
|
+
var TITLE_PX_MAX = 561;
|
|
263
|
+
var TITLE_MIN = 30;
|
|
264
|
+
var META_MAX = 155;
|
|
265
|
+
var META_MIN = 70;
|
|
266
|
+
var H1_MAX = 70;
|
|
267
|
+
var URL_MAX = 115;
|
|
268
|
+
function dupes(pages, key) {
|
|
269
|
+
const groups = /* @__PURE__ */ new Map();
|
|
270
|
+
for (const p of pages) {
|
|
271
|
+
const k = key(p);
|
|
272
|
+
if (k === null || k === void 0 || k === "") continue;
|
|
273
|
+
const ks = String(k);
|
|
274
|
+
if (!groups.has(ks)) groups.set(ks, []);
|
|
275
|
+
groups.get(ks).push(p.url);
|
|
276
|
+
}
|
|
277
|
+
return [...groups.values()].filter((g) => g.length > 1).flat();
|
|
278
|
+
}
|
|
279
|
+
function computeIssues(pages, metrics) {
|
|
280
|
+
const group = (urls) => ({ count: urls.length, urls: urls.slice(0, 200) });
|
|
281
|
+
const where = (fn) => pages.filter(fn).map((p) => p.url);
|
|
282
|
+
const ok = (p) => p.status === 200;
|
|
283
|
+
const pathname = (u) => {
|
|
284
|
+
try {
|
|
285
|
+
return new URL(u).pathname;
|
|
286
|
+
} catch {
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const report = {
|
|
291
|
+
"title.missing": group(where((p) => ok(p) && !p.title)),
|
|
292
|
+
"title.duplicate": group(dupes(pages.filter(ok), (p) => p.title)),
|
|
293
|
+
"title.tooLong": group(where((p) => (p.titleLength ?? 0) > TITLE_MAX || (p.titlePixels ?? 0) > TITLE_PX_MAX)),
|
|
294
|
+
"title.tooShort": group(where((p) => p.title != null && (p.titleLength ?? 0) < TITLE_MIN)),
|
|
295
|
+
"title.sameAsH1": group(where((p) => !!p.title && !!p.h1 && p.title.trim() === p.h1.trim())),
|
|
296
|
+
"meta.missing": group(where((p) => ok(p) && !p.metaDescription)),
|
|
297
|
+
"meta.duplicate": group(dupes(pages.filter(ok), (p) => p.metaDescription)),
|
|
298
|
+
"meta.tooLong": group(where((p) => (p.metaDescLength ?? 0) > META_MAX)),
|
|
299
|
+
"meta.tooShort": group(where((p) => p.metaDescription != null && (p.metaDescLength ?? 0) < META_MIN)),
|
|
300
|
+
"h1.missing": group(where((p) => ok(p) && !p.h1)),
|
|
301
|
+
"h1.multiple": group(where((p) => !!p.h1_2)),
|
|
302
|
+
"h1.duplicate": group(dupes(pages.filter(ok), (p) => p.h1)),
|
|
303
|
+
"h1.tooLong": group(where((p) => (p.h1?.length ?? 0) > H1_MAX)),
|
|
304
|
+
"h2.missing": group(where((p) => ok(p) && p.h2Count === 0)),
|
|
305
|
+
"indexability.nonIndexable": group(where((p) => !p.indexable)),
|
|
306
|
+
"indexability.noindex": group(where((p) => p.indexabilityReason === "noindex")),
|
|
307
|
+
"canonical.missing": group(where((p) => ok(p) && !p.canonicalUrl)),
|
|
308
|
+
"canonical.canonicalised": group(where((p) => p.indexabilityReason === "canonicalised")),
|
|
309
|
+
"response.broken4xx": group(where((p) => (p.status ?? 0) >= 400 && (p.status ?? 0) < 500)),
|
|
310
|
+
"response.error5xx": group(where((p) => (p.status ?? 0) >= 500)),
|
|
311
|
+
"response.redirect3xx": group(where((p) => (p.status ?? 0) >= 300 && (p.status ?? 0) < 400)),
|
|
312
|
+
"response.noResponse": group(where((p) => p.status === null)),
|
|
313
|
+
"content.thin": group(where((p) => ok(p) && p.wordCount > 0 && p.wordCount < THIN_WORDS)),
|
|
314
|
+
"content.exactDuplicate": group(dupes(pages.filter((p) => ok(p) && p.contentHash), (p) => p.contentHash)),
|
|
315
|
+
"images.missingAlt": group(where((p) => p.imagesMissingAlt > 0)),
|
|
316
|
+
"schema.missing": group(where((p) => ok(p) && p.schemaTypes.length === 0)),
|
|
317
|
+
"url.tooLong": group(where((p) => p.url.length > URL_MAX)),
|
|
318
|
+
"url.uppercase": group(where((p) => /[A-Z]/.test(pathname(p.url)))),
|
|
319
|
+
"url.underscores": group(where((p) => pathname(p.url).includes("_"))),
|
|
320
|
+
"links.orphan": group([...metrics.values()].filter((m) => m.orphan).map((m) => m.url))
|
|
321
|
+
};
|
|
322
|
+
const normUrl = (u) => {
|
|
323
|
+
try {
|
|
324
|
+
const x = new URL(u);
|
|
325
|
+
x.hash = "";
|
|
326
|
+
return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
|
|
327
|
+
} catch {
|
|
328
|
+
return u.replace(/#.*$/, "").replace(/\/+$/, "");
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const statusByUrl = new Map(pages.map((p) => [normUrl(p.url), p.status]));
|
|
332
|
+
const brokenLinkPages = /* @__PURE__ */ new Set();
|
|
333
|
+
for (const p of pages) {
|
|
334
|
+
for (const l of p.outlinks ?? []) {
|
|
335
|
+
if (!l.internal) continue;
|
|
336
|
+
const st = statusByUrl.get(normUrl(l.href));
|
|
337
|
+
if (st != null && st >= 400) brokenLinkPages.add(p.url);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
report["links.brokenInternal"] = group([...brokenLinkPages]);
|
|
341
|
+
return report;
|
|
342
|
+
}
|
|
343
|
+
function renderIssueReport(siteUrl, pages, report, metrics) {
|
|
344
|
+
const total = pages.length;
|
|
345
|
+
const sev = (key) => key.startsWith("response.broken") || key.startsWith("response.error") || key.startsWith("links.broken") ? "\u{1F534}" : key.startsWith("title.missing") || key.startsWith("h1.missing") || key.startsWith("indexability") || key.startsWith("canonical.missing") ? "\u{1F7E0}" : "\u{1F7E1}";
|
|
346
|
+
const rows = Object.entries(report).filter(([, g]) => g.count > 0).sort((a, b) => b[1].count - a[1].count).map(([k, g]) => `| ${sev(k)} | \`${k}\` | ${g.count} | ${g.urls.slice(0, 3).join(" \xB7 ")}${g.urls.length > 3 ? " \u2026" : ""} |`);
|
|
347
|
+
const depths = [...metrics.values()].map((m) => m.crawlDepth).filter((d) => d != null);
|
|
348
|
+
const maxDepth = depths.length ? Math.max(...depths) : 0;
|
|
349
|
+
const orphans = [...metrics.values()].filter((m) => m.orphan).length;
|
|
350
|
+
const topLinked = [...metrics.values()].sort((a, b) => b.inlinks - a.inlinks).slice(0, 5);
|
|
351
|
+
return [
|
|
352
|
+
`# SEO Crawl Report: ${siteUrl}`,
|
|
353
|
+
`**${total} pages crawled** \xB7 max crawl depth ${maxDepth} \xB7 ${orphans} orphan page(s)`,
|
|
354
|
+
`
|
|
355
|
+
## Issues
|
|
356
|
+
| | Issue | Count | Examples |
|
|
357
|
+
|---|-------|-------|----------|
|
|
358
|
+
${rows.join("\n") || "| \u2705 | none | 0 | \u2014 |"}`,
|
|
359
|
+
`
|
|
360
|
+
## Most-linked pages
|
|
361
|
+
${topLinked.map((m) => `- ${m.inlinks} inlinks \xB7 depth ${m.crawlDepth ?? "\u2014"} \xB7 ${m.url}`).join("\n")}`,
|
|
362
|
+
`
|
|
363
|
+
_Thresholds: title ${TITLE_MAX}ch/${TITLE_PX_MAX}px, meta ${META_MAX}ch, H1 ${H1_MAX}ch, thin <${THIN_WORDS} words, URL ${URL_MAX}ch. Pixel widths are estimates._`
|
|
364
|
+
].join("\n");
|
|
365
|
+
}
|
|
366
|
+
|
|
155
367
|
// src/mcp/mcp-response-formatter.ts
|
|
156
368
|
var reportSavingEnabled = true;
|
|
157
369
|
function configureReportSaving(enabled) {
|
|
@@ -185,6 +397,85 @@ function saveFullReport(full) {
|
|
|
185
397
|
return null;
|
|
186
398
|
}
|
|
187
399
|
}
|
|
400
|
+
var BULK_PAGE_THRESHOLD = 25;
|
|
401
|
+
var BULK_URL_THRESHOLD = 500;
|
|
402
|
+
function reportSavingActive() {
|
|
403
|
+
return reportSavingEnabled && process.env.MCP_SCRAPER_SAVE_REPORTS !== "false";
|
|
404
|
+
}
|
|
405
|
+
function saveBulkSite(siteUrl, pages, seo) {
|
|
406
|
+
if (!reportSavingActive()) return null;
|
|
407
|
+
try {
|
|
408
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
409
|
+
const dir = join(outputBaseDir(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
|
|
410
|
+
const pagesDir = join(dir, "pages");
|
|
411
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
412
|
+
const indexRows = pages.map((p, i) => {
|
|
413
|
+
const num = String(i + 1).padStart(4, "0");
|
|
414
|
+
const slug = slugifyReportName(p.url.replace(/^https?:\/\//, "")).slice(0, 60) || "page";
|
|
415
|
+
const fname = `${num}-${slug}.md`;
|
|
416
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
417
|
+
const content = [
|
|
418
|
+
`# ${p.title ?? "Untitled"}`,
|
|
419
|
+
`- **URL:** ${p.url}`,
|
|
420
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
421
|
+
p.schemaTypes?.length ? `- **Schema:** ${p.schemaTypes.join(", ")}` : "",
|
|
422
|
+
"",
|
|
423
|
+
body || "_(no content extracted)_"
|
|
424
|
+
].filter(Boolean).join("\n");
|
|
425
|
+
writeFileSync(join(pagesDir, fname), content, "utf8");
|
|
426
|
+
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
427
|
+
});
|
|
428
|
+
const index = [
|
|
429
|
+
`# Site Extract: ${siteUrl}`,
|
|
430
|
+
`**${pages.length} pages** \u2014 each saved as an individual Markdown file under \`pages/\`.`,
|
|
431
|
+
`
|
|
432
|
+
## Pages
|
|
433
|
+
| # | Title | URL | File |
|
|
434
|
+
|---|-------|-----|------|
|
|
435
|
+
${indexRows.join("\n")}`
|
|
436
|
+
].join("\n");
|
|
437
|
+
const indexFile = join(dir, "index.md");
|
|
438
|
+
writeFileSync(indexFile, index, "utf8");
|
|
439
|
+
let seoFiles;
|
|
440
|
+
if (seo) {
|
|
441
|
+
const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
442
|
+
const pagesJsonl = join(dir, "pages.jsonl");
|
|
443
|
+
const linksJsonl = join(dir, "links.jsonl");
|
|
444
|
+
const metricsJsonl = join(dir, "link-metrics.jsonl");
|
|
445
|
+
const issuesFile = join(dir, "issues.json");
|
|
446
|
+
const reportFile = join(dir, "report.md");
|
|
447
|
+
writeFileSync(pagesJsonl, toJsonl(seo.pageRows), "utf8");
|
|
448
|
+
writeFileSync(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
449
|
+
writeFileSync(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
450
|
+
writeFileSync(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
451
|
+
writeFileSync(reportFile, seo.reportMd, "utf8");
|
|
452
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
|
|
453
|
+
if (seo.branding) {
|
|
454
|
+
const brandingFile = join(dir, "branding.json");
|
|
455
|
+
writeFileSync(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
456
|
+
seoFiles.push(brandingFile);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { dir, indexFile, fileCount: pages.length, seoFiles };
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function saveUrlInventory(siteUrl, urls) {
|
|
465
|
+
if (!reportSavingActive()) return null;
|
|
466
|
+
try {
|
|
467
|
+
const outDir = outputBaseDir();
|
|
468
|
+
mkdirSync(outDir, { recursive: true });
|
|
469
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
470
|
+
const file = join(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
|
|
471
|
+
const csv = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
472
|
+
const rows = ["url,status", ...urls.map((u) => `${csv(u.url)},${u.status ?? ""}`)];
|
|
473
|
+
writeFileSync(file, rows.join("\n"), "utf8");
|
|
474
|
+
return file;
|
|
475
|
+
} catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
188
479
|
function persistScreenshotLocally(base64, url) {
|
|
189
480
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
190
481
|
try {
|
|
@@ -199,8 +490,8 @@ function persistScreenshotLocally(base64, url) {
|
|
|
199
490
|
return null;
|
|
200
491
|
}
|
|
201
492
|
}
|
|
202
|
-
function oneBlock(content) {
|
|
203
|
-
const filePath = saveFullReport(content);
|
|
493
|
+
function oneBlock(content, diskContent) {
|
|
494
|
+
const filePath = saveFullReport(diskContent ?? content);
|
|
204
495
|
const text = filePath ? `${content}
|
|
205
496
|
|
|
206
497
|
\u{1F4C4} Saved: \`${filePath}\`` : content;
|
|
@@ -469,7 +760,12 @@ ${[h1Lines, h2Lines].filter(Boolean).join("\n")}` : "";
|
|
|
469
760
|
].filter(Boolean).join("\n") : "";
|
|
470
761
|
const bodySection = bodyMd ? `
|
|
471
762
|
## Page Content
|
|
472
|
-
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ?
|
|
763
|
+
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
|
|
764
|
+
|
|
765
|
+
*(truncated to 3,000 of ${bodyMd.length.toLocaleString()} chars \u2014 full content in the saved report)*` : ""}` : "";
|
|
766
|
+
const bodySectionFull = bodyMd ? `
|
|
767
|
+
## Page Content
|
|
768
|
+
${bodyMd}` : "";
|
|
473
769
|
const screenshotSection = screenshotMeta ? `
|
|
474
770
|
## Screenshot
|
|
475
771
|
- **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
|
|
@@ -500,7 +796,10 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
|
|
|
500
796
|
const full = `# URL Extract: ${url}
|
|
501
797
|
**${title}**
|
|
502
798
|
${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
|
|
503
|
-
const
|
|
799
|
+
const diskReport = `# URL Extract: ${url}
|
|
800
|
+
**${title}**
|
|
801
|
+
${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
|
|
802
|
+
const textResult = oneBlock(full, diskReport);
|
|
504
803
|
const structuredContent = {
|
|
505
804
|
url,
|
|
506
805
|
title: d.title ?? null,
|
|
@@ -533,7 +832,16 @@ function formatMapSiteUrls(raw, input) {
|
|
|
533
832
|
const ok = urls.filter((u) => (u.status ?? 0) >= 200 && (u.status ?? 0) < 300);
|
|
534
833
|
const broken = urls.filter((u) => u.status !== null && u.status >= 400);
|
|
535
834
|
const redirects = urls.filter((u) => u.status !== null && u.status >= 300 && u.status < 400);
|
|
536
|
-
const
|
|
835
|
+
const isBulk = urls.length > BULK_URL_THRESHOLD;
|
|
836
|
+
const inventoryFile = isBulk ? saveUrlInventory(input.url, urls.map((u) => ({ url: u.url, status: u.status ?? null }))) : null;
|
|
837
|
+
const inlineCount = isBulk ? BULK_URL_THRESHOLD : 200;
|
|
838
|
+
const urlRows = urls.slice(0, inlineCount).map((u, i) => `| ${i + 1} | ${u.url} | ${u.status ?? "\u2014"} |`).join("\n");
|
|
839
|
+
const inventoryNote = isBulk ? inventoryFile ? `
|
|
840
|
+
## \u{1F4C1} Full inventory saved
|
|
841
|
+
- **File:** \`${inventoryFile}\` (CSV: url, status \u2014 all ${urls.length} URLs)
|
|
842
|
+
Showing the first ${inlineCount} below; read the file for the complete list.` : `
|
|
843
|
+
## \u26A0\uFE0F Full inventory not saved
|
|
844
|
+
Report saving is disabled, so only the first ${inlineCount} of ${urls.length} URLs are shown. Enable \`MCP_SCRAPER_SAVE_REPORTS\` to capture the full inventory.` : "";
|
|
537
845
|
const full = [
|
|
538
846
|
`# URL Map: ${input.url}`,
|
|
539
847
|
`**${d.totalFound} URLs** \xB7 ${(d.durationMs / 1e3).toFixed(1)}s${d.truncated ? " \xB7 *truncated*" : ""}`,
|
|
@@ -542,12 +850,13 @@ function formatMapSiteUrls(raw, input) {
|
|
|
542
850
|
- \u2705 2xx: ${ok.length}
|
|
543
851
|
- \u{1F500} 3xx: ${redirects.length}
|
|
544
852
|
- \u274C 4xx+: ${broken.length}`,
|
|
853
|
+
inventoryNote,
|
|
545
854
|
`
|
|
546
|
-
## URL Inventory
|
|
855
|
+
## URL Inventory${isBulk ? ` (first ${inlineCount} of ${urls.length})` : ""}
|
|
547
856
|
| # | URL | Status |
|
|
548
857
|
|---|-----|--------|
|
|
549
858
|
${urlRows}`,
|
|
550
|
-
broken.length ? `
|
|
859
|
+
!isBulk && broken.length ? `
|
|
551
860
|
## Broken URLs
|
|
552
861
|
${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
553
862
|
`
|
|
@@ -565,47 +874,116 @@ ${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
|
565
874
|
okCount: ok.length,
|
|
566
875
|
redirectCount: redirects.length,
|
|
567
876
|
brokenCount: broken.length,
|
|
877
|
+
inventoryFile: inventoryFile ?? null,
|
|
568
878
|
urls: urls.map((u) => ({ url: u.url, status: u.status ?? null })),
|
|
569
879
|
durationMs: d.durationMs ?? 0
|
|
570
880
|
}
|
|
571
881
|
};
|
|
572
882
|
}
|
|
883
|
+
function buildSeoExport(siteUrl, pages, branding) {
|
|
884
|
+
const { edges, metrics } = buildLinkGraph(pages, siteUrl);
|
|
885
|
+
const pageRows = pages.map((p) => {
|
|
886
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
887
|
+
const m = metrics.get(p.url);
|
|
888
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
889
|
+
});
|
|
890
|
+
const issues = computeIssues(pages, metrics);
|
|
891
|
+
return {
|
|
892
|
+
pageRows,
|
|
893
|
+
edges,
|
|
894
|
+
metrics: [...metrics.values()],
|
|
895
|
+
issues,
|
|
896
|
+
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
897
|
+
branding: branding ?? void 0
|
|
898
|
+
};
|
|
899
|
+
}
|
|
573
900
|
function formatExtractSite(raw, input) {
|
|
574
901
|
const parsed = parseData(raw);
|
|
575
902
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
576
903
|
const d = parsed.data;
|
|
577
904
|
const pages = d.pages ?? [];
|
|
578
|
-
const
|
|
579
|
-
|
|
905
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
906
|
+
const pageRow = (p, i) => {
|
|
907
|
+
const schemaInfo = schemaTypesOf(p).join(", ") || "\u2014";
|
|
580
908
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | ${schemaInfo} |`;
|
|
581
|
-
}
|
|
582
|
-
const
|
|
909
|
+
};
|
|
910
|
+
const tips = `
|
|
911
|
+
---
|
|
912
|
+
\u{1F4A1} **Tips**
|
|
913
|
+
- Map URLs first: use \`map_site_urls\`
|
|
914
|
+
- Inspect a single page: use \`extract_url\``;
|
|
915
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
916
|
+
const structuredContent = {
|
|
917
|
+
url: input.url,
|
|
918
|
+
pageCount: pages.length,
|
|
919
|
+
pages: pages.map((p) => ({
|
|
920
|
+
url: String(p.url ?? ""),
|
|
921
|
+
title: p.title ?? null,
|
|
922
|
+
schemaTypes: schemaTypesOf(p)
|
|
923
|
+
})),
|
|
924
|
+
durationMs: d.durationMs ?? 0
|
|
925
|
+
};
|
|
926
|
+
if (pages.length > BULK_PAGE_THRESHOLD) {
|
|
927
|
+
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
928
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
929
|
+
url: String(p.url ?? ""),
|
|
930
|
+
title: p.title ?? null,
|
|
931
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
932
|
+
metaDescription: p.metaDescription ?? null,
|
|
933
|
+
schemaTypes: schemaTypesOf(p)
|
|
934
|
+
})), seo);
|
|
935
|
+
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
936
|
+
const seoLine = bulk?.seoFiles?.length ? `
|
|
937
|
+
- **SEO data:** \`report.md\` (issues summary), \`issues.json\` (findings + offender URLs), \`pages.jsonl\` (per-page fields), \`links.jsonl\` (link edges), \`link-metrics.jsonl\` (inlinks / crawl depth / orphans)` : "";
|
|
938
|
+
const location = bulk ? `
|
|
939
|
+
## \u{1F4C1} Bulk scrape saved
|
|
940
|
+
- **Folder:** \`${bulk.dir}\`
|
|
941
|
+
- **Index:** \`${bulk.indexFile}\`
|
|
942
|
+
- **Files:** ${bulk.fileCount} page files under \`pages/\`${seoLine}
|
|
943
|
+
|
|
944
|
+
Read \`index.md\` for the full page list, open files in \`pages/\` for full content, or read \`pages.jsonl\` / \`link-metrics.jsonl\` for the SEO crawl data. Page bodies are not inlined here to keep the context window clear.` : `
|
|
945
|
+
## \u26A0\uFE0F Bulk scrape not saved
|
|
946
|
+
Report saving is disabled in this environment, so the ${pages.length} pages of content could not be written to disk and are omitted to protect the context window. Enable \`MCP_SCRAPER_SAVE_REPORTS\` to capture bulk crawls.`;
|
|
947
|
+
const full2 = [
|
|
948
|
+
`# Site Extract: ${input.url}`,
|
|
949
|
+
durationLine,
|
|
950
|
+
location,
|
|
951
|
+
`
|
|
952
|
+
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
953
|
+
| # | Title | URL | Schema |
|
|
954
|
+
|---|-------|-----|--------|
|
|
955
|
+
${preview}`,
|
|
956
|
+
tips
|
|
957
|
+
].join("\n");
|
|
958
|
+
return { content: [{ type: "text", text: full2 }], structuredContent: { ...structuredContent, bulkFolder: bulk?.dir ?? null } };
|
|
959
|
+
}
|
|
960
|
+
const header = [
|
|
583
961
|
`# Site Extract: ${input.url}`,
|
|
584
|
-
|
|
962
|
+
durationLine,
|
|
585
963
|
`
|
|
586
964
|
## Pages
|
|
587
965
|
| # | Title | URL | Schema |
|
|
588
966
|
|---|-------|-----|--------|
|
|
589
|
-
${
|
|
590
|
-
`
|
|
591
|
-
---
|
|
592
|
-
\u{1F4A1} **Tips**
|
|
593
|
-
- Map URLs first: use \`map_site_urls\`
|
|
594
|
-
- Inspect a single page: use \`extract_url\``
|
|
967
|
+
${pages.map(pageRow).join("\n")}`
|
|
595
968
|
].join("\n");
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
969
|
+
const full = `${header}${tips}`;
|
|
970
|
+
const pageDetails = pages.map((p, i) => {
|
|
971
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
972
|
+
return [
|
|
973
|
+
`
|
|
974
|
+
## ${i + 1}. ${p.title ?? "Untitled"}`,
|
|
975
|
+
`- **URL:** ${p.url}`,
|
|
976
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
977
|
+
body ? `
|
|
978
|
+
${body}` : "_(no content extracted)_"
|
|
979
|
+
].filter(Boolean).join("\n");
|
|
980
|
+
}).join("\n");
|
|
981
|
+
const diskReport = `${header}
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
# Full Page Content
|
|
985
|
+
${pageDetails}${tips}`;
|
|
986
|
+
return { ...oneBlock(full, diskReport), structuredContent };
|
|
609
987
|
}
|
|
610
988
|
function formatYoutubeHarvest(raw, input) {
|
|
611
989
|
const parsed = parseData(raw);
|
|
@@ -1806,11 +2184,14 @@ var ExtractUrlInputSchema = {
|
|
|
1806
2184
|
};
|
|
1807
2185
|
var MapSiteUrlsInputSchema = {
|
|
1808
2186
|
url: z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
1809
|
-
maxUrls: z.number().int().min(1).max(
|
|
2187
|
+
maxUrls: z.number().int().min(1).max(1e4).optional().describe("Maximum URLs to discover. Use 100 for normal maps, up to 10000 for a full inventory. Large maps (over 500 URLs) write the complete inventory to a local file and return only a summary plus the file path instead of the full list inline.")
|
|
1810
2188
|
};
|
|
1811
2189
|
var ExtractSiteInputSchema = {
|
|
1812
2190
|
url: z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
|
|
1813
|
-
maxPages: z.number().int().min(1).max(
|
|
2191
|
+
maxPages: z.number().int().min(1).max(1e4).optional().describe("Maximum pages to extract. Use 50 for a normal crawl, up to 10000 for a full-site bulk scrape. Bulk crawls (over 25 pages) switch to folder mode: every page is saved as its own Markdown file in a local folder and the response returns only a summary plus the folder path, so the full content never floods the context window."),
|
|
2192
|
+
rotateProxies: z.boolean().optional().describe("Route page fetches through rotating residential proxies in headful browsers to defeat rate-limiting and bot blocks (403/429). Discovers URLs from the sitemap, fetches in batches with a fresh proxy per batch, retries failures on a new proxy, and automatically parallelizes across the account's concurrency slots. Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
|
|
2193
|
+
rotateProxyEvery: z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, how many pages to fetch per browser/proxy before discarding it and minting a fresh one. Default 30. Lower values rotate IPs more aggressively against strict rate limits."),
|
|
2194
|
+
formats: z.array(z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Which data formats to include in the per-page output. markdown=page content as Markdown, links=internal/external link graph, json=structured data/schema, images=per-page image URLs (links only, no downloads), branding=site-level logo/colors/fonts captured once from the homepage (requires a browser, adds time). markdown, links, json, and images are always captured cheaply from the HTML; branding is the only opt-in capture. Defaults to markdown+links when omitted.")
|
|
1814
2195
|
};
|
|
1815
2196
|
var YoutubeHarvestInputSchema = {
|
|
1816
2197
|
mode: z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
@@ -2915,7 +3296,7 @@ function registerSavedReportResources(server) {
|
|
|
2915
3296
|
);
|
|
2916
3297
|
}
|
|
2917
3298
|
function buildPaaExtractorMcpServer(executor, options = {}) {
|
|
2918
|
-
const server = new McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
3299
|
+
const server = new McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
2919
3300
|
registerPaaExtractorMcpTools(server, executor, options);
|
|
2920
3301
|
return server;
|
|
2921
3302
|
}
|
|
@@ -2947,14 +3328,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
2947
3328
|
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
2948
3329
|
server.registerTool("map_site_urls", {
|
|
2949
3330
|
title: "Site URL Map",
|
|
2950
|
-
description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
|
|
3331
|
+
description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Supports up to maxUrls=10000; maps over 500 URLs write the complete inventory to a local CSV file and return a summary plus the file path instead of the full list inline. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
|
|
2951
3332
|
inputSchema: MapSiteUrlsInputSchema,
|
|
2952
3333
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
2953
3334
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
2954
3335
|
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
2955
3336
|
server.registerTool("extract_site", {
|
|
2956
3337
|
title: "Multi-Page Site Extract",
|
|
2957
|
-
description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
3338
|
+
description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Supports up to maxPages=10000; bulk crawls over 25 pages switch to folder mode: each page is saved as its own Markdown file in a local folder and the response returns a summary plus the folder path instead of inlining all page content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
2958
3339
|
inputSchema: ExtractSiteInputSchema,
|
|
2959
3340
|
outputSchema: ExtractSiteOutputSchema,
|
|
2960
3341
|
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
@@ -3078,7 +3459,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
3078
3459
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
3079
3460
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
3080
3461
|
}, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
|
|
3081
|
-
server.registerTool("
|
|
3462
|
+
server.registerTool("rank_tracker_workflow", {
|
|
3082
3463
|
title: "Rank Tracker Blueprint Builder",
|
|
3083
3464
|
description: "Generate a build-ready database, cron/heartbeat, ingestion, metrics, and AI implementation prompt for a rank tracker powered by MCP Scraper. Supports Maps rankings through directory_workflow/maps_search, organic rankings through search_serp, AI Overview citation tracking, and People Also Ask source presence tracking. This tool is local planning only; it does not call the web or spend credits.",
|
|
3084
3465
|
inputSchema: RankTrackerBlueprintInputSchema,
|
|
@@ -3362,10 +3743,14 @@ var HttpMcpToolExecutor = class {
|
|
|
3362
3743
|
|
|
3363
3744
|
export {
|
|
3364
3745
|
harvestTimeoutBudget,
|
|
3746
|
+
buildLinkGraph,
|
|
3747
|
+
computeIssues,
|
|
3748
|
+
renderIssueReport,
|
|
3365
3749
|
configureReportSaving,
|
|
3366
3750
|
outputBaseDir,
|
|
3367
3751
|
formatCaptureSerpSnapshot,
|
|
3368
3752
|
formatCaptureSerpPageSnapshots,
|
|
3753
|
+
SERVER_INSTRUCTIONS,
|
|
3369
3754
|
CaptureSerpSnapshotOutputSchema,
|
|
3370
3755
|
CaptureSerpPageSnapshotsOutputSchema,
|
|
3371
3756
|
CaptureSerpSnapshotInputSchema,
|
|
@@ -3375,4 +3760,4 @@ export {
|
|
|
3375
3760
|
registerPaaExtractorMcpTools,
|
|
3376
3761
|
HttpMcpToolExecutor
|
|
3377
3762
|
};
|
|
3378
|
-
//# sourceMappingURL=chunk-
|
|
3763
|
+
//# sourceMappingURL=chunk-WQAIOY2D.js.map
|