mcp-scraper 0.3.15 → 0.3.16
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 +290 -23
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/browser-agent-stdio-server.cjs +1 -1
- 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 +253 -19
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -4
- package/dist/bin/mcp-scraper-install.cjs +3 -3
- 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 +251 -17
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/{chunk-WQAIOY2D.js → chunk-7XARJHS6.js} +254 -18
- package/dist/chunk-7XARJHS6.js.map +1 -0
- package/dist/{chunk-7Y6JDBML.js → chunk-ACIUCZ27.js} +3 -3
- package/dist/chunk-ACIUCZ27.js.map +1 -0
- package/dist/{chunk-WJ2LWHPA.js → chunk-ISZWGIWL.js} +2 -2
- package/dist/chunk-SOMBZK3V.js +7 -0
- package/dist/chunk-SOMBZK3V.js.map +1 -0
- package/dist/{server-UOQ2JIUQ.js → server-LPVBSE2E.js} +35 -8
- package/dist/server-LPVBSE2E.js.map +1 -0
- package/docs/mcp-tool-manifest.generated.json +45 -10
- package/package.json +1 -1
- package/dist/chunk-2MX5WOII.js +0 -7
- package/dist/chunk-2MX5WOII.js.map +0 -1
- package/dist/chunk-7Y6JDBML.js.map +0 -1
- package/dist/chunk-WQAIOY2D.js.map +0 -1
- package/dist/server-UOQ2JIUQ.js.map +0 -1
- /package/dist/{chunk-WJ2LWHPA.js.map → chunk-ISZWGIWL.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -9168,6 +9168,143 @@ var init_seo_issues = __esm({
|
|
|
9168
9168
|
}
|
|
9169
9169
|
});
|
|
9170
9170
|
|
|
9171
|
+
// src/api/image-audit.ts
|
|
9172
|
+
function formatBytes(n) {
|
|
9173
|
+
if (n == null) return null;
|
|
9174
|
+
if (n < 1024) return `${n} B`;
|
|
9175
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
9176
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
9177
|
+
}
|
|
9178
|
+
function collectUrls(pages) {
|
|
9179
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
9180
|
+
for (const p of pages) for (const raw of p.imageLinks || []) {
|
|
9181
|
+
const u = decodeEntities(raw);
|
|
9182
|
+
if (!/^https?:\/\//i.test(u)) continue;
|
|
9183
|
+
if (/[{}]|%7[bd]/i.test(u)) continue;
|
|
9184
|
+
const k = dedupKey(u);
|
|
9185
|
+
const prev = byKey.get(k);
|
|
9186
|
+
if (!prev || /^https:/i.test(u) && /^http:/i.test(prev)) byKey.set(k, u);
|
|
9187
|
+
}
|
|
9188
|
+
return [...byKey.values()];
|
|
9189
|
+
}
|
|
9190
|
+
async function sizeAndType(url, timeoutMs) {
|
|
9191
|
+
const ctrl = new AbortController();
|
|
9192
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
9193
|
+
const read = (res) => ({
|
|
9194
|
+
len: res.headers.get("content-length"),
|
|
9195
|
+
cr: res.headers.get("content-range"),
|
|
9196
|
+
ct: res.headers.get("content-type"),
|
|
9197
|
+
status: res.status
|
|
9198
|
+
});
|
|
9199
|
+
try {
|
|
9200
|
+
let bytes = null;
|
|
9201
|
+
let ct = null;
|
|
9202
|
+
let status = null;
|
|
9203
|
+
try {
|
|
9204
|
+
const h = read(await fetch(url, { method: "HEAD", redirect: "follow", signal: ctrl.signal }));
|
|
9205
|
+
status = h.status;
|
|
9206
|
+
ct = h.ct;
|
|
9207
|
+
if (h.len) bytes = Number(h.len);
|
|
9208
|
+
} catch {
|
|
9209
|
+
}
|
|
9210
|
+
if (bytes == null) {
|
|
9211
|
+
const g = await fetch(url, { method: "GET", headers: { Range: "bytes=0-0" }, redirect: "follow", signal: ctrl.signal });
|
|
9212
|
+
const r = read(g);
|
|
9213
|
+
status = status ?? r.status;
|
|
9214
|
+
ct = ct ?? r.ct;
|
|
9215
|
+
if (r.cr && r.cr.includes("/")) {
|
|
9216
|
+
const tail = r.cr.split("/")[1];
|
|
9217
|
+
if (tail && tail !== "*") bytes = Number(tail);
|
|
9218
|
+
} else if (r.len) bytes = Number(r.len);
|
|
9219
|
+
try {
|
|
9220
|
+
await g.body?.cancel();
|
|
9221
|
+
} catch {
|
|
9222
|
+
}
|
|
9223
|
+
}
|
|
9224
|
+
return { url, status, bytes: Number.isFinite(bytes) ? bytes : null, contentType: ct };
|
|
9225
|
+
} catch (e) {
|
|
9226
|
+
return { url, status: null, bytes: null, contentType: null, error: String(e.message || e) };
|
|
9227
|
+
} finally {
|
|
9228
|
+
clearTimeout(t);
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
async function pool(items, n, fn) {
|
|
9232
|
+
const out = new Array(items.length);
|
|
9233
|
+
let i = 0;
|
|
9234
|
+
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
9235
|
+
while (i < items.length) {
|
|
9236
|
+
const idx = i++;
|
|
9237
|
+
out[idx] = await fn(items[idx]);
|
|
9238
|
+
}
|
|
9239
|
+
}));
|
|
9240
|
+
return out;
|
|
9241
|
+
}
|
|
9242
|
+
async function auditImages(pages, opts = {}) {
|
|
9243
|
+
const concurrency = opts.concurrency ?? 12;
|
|
9244
|
+
const timeoutMs = opts.timeoutMs ?? 12e3;
|
|
9245
|
+
const urls = collectUrls(pages).slice(0, opts.max ?? 5e3);
|
|
9246
|
+
const heads = await pool(urls, concurrency, (u) => sizeAndType(u, timeoutMs));
|
|
9247
|
+
const rows = heads.map((r) => {
|
|
9248
|
+
const format = formatOf(r.contentType, r.url);
|
|
9249
|
+
return {
|
|
9250
|
+
...r,
|
|
9251
|
+
size: formatBytes(r.bytes),
|
|
9252
|
+
format,
|
|
9253
|
+
over100kb: r.bytes != null && r.bytes > OVER_BYTES,
|
|
9254
|
+
legacyFormat: format !== "unknown" && !MODERN.has(format)
|
|
9255
|
+
};
|
|
9256
|
+
});
|
|
9257
|
+
const sized = rows.filter((r) => r.bytes != null);
|
|
9258
|
+
const totalBytes = sized.reduce((a, r) => a + r.bytes, 0);
|
|
9259
|
+
const formatCounts = {};
|
|
9260
|
+
for (const r of rows) formatCounts[r.format] = (formatCounts[r.format] || 0) + 1;
|
|
9261
|
+
return {
|
|
9262
|
+
rows: rows.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0)),
|
|
9263
|
+
summary: {
|
|
9264
|
+
unique: rows.length,
|
|
9265
|
+
sized: sized.length,
|
|
9266
|
+
totalBytes,
|
|
9267
|
+
totalSize: formatBytes(totalBytes) ?? "0 B",
|
|
9268
|
+
avgSize: formatBytes(Math.round(totalBytes / (sized.length || 1))) ?? "0 B",
|
|
9269
|
+
over100kb: rows.filter((r) => r.over100kb).length,
|
|
9270
|
+
legacyFormat: rows.filter((r) => r.legacyFormat).length,
|
|
9271
|
+
formatCounts
|
|
9272
|
+
}
|
|
9273
|
+
};
|
|
9274
|
+
}
|
|
9275
|
+
function renderImageSection(audit) {
|
|
9276
|
+
const s = audit.summary;
|
|
9277
|
+
const fmt = Object.entries(s.formatCounts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(" ");
|
|
9278
|
+
const heaviest = audit.rows.filter((r) => r.over100kb).slice(0, 15);
|
|
9279
|
+
const lines = [
|
|
9280
|
+
`## Images`,
|
|
9281
|
+
`**${s.unique} unique images** \xB7 ${s.totalSize} total \xB7 ${s.avgSize} avg \xB7 ${s.over100kb} over 100 KB \xB7 ${s.legacyFormat} legacy format`,
|
|
9282
|
+
`Formats: ${fmt}`
|
|
9283
|
+
];
|
|
9284
|
+
if (heaviest.length) {
|
|
9285
|
+
lines.push("", "| Size | Format | URL |", "|------|--------|-----|");
|
|
9286
|
+
for (const r of heaviest) lines.push(`| ${r.size} | ${r.format} | ${r.url} |`);
|
|
9287
|
+
}
|
|
9288
|
+
return lines.join("\n");
|
|
9289
|
+
}
|
|
9290
|
+
var OVER_BYTES, MODERN, decodeEntities, dedupKey, formatOf;
|
|
9291
|
+
var init_image_audit = __esm({
|
|
9292
|
+
"src/api/image-audit.ts"() {
|
|
9293
|
+
"use strict";
|
|
9294
|
+
OVER_BYTES = 100 * 1024;
|
|
9295
|
+
MODERN = /* @__PURE__ */ new Set(["webp", "avif", "svg", "svg+xml"]);
|
|
9296
|
+
decodeEntities = (u) => u.replace(/&/g, "&").replace(/�?38;/g, "&").replace(/&/gi, "&").trim();
|
|
9297
|
+
dedupKey = (u) => u.replace(/^https?:\/\//i, "//").replace(/\/$/, "");
|
|
9298
|
+
formatOf = (ct, url) => {
|
|
9299
|
+
if (ct) {
|
|
9300
|
+
const m = ct.split(";")[0].trim().toLowerCase();
|
|
9301
|
+
if (m.startsWith("image/")) return m.replace("image/", "");
|
|
9302
|
+
}
|
|
9303
|
+
return (url.split("?")[0].match(/\.([a-z0-9]+)$/i)?.[1] || "unknown").toLowerCase();
|
|
9304
|
+
};
|
|
9305
|
+
}
|
|
9306
|
+
});
|
|
9307
|
+
|
|
9171
9308
|
// src/api/blob-store.ts
|
|
9172
9309
|
function byteLength(data) {
|
|
9173
9310
|
return Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
|
|
@@ -9582,10 +9719,13 @@ function chunk(items, size) {
|
|
|
9582
9719
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
9583
9720
|
return out;
|
|
9584
9721
|
}
|
|
9585
|
-
function buildArtifacts(job, pages, branding) {
|
|
9722
|
+
function buildArtifacts(job, pages, branding, imageAudit) {
|
|
9586
9723
|
const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
|
|
9587
9724
|
const issues = computeIssues(pages, metrics);
|
|
9588
|
-
|
|
9725
|
+
let reportMd = renderIssueReport(job.startUrl, pages, issues, metrics);
|
|
9726
|
+
if (imageAudit) reportMd += `
|
|
9727
|
+
|
|
9728
|
+
${renderImageSection(imageAudit)}`;
|
|
9589
9729
|
const pageRows = pages.map((p) => {
|
|
9590
9730
|
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
9591
9731
|
const m = metrics.get(p.url);
|
|
@@ -9599,6 +9739,10 @@ function buildArtifacts(job, pages, branding) {
|
|
|
9599
9739
|
{ name: "links.jsonl", body: jsonl(edges), type: "application/x-ndjson" },
|
|
9600
9740
|
{ name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" }
|
|
9601
9741
|
];
|
|
9742
|
+
if (imageAudit) {
|
|
9743
|
+
files.push({ name: "images.jsonl", body: jsonl(imageAudit.rows), type: "application/x-ndjson" });
|
|
9744
|
+
files.push({ name: "images-summary.json", body: JSON.stringify(imageAudit.summary, null, 2), type: "application/json" });
|
|
9745
|
+
}
|
|
9602
9746
|
if (branding) files.push({ name: "branding.json", body: JSON.stringify(branding, null, 2), type: "application/json" });
|
|
9603
9747
|
return files;
|
|
9604
9748
|
}
|
|
@@ -9610,6 +9754,7 @@ var init_site_extract = __esm({
|
|
|
9610
9754
|
init_site_extractor();
|
|
9611
9755
|
init_seo_link_graph();
|
|
9612
9756
|
init_seo_issues();
|
|
9757
|
+
init_image_audit();
|
|
9613
9758
|
init_blob_store();
|
|
9614
9759
|
init_screenshot();
|
|
9615
9760
|
init_browser_service_env();
|
|
@@ -9657,10 +9802,14 @@ var init_site_extract = __esm({
|
|
|
9657
9802
|
});
|
|
9658
9803
|
}
|
|
9659
9804
|
const branding = Array.isArray(job.options.formats) && job.options.formats.includes("branding") ? await step.run("branding", () => extractBranding(job.startUrl, key).catch(() => null)) : null;
|
|
9805
|
+
const imageAudit = Array.isArray(job.options.formats) && job.options.formats.includes("images") ? await step.run("image-audit", async () => {
|
|
9806
|
+
const pages = await getExtractedPages(jobId);
|
|
9807
|
+
return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
9808
|
+
}) : null;
|
|
9660
9809
|
const artifacts = await step.run("finalize", async () => {
|
|
9661
9810
|
const pages = await getExtractedPages(jobId);
|
|
9662
9811
|
const store = getBlobStore();
|
|
9663
|
-
const files = buildArtifacts(job, pages, branding);
|
|
9812
|
+
const files = buildArtifacts(job, pages, branding, imageAudit);
|
|
9664
9813
|
const stored = [];
|
|
9665
9814
|
for (const f of files) stored.push(await store.put(`extract/${jobId}/${f.name}`, f.body, f.type));
|
|
9666
9815
|
await completeExtractJob(jobId, stored);
|
|
@@ -9935,7 +10084,7 @@ var init_catalog_seed = __esm({
|
|
|
9935
10084
|
kind: "simple",
|
|
9936
10085
|
endpoint: "/extract-site",
|
|
9937
10086
|
mcpName: "extract_site",
|
|
9938
|
-
tagline: "Map + bulk scrape
|
|
10087
|
+
tagline: "Map + bulk scrape (content)",
|
|
9939
10088
|
params: [
|
|
9940
10089
|
URL_PARAM,
|
|
9941
10090
|
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100 },
|
|
@@ -9943,7 +10092,21 @@ var init_catalog_seed = __esm({
|
|
|
9943
10092
|
{ key: "rotateProxies", label: "Rotate proxies (blocked sites)", type: "boolean", default: true },
|
|
9944
10093
|
{ key: "background", label: "Run in background", type: "boolean", default: true, advanced: true }
|
|
9945
10094
|
],
|
|
9946
|
-
examples: ["https://steenshoney.com/ (full-site
|
|
10095
|
+
examples: ["https://steenshoney.com/ (full-site content crawl)"]
|
|
10096
|
+
},
|
|
10097
|
+
{
|
|
10098
|
+
key: "audit_site",
|
|
10099
|
+
label: "Audit",
|
|
10100
|
+
kind: "simple",
|
|
10101
|
+
endpoint: "/extract-site",
|
|
10102
|
+
mcpName: "audit_site",
|
|
10103
|
+
tagline: "Technical SEO audit (Screaming-Frog style)",
|
|
10104
|
+
params: [
|
|
10105
|
+
URL_PARAM,
|
|
10106
|
+
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100 },
|
|
10107
|
+
{ key: "rotateProxies", label: "Rotate proxies (blocked sites)", type: "boolean", default: true }
|
|
10108
|
+
],
|
|
10109
|
+
examples: ["https://steenshoney.com/ (issues + link graph + image audit)"]
|
|
9947
10110
|
}
|
|
9948
10111
|
]
|
|
9949
10112
|
},
|
|
@@ -16037,7 +16200,7 @@ function saveFullReport(full) {
|
|
|
16037
16200
|
function reportSavingActive() {
|
|
16038
16201
|
return reportSavingEnabled && process.env.MCP_SCRAPER_SAVE_REPORTS !== "false";
|
|
16039
16202
|
}
|
|
16040
|
-
function saveBulkSite(siteUrl, pages, seo) {
|
|
16203
|
+
function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
16041
16204
|
if (!reportSavingActive()) return null;
|
|
16042
16205
|
try {
|
|
16043
16206
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -16060,15 +16223,32 @@ function saveBulkSite(siteUrl, pages, seo) {
|
|
|
16060
16223
|
(0, import_node_fs5.writeFileSync)((0, import_node_path7.join)(pagesDir, fname), content, "utf8");
|
|
16061
16224
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
16062
16225
|
});
|
|
16226
|
+
const dataFilesSection = seo ? [
|
|
16227
|
+
`
|
|
16228
|
+
## Data files (in this folder)`,
|
|
16229
|
+
`- \`report.md\` \u2014 SEO issues summary (counts + example URLs)`,
|
|
16230
|
+
`- \`issues.json\` \u2014 every finding with the full list of offender URLs`,
|
|
16231
|
+
`- \`pages.jsonl\` \u2014 per-page SEO fields, one JSON object per page`,
|
|
16232
|
+
`- \`links.jsonl\` \u2014 internal/external link edges (anchor, rel, target status)`,
|
|
16233
|
+
`- \`link-metrics.jsonl\` \u2014 inlinks, crawl depth, orphan flags per page`,
|
|
16234
|
+
...imageAudit ? [
|
|
16235
|
+
`- \`images.jsonl\` \u2014 every image with byte size, format, over-100KB and legacy-format flags`,
|
|
16236
|
+
`- \`images-summary.json\` \u2014 image totals (count, total weight, over-100KB, legacy)`
|
|
16237
|
+
] : [],
|
|
16238
|
+
...seo.branding ? [`- \`branding.json\` \u2014 site logo, colors, fonts`] : []
|
|
16239
|
+
].join("\n") : "";
|
|
16063
16240
|
const index = [
|
|
16064
16241
|
`# Site Extract: ${siteUrl}`,
|
|
16065
|
-
`**${pages.length} pages**
|
|
16242
|
+
`**${pages.length} pages** crawled. Everything is in this folder.`,
|
|
16243
|
+
`- Page content: one Markdown file per page under \`pages/\` (listed in the table below).`,
|
|
16244
|
+
seo ? `- SEO crawl data: the data files listed below.` : "",
|
|
16245
|
+
dataFilesSection,
|
|
16066
16246
|
`
|
|
16067
16247
|
## Pages
|
|
16068
16248
|
| # | Title | URL | File |
|
|
16069
16249
|
|---|-------|-----|------|
|
|
16070
16250
|
${indexRows.join("\n")}`
|
|
16071
|
-
].join("\n");
|
|
16251
|
+
].filter(Boolean).join("\n");
|
|
16072
16252
|
const indexFile = (0, import_node_path7.join)(dir, "index.md");
|
|
16073
16253
|
(0, import_node_fs5.writeFileSync)(indexFile, index, "utf8");
|
|
16074
16254
|
let seoFiles;
|
|
@@ -16083,8 +16263,17 @@ ${indexRows.join("\n")}`
|
|
|
16083
16263
|
(0, import_node_fs5.writeFileSync)(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
16084
16264
|
(0, import_node_fs5.writeFileSync)(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
16085
16265
|
(0, import_node_fs5.writeFileSync)(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
16086
|
-
(0, import_node_fs5.writeFileSync)(reportFile, seo.reportMd
|
|
16266
|
+
(0, import_node_fs5.writeFileSync)(reportFile, seo.reportMd + (imageAudit ? `
|
|
16267
|
+
|
|
16268
|
+
${renderImageSection(imageAudit)}` : ""), "utf8");
|
|
16087
16269
|
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
|
|
16270
|
+
if (imageAudit) {
|
|
16271
|
+
const imagesJsonl = (0, import_node_path7.join)(dir, "images.jsonl");
|
|
16272
|
+
const imagesSummary = (0, import_node_path7.join)(dir, "images-summary.json");
|
|
16273
|
+
(0, import_node_fs5.writeFileSync)(imagesJsonl, imageAudit.rows.map((r) => JSON.stringify(r)).join("\n"), "utf8");
|
|
16274
|
+
(0, import_node_fs5.writeFileSync)(imagesSummary, JSON.stringify(imageAudit.summary, null, 2), "utf8");
|
|
16275
|
+
seoFiles.push(imagesJsonl, imagesSummary);
|
|
16276
|
+
}
|
|
16088
16277
|
if (seo.branding) {
|
|
16089
16278
|
const brandingFile = (0, import_node_path7.join)(dir, "branding.json");
|
|
16090
16279
|
(0, import_node_fs5.writeFileSync)(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
@@ -16559,24 +16748,23 @@ function formatExtractSite(raw, input) {
|
|
|
16559
16748
|
durationMs: d.durationMs ?? 0
|
|
16560
16749
|
};
|
|
16561
16750
|
if (pages.length > BULK_PAGE_THRESHOLD) {
|
|
16562
|
-
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
16563
16751
|
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
16564
16752
|
url: String(p.url ?? ""),
|
|
16565
16753
|
title: p.title ?? null,
|
|
16566
16754
|
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
16567
16755
|
metaDescription: p.metaDescription ?? null,
|
|
16568
16756
|
schemaTypes: schemaTypesOf(p)
|
|
16569
|
-
}))
|
|
16757
|
+
})));
|
|
16570
16758
|
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
16571
|
-
const seoLine = bulk?.seoFiles?.length ? `
|
|
16572
|
-
- **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)` : "";
|
|
16573
16759
|
const location2 = bulk ? `
|
|
16574
16760
|
## \u{1F4C1} Bulk scrape saved
|
|
16575
|
-
- **Folder:** \`${bulk.dir}\`
|
|
16576
|
-
- **
|
|
16577
|
-
- **
|
|
16761
|
+
- **Folder:** \`${bulk.dir}\` \u2190 all scraped page content is here
|
|
16762
|
+
- **Start here:** \`${bulk.indexFile}\` \u2014 lists every page and its file
|
|
16763
|
+
- **Page content:** ${bulk.fileCount} Markdown files under \`pages/\` (one per page)
|
|
16764
|
+
|
|
16765
|
+
The page content is NOT inlined here to keep the context window clear \u2014 it is saved to disk. Open \`index.md\`, then open the page file you need from \`pages/\`.
|
|
16578
16766
|
|
|
16579
|
-
|
|
16767
|
+
\u{1F4A1} Want the full technical SEO audit (headings, link graph, indexability, image sizes/formats, issues)? Run \`audit_site\` on this URL instead.` : `
|
|
16580
16768
|
## \u26A0\uFE0F Bulk scrape not saved
|
|
16581
16769
|
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.`;
|
|
16582
16770
|
const full2 = [
|
|
@@ -16620,6 +16808,55 @@ ${body}` : "_(no content extracted)_"
|
|
|
16620
16808
|
${pageDetails}${tips}`;
|
|
16621
16809
|
return { ...oneBlock(full, diskReport), structuredContent };
|
|
16622
16810
|
}
|
|
16811
|
+
async function formatAuditSite(raw, input) {
|
|
16812
|
+
const parsed = parseData(raw);
|
|
16813
|
+
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
16814
|
+
const d = parsed.data;
|
|
16815
|
+
const pages = d.pages ?? [];
|
|
16816
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
16817
|
+
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
16818
|
+
const imageAudit = await auditImages(pages, { concurrency: 12 });
|
|
16819
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
16820
|
+
url: String(p.url ?? ""),
|
|
16821
|
+
title: p.title ?? null,
|
|
16822
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
16823
|
+
metaDescription: p.metaDescription ?? null,
|
|
16824
|
+
schemaTypes: schemaTypesOf(p)
|
|
16825
|
+
})), seo, imageAudit);
|
|
16826
|
+
const topIssues = Object.entries(seo.issues).sort((a, b) => b[1].count - a[1].count).slice(0, 8).map(([k, v]) => `- \`${k}\` \u2014 ${v.count}`).join("\n");
|
|
16827
|
+
const img = imageAudit.summary;
|
|
16828
|
+
const imgLine = `${img.unique} images \xB7 ${img.totalSize} total \xB7 ${img.over100kb} over 100 KB \xB7 ${img.legacyFormat} legacy format`;
|
|
16829
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
16830
|
+
const structuredContent = {
|
|
16831
|
+
url: input.url,
|
|
16832
|
+
pageCount: pages.length,
|
|
16833
|
+
durationMs: d.durationMs ?? 0,
|
|
16834
|
+
bulkFolder: bulk?.dir ?? null,
|
|
16835
|
+
issues: Object.fromEntries(Object.entries(seo.issues).map(([k, v]) => [k, v.count])),
|
|
16836
|
+
images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat }
|
|
16837
|
+
};
|
|
16838
|
+
const location2 = bulk ? `
|
|
16839
|
+
## \u{1F4C1} Technical audit saved
|
|
16840
|
+
- **Folder:** \`${bulk.dir}\` \u2190 full audit + page content here
|
|
16841
|
+
- **Start here:** \`${bulk.indexFile}\` \u2014 maps every file in the folder
|
|
16842
|
+
- **Data:** \`report.md\` (issues + image audit), \`issues.json\`, \`pages.jsonl\` (per-page fields), \`links.jsonl\`, \`link-metrics.jsonl\`, \`images.jsonl\`
|
|
16843
|
+
|
|
16844
|
+
Detail is saved to disk, not inlined. Open \`report.md\` for the audit, \`pages.jsonl\` for per-page fields (headings, canonical, indexability), \`images.jsonl\` for image sizes/formats.` : `
|
|
16845
|
+
## \u26A0\uFE0F Audit not saved
|
|
16846
|
+
Report saving is disabled in this environment. Enable \`MCP_SCRAPER_SAVE_REPORTS\` to capture the technical audit.`;
|
|
16847
|
+
const full = [
|
|
16848
|
+
`# Technical SEO Audit: ${input.url}`,
|
|
16849
|
+
durationLine,
|
|
16850
|
+
location2,
|
|
16851
|
+
`
|
|
16852
|
+
## Top issues
|
|
16853
|
+
${topIssues || "_none found_"}`,
|
|
16854
|
+
`
|
|
16855
|
+
## Images
|
|
16856
|
+
${imgLine}`
|
|
16857
|
+
].join("\n");
|
|
16858
|
+
return { content: [{ type: "text", text: full }], structuredContent };
|
|
16859
|
+
}
|
|
16623
16860
|
function formatYoutubeHarvest(raw, input) {
|
|
16624
16861
|
const parsed = parseData(raw);
|
|
16625
16862
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
@@ -17805,6 +18042,7 @@ var init_mcp_response_formatter = __esm({
|
|
|
17805
18042
|
init_workflow_catalog();
|
|
17806
18043
|
init_seo_link_graph();
|
|
17807
18044
|
init_seo_issues();
|
|
18045
|
+
init_image_audit();
|
|
17808
18046
|
reportSavingEnabled = true;
|
|
17809
18047
|
BULK_PAGE_THRESHOLD = 25;
|
|
17810
18048
|
BULK_URL_THRESHOLD = 500;
|
|
@@ -23339,7 +23577,7 @@ var PACKAGE_VERSION;
|
|
|
23339
23577
|
var init_version = __esm({
|
|
23340
23578
|
"src/version.ts"() {
|
|
23341
23579
|
"use strict";
|
|
23342
|
-
PACKAGE_VERSION = "0.3.
|
|
23580
|
+
PACKAGE_VERSION = "0.3.16";
|
|
23343
23581
|
}
|
|
23344
23582
|
});
|
|
23345
23583
|
|
|
@@ -23377,7 +23615,7 @@ NOTES
|
|
|
23377
23615
|
});
|
|
23378
23616
|
|
|
23379
23617
|
// src/mcp/mcp-tool-schemas.ts
|
|
23380
|
-
var import_zod27, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
|
|
23618
|
+
var import_zod27, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
|
|
23381
23619
|
var init_mcp_tool_schemas = __esm({
|
|
23382
23620
|
"src/mcp/mcp-tool-schemas.ts"() {
|
|
23383
23621
|
"use strict";
|
|
@@ -23408,12 +23646,18 @@ var init_mcp_tool_schemas = __esm({
|
|
|
23408
23646
|
maxUrls: import_zod27.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.")
|
|
23409
23647
|
};
|
|
23410
23648
|
ExtractSiteInputSchema = {
|
|
23411
|
-
url: import_zod27.z.string().url().describe("Public website URL or domain to
|
|
23649
|
+
url: import_zod27.z.string().url().describe("Public website URL or domain to crawl for page CONTENT across multiple pages (map + scrape). Use when the user wants the content/text/markdown of a site's pages. For a technical SEO audit (issues, link graph, indexability, headings, image weights) use audit_site instead \u2014 extract_site returns content only, not analysis."),
|
|
23412
23650
|
maxPages: import_zod27.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."),
|
|
23413
23651
|
rotateProxies: import_zod27.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."),
|
|
23414
23652
|
rotateProxyEvery: import_zod27.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."),
|
|
23415
23653
|
formats: import_zod27.z.array(import_zod27.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.")
|
|
23416
23654
|
};
|
|
23655
|
+
AuditSiteInputSchema = {
|
|
23656
|
+
url: import_zod27.z.string().url().describe("Public website URL or domain to run a full technical SEO audit on. Use when the user asks for a technical audit, SEO audit, site health check, or a Screaming-Frog-style crawl \u2014 i.e. they want ANALYSIS (issues, internal link graph, indexability, heading breakdown, image sizes/formats), not just page content. For plain content scraping use extract_site instead."),
|
|
23657
|
+
maxPages: import_zod27.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to crawl and audit. Use 50 for a normal audit, up to 10000 for a full-site audit. The audit always writes a folder of analysis files (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, images.jsonl) plus per-page content, and returns a headline summary plus the folder path."),
|
|
23658
|
+
rotateProxies: import_zod27.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks (403/429). Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
|
|
23659
|
+
rotateProxyEvery: import_zod27.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.")
|
|
23660
|
+
};
|
|
23417
23661
|
YoutubeHarvestInputSchema = {
|
|
23418
23662
|
mode: import_zod27.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
23419
23663
|
query: import_zod27.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
|
|
@@ -23714,6 +23958,19 @@ var init_mcp_tool_schemas = __esm({
|
|
|
23714
23958
|
})),
|
|
23715
23959
|
durationMs: import_zod27.z.number().min(0)
|
|
23716
23960
|
};
|
|
23961
|
+
AuditSiteOutputSchema = {
|
|
23962
|
+
url: import_zod27.z.string(),
|
|
23963
|
+
pageCount: import_zod27.z.number().int().min(0),
|
|
23964
|
+
durationMs: import_zod27.z.number().min(0),
|
|
23965
|
+
bulkFolder: import_zod27.z.string().nullable(),
|
|
23966
|
+
issues: import_zod27.z.record(import_zod27.z.string(), import_zod27.z.number()),
|
|
23967
|
+
images: import_zod27.z.object({
|
|
23968
|
+
unique: import_zod27.z.number().int().min(0),
|
|
23969
|
+
totalBytes: import_zod27.z.number().min(0),
|
|
23970
|
+
over100kb: import_zod27.z.number().int().min(0),
|
|
23971
|
+
legacyFormat: import_zod27.z.number().int().min(0)
|
|
23972
|
+
})
|
|
23973
|
+
};
|
|
23717
23974
|
MapsPlaceIntelOutputSchema = {
|
|
23718
23975
|
name: import_zod27.z.string(),
|
|
23719
23976
|
rating: NullableString,
|
|
@@ -24563,12 +24820,19 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
24563
24820
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
24564
24821
|
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
24565
24822
|
server.registerTool("extract_site", {
|
|
24566
|
-
title: "Multi-Page Site
|
|
24567
|
-
description: withReportNote("
|
|
24823
|
+
title: "Multi-Page Site Content Crawl",
|
|
24824
|
+
description: withReportNote("Crawl a public website and return the page CONTENT across multiple pages (map + scrape). Returns per-page titles and Markdown 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 content. This tool returns content only \u2014 for a technical SEO audit (issues, internal link graph, indexability, heading breakdown, image sizes/formats) use audit_site. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
24568
24825
|
inputSchema: ExtractSiteInputSchema,
|
|
24569
24826
|
outputSchema: ExtractSiteOutputSchema,
|
|
24570
|
-
annotations: liveWebToolAnnotations("Multi-Page Site
|
|
24827
|
+
annotations: liveWebToolAnnotations("Multi-Page Site Content Crawl")
|
|
24571
24828
|
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
24829
|
+
server.registerTool("audit_site", {
|
|
24830
|
+
title: "Technical SEO Audit",
|
|
24831
|
+
description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph with inlinks/orphans/crawl-depth, an issues report, and an image audit (byte size, format, over-100KB and legacy-format flags). Writes everything to a local folder (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, images.jsonl) plus per-page content, and returns a headline summary plus the folder path. Use this when the user wants an audit or analysis; use extract_site when they just want page content."),
|
|
24832
|
+
inputSchema: AuditSiteInputSchema,
|
|
24833
|
+
outputSchema: AuditSiteOutputSchema,
|
|
24834
|
+
annotations: liveWebToolAnnotations("Technical SEO Audit")
|
|
24835
|
+
}, async (input) => formatAuditSite(await executor.auditSite(input), input));
|
|
24572
24836
|
server.registerTool("youtube_harvest", {
|
|
24573
24837
|
title: "YouTube Video Harvest",
|
|
24574
24838
|
description: withReportNote('Harvest YouTube video metadata when the user wants to find videos by topic, inspect a channel library, compare video angles, or get videoIds for later transcription. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, durations, URLs, and videoIds for follow-up transcription. Use youtube_transcribe after selecting one video.'),
|
|
@@ -24906,6 +25170,9 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
24906
25170
|
extractSite(input) {
|
|
24907
25171
|
return this.call("/extract-site", input);
|
|
24908
25172
|
}
|
|
25173
|
+
auditSite(input) {
|
|
25174
|
+
return this.call("/extract-site", input);
|
|
25175
|
+
}
|
|
24909
25176
|
youtubeHarvest(input) {
|
|
24910
25177
|
return this.call("/youtube/harvest", input);
|
|
24911
25178
|
}
|