mcp-scraper 0.3.16 → 0.3.17

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 (32) hide show
  1. package/dist/bin/api-server.cjs +133 -5
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +1 -1
  4. package/dist/bin/browser-agent-stdio-server.cjs +1 -1
  5. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  6. package/dist/bin/browser-agent-stdio-server.js +2 -2
  7. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  8. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.js +1 -1
  10. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +121 -4
  11. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -3
  13. package/dist/bin/mcp-scraper-install.cjs +1 -1
  14. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  15. package/dist/bin/mcp-scraper-install.js +1 -1
  16. package/dist/bin/mcp-stdio-server.cjs +121 -4
  17. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  18. package/dist/bin/mcp-stdio-server.js +2 -2
  19. package/dist/{chunk-7XARJHS6.js → chunk-N33YEY6W.js} +124 -5
  20. package/dist/chunk-N33YEY6W.js.map +1 -0
  21. package/dist/chunk-PVWWY5NV.js +7 -0
  22. package/dist/chunk-PVWWY5NV.js.map +1 -0
  23. package/dist/{chunk-ISZWGIWL.js → chunk-UFD7N36R.js} +2 -2
  24. package/dist/{server-LPVBSE2E.js → server-TKWY47JA.js} +11 -5
  25. package/dist/server-TKWY47JA.js.map +1 -0
  26. package/docs/mcp-tool-manifest.generated.json +1 -1
  27. package/package.json +1 -1
  28. package/dist/chunk-7XARJHS6.js.map +0 -1
  29. package/dist/chunk-SOMBZK3V.js +0 -7
  30. package/dist/chunk-SOMBZK3V.js.map +0 -1
  31. package/dist/server-LPVBSE2E.js.map +0 -1
  32. /package/dist/{chunk-ISZWGIWL.js.map → chunk-UFD7N36R.js.map} +0 -0
@@ -9054,6 +9054,104 @@ var init_seo_link_graph = __esm({
9054
9054
  }
9055
9055
  });
9056
9056
 
9057
+ // src/api/seo-link-report.ts
9058
+ function registrableDomain(host) {
9059
+ const h = host.replace(/^www\./, "").toLowerCase();
9060
+ const parts = h.split(".");
9061
+ return parts.length <= 2 ? h : parts.slice(-2).join(".");
9062
+ }
9063
+ function buildLinkReport(edges, metrics, siteUrl) {
9064
+ let siteReg = "";
9065
+ try {
9066
+ siteReg = registrableDomain(new URL(siteUrl).hostname);
9067
+ } catch {
9068
+ siteReg = "";
9069
+ }
9070
+ const internalEdges = edges.filter((e) => e.internal);
9071
+ const pages = metrics.length;
9072
+ const orphans = metrics.filter((m) => m.orphan).length;
9073
+ const brokenInternal = internalEdges.filter((e) => e.targetStatus != null && e.targetStatus >= 400).length;
9074
+ const sumInlinks = metrics.reduce((a, m) => a + m.inlinks, 0);
9075
+ const sumOutlinks = metrics.reduce((a, m) => a + m.outlinksInternal + m.outlinksExternal, 0);
9076
+ const distribution = { zero: 0, oneToTwo: 0, threeToTen: 0, elevenPlus: 0 };
9077
+ for (const m of metrics) {
9078
+ if (m.inlinks === 0) distribution.zero++;
9079
+ else if (m.inlinks <= 2) distribution.oneToTwo++;
9080
+ else if (m.inlinks <= 10) distribution.threeToTen++;
9081
+ else distribution.elevenPlus++;
9082
+ }
9083
+ const topByInlinks = [...metrics].sort((a, b) => b.inlinks - a.inlinks).slice(0, 20).map((m) => ({ url: m.url, inlinks: m.inlinks, outlinksInternal: m.outlinksInternal, outlinksExternal: m.outlinksExternal }));
9084
+ const domMap = /* @__PURE__ */ new Map();
9085
+ let externalTotal = 0;
9086
+ for (const e of edges) {
9087
+ let domain;
9088
+ try {
9089
+ domain = registrableDomain(new URL(e.to).hostname);
9090
+ } catch {
9091
+ continue;
9092
+ }
9093
+ if (!domain || domain === siteReg) continue;
9094
+ externalTotal++;
9095
+ const d = domMap.get(domain) ?? { links: 0, nofollow: 0, pages: /* @__PURE__ */ new Set() };
9096
+ d.links++;
9097
+ if (e.nofollow) d.nofollow++;
9098
+ d.pages.add(e.from);
9099
+ domMap.set(domain, d);
9100
+ }
9101
+ const externalDomains = [...domMap.entries()].map(([domain, d]) => ({ domain, links: d.links, nofollow: d.nofollow, pages: d.pages.size })).sort((a, b) => b.links - a.links);
9102
+ const round = (n) => Math.round(n * 10) / 10;
9103
+ return {
9104
+ summary: {
9105
+ internal: {
9106
+ totalLinks: internalEdges.length,
9107
+ pages,
9108
+ orphans,
9109
+ brokenInternal,
9110
+ avgInlinks: pages ? round(sumInlinks / pages) : 0,
9111
+ avgOutlinks: pages ? round(sumOutlinks / pages) : 0,
9112
+ distribution,
9113
+ topByInlinks
9114
+ },
9115
+ external: {
9116
+ totalLinks: externalTotal,
9117
+ uniqueDomains: externalDomains.length,
9118
+ topDomains: externalDomains.slice(0, 20)
9119
+ }
9120
+ },
9121
+ externalDomains
9122
+ };
9123
+ }
9124
+ function renderLinkReport(r) {
9125
+ const s = r.summary;
9126
+ const lines = [
9127
+ `## Link analysis`,
9128
+ `**Internal:** ${s.internal.totalLinks} links \xB7 ${s.internal.pages} pages \xB7 avg ${s.internal.avgInlinks} inlinks/page \xB7 ${s.internal.orphans} orphans \xB7 ${s.internal.brokenInternal} broken`,
9129
+ `**External:** ${s.external.totalLinks} links to ${s.external.uniqueDomains} domains`,
9130
+ ``,
9131
+ `### Inlink distribution`,
9132
+ `- 0 inlinks (orphans): ${s.internal.distribution.zero}`,
9133
+ `- 1\u20132: ${s.internal.distribution.oneToTwo}`,
9134
+ `- 3\u201310: ${s.internal.distribution.threeToTen}`,
9135
+ `- 11+: ${s.internal.distribution.elevenPlus}`,
9136
+ ``,
9137
+ `### Top 20 internal pages by inlinks`,
9138
+ `| inlinks | out (int/ext) | URL |`,
9139
+ `|---|---|---|`,
9140
+ ...s.internal.topByInlinks.map((p) => `| ${p.inlinks} | ${p.outlinksInternal}/${p.outlinksExternal} | ${p.url} |`),
9141
+ ``,
9142
+ `### Top 20 external domains by links`,
9143
+ `| links | nofollow | from pages | domain |`,
9144
+ `|---|---|---|---|`,
9145
+ ...s.external.topDomains.map((d) => `| ${d.links} | ${d.nofollow} | ${d.pages} | ${d.domain} |`)
9146
+ ];
9147
+ return lines.join("\n");
9148
+ }
9149
+ var init_seo_link_report = __esm({
9150
+ "src/api/seo-link-report.ts"() {
9151
+ "use strict";
9152
+ }
9153
+ });
9154
+
9057
9155
  // src/api/seo-issues.ts
9058
9156
  function dupes(pages, key) {
9059
9157
  const groups = /* @__PURE__ */ new Map();
@@ -9722,6 +9820,7 @@ function chunk(items, size) {
9722
9820
  function buildArtifacts(job, pages, branding, imageAudit) {
9723
9821
  const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
9724
9822
  const issues = computeIssues(pages, metrics);
9823
+ const linkReport = buildLinkReport(edges, [...metrics.values()], job.startUrl);
9725
9824
  let reportMd = renderIssueReport(job.startUrl, pages, issues, metrics);
9726
9825
  if (imageAudit) reportMd += `
9727
9826
 
@@ -9737,7 +9836,10 @@ ${renderImageSection(imageAudit)}`;
9737
9836
  { name: "issues.json", body: JSON.stringify(issues, null, 2), type: "application/json" },
9738
9837
  { name: "pages.jsonl", body: jsonl(pageRows), type: "application/x-ndjson" },
9739
9838
  { name: "links.jsonl", body: jsonl(edges), type: "application/x-ndjson" },
9740
- { name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" }
9839
+ { name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" },
9840
+ { name: "link-report.md", body: renderLinkReport(linkReport), type: "text/markdown" },
9841
+ { name: "links-summary.json", body: JSON.stringify(linkReport.summary, null, 2), type: "application/json" },
9842
+ { name: "external-domains.json", body: JSON.stringify(linkReport.externalDomains, null, 2), type: "application/json" }
9741
9843
  ];
9742
9844
  if (imageAudit) {
9743
9845
  files.push({ name: "images.jsonl", body: jsonl(imageAudit.rows), type: "application/x-ndjson" });
@@ -9753,6 +9855,7 @@ var init_site_extract = __esm({
9753
9855
  init_client();
9754
9856
  init_site_extractor();
9755
9857
  init_seo_link_graph();
9858
+ init_seo_link_report();
9756
9859
  init_seo_issues();
9757
9860
  init_image_audit();
9758
9861
  init_blob_store();
@@ -16231,6 +16334,9 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
16231
16334
  `- \`pages.jsonl\` \u2014 per-page SEO fields, one JSON object per page`,
16232
16335
  `- \`links.jsonl\` \u2014 internal/external link edges (anchor, rel, target status)`,
16233
16336
  `- \`link-metrics.jsonl\` \u2014 inlinks, crawl depth, orphan flags per page`,
16337
+ `- \`link-report.md\` \u2014 link analysis: top internal pages by inlinks, top external domains, distribution`,
16338
+ `- \`links-summary.json\` \u2014 link totals + top-20 internal pages + top-20 external domains`,
16339
+ `- \`external-domains.json\` \u2014 every external domain linked to, ordered by link count`,
16234
16340
  ...imageAudit ? [
16235
16341
  `- \`images.jsonl\` \u2014 every image with byte size, format, over-100KB and legacy-format flags`,
16236
16342
  `- \`images-summary.json\` \u2014 image totals (count, total weight, over-100KB, legacy)`
@@ -16266,7 +16372,13 @@ ${indexRows.join("\n")}`
16266
16372
  (0, import_node_fs5.writeFileSync)(reportFile, seo.reportMd + (imageAudit ? `
16267
16373
 
16268
16374
  ${renderImageSection(imageAudit)}` : ""), "utf8");
16269
- seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
16375
+ const linkReportFile = (0, import_node_path7.join)(dir, "link-report.md");
16376
+ const linksSummaryFile = (0, import_node_path7.join)(dir, "links-summary.json");
16377
+ const externalDomainsFile = (0, import_node_path7.join)(dir, "external-domains.json");
16378
+ (0, import_node_fs5.writeFileSync)(linkReportFile, renderLinkReport(seo.linkReport), "utf8");
16379
+ (0, import_node_fs5.writeFileSync)(linksSummaryFile, JSON.stringify(seo.linkReport.summary, null, 2), "utf8");
16380
+ (0, import_node_fs5.writeFileSync)(externalDomainsFile, JSON.stringify(seo.linkReport.externalDomains, null, 2), "utf8");
16381
+ seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile, linkReportFile, linksSummaryFile, externalDomainsFile];
16270
16382
  if (imageAudit) {
16271
16383
  const imagesJsonl = (0, import_node_path7.join)(dir, "images.jsonl");
16272
16384
  const imagesSummary = (0, import_node_path7.join)(dir, "images-summary.json");
@@ -16718,6 +16830,7 @@ function buildSeoExport(siteUrl, pages, branding) {
16718
16830
  metrics: [...metrics.values()],
16719
16831
  issues,
16720
16832
  reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
16833
+ linkReport: buildLinkReport(edges, [...metrics.values()], siteUrl),
16721
16834
  branding: branding ?? void 0
16722
16835
  };
16723
16836
  }
@@ -16826,6 +16939,9 @@ async function formatAuditSite(raw, input) {
16826
16939
  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
16940
  const img = imageAudit.summary;
16828
16941
  const imgLine = `${img.unique} images \xB7 ${img.totalSize} total \xB7 ${img.over100kb} over 100 KB \xB7 ${img.legacyFormat} legacy format`;
16942
+ const lr = seo.linkReport.summary;
16943
+ const topExt = lr.external.topDomains.slice(0, 3).map((dm) => `${dm.domain} (${dm.links})`).join(", ") || "\u2014";
16944
+ const linkLine = `${lr.internal.totalLinks} internal / ${lr.external.totalLinks} external links \xB7 ${lr.internal.orphans} orphans \xB7 ${lr.internal.brokenInternal} broken \xB7 avg ${lr.internal.avgInlinks} inlinks/page \xB7 top external: ${topExt}`;
16829
16945
  const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
16830
16946
  const structuredContent = {
16831
16947
  url: input.url,
@@ -16833,7 +16949,8 @@ async function formatAuditSite(raw, input) {
16833
16949
  durationMs: d.durationMs ?? 0,
16834
16950
  bulkFolder: bulk?.dir ?? null,
16835
16951
  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 }
16952
+ images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat },
16953
+ links: { internal: lr.internal.totalLinks, external: lr.external.totalLinks, orphans: lr.internal.orphans, brokenInternal: lr.internal.brokenInternal, externalDomains: lr.external.uniqueDomains }
16837
16954
  };
16838
16955
  const location2 = bulk ? `
16839
16956
  ## \u{1F4C1} Technical audit saved
@@ -16852,6 +16969,9 @@ Report saving is disabled in this environment. Enable \`MCP_SCRAPER_SAVE_REPORTS
16852
16969
  ## Top issues
16853
16970
  ${topIssues || "_none found_"}`,
16854
16971
  `
16972
+ ## Links
16973
+ ${linkLine}`,
16974
+ `
16855
16975
  ## Images
16856
16976
  ${imgLine}`
16857
16977
  ].join("\n");
@@ -18041,6 +18161,7 @@ var init_mcp_response_formatter = __esm({
18041
18161
  init_errors();
18042
18162
  init_workflow_catalog();
18043
18163
  init_seo_link_graph();
18164
+ init_seo_link_report();
18044
18165
  init_seo_issues();
18045
18166
  init_image_audit();
18046
18167
  reportSavingEnabled = true;
@@ -23577,7 +23698,7 @@ var PACKAGE_VERSION;
23577
23698
  var init_version = __esm({
23578
23699
  "src/version.ts"() {
23579
23700
  "use strict";
23580
- PACKAGE_VERSION = "0.3.16";
23701
+ PACKAGE_VERSION = "0.3.17";
23581
23702
  }
23582
23703
  });
23583
23704
 
@@ -23969,6 +24090,13 @@ var init_mcp_tool_schemas = __esm({
23969
24090
  totalBytes: import_zod27.z.number().min(0),
23970
24091
  over100kb: import_zod27.z.number().int().min(0),
23971
24092
  legacyFormat: import_zod27.z.number().int().min(0)
24093
+ }),
24094
+ links: import_zod27.z.object({
24095
+ internal: import_zod27.z.number().int().min(0),
24096
+ external: import_zod27.z.number().int().min(0),
24097
+ orphans: import_zod27.z.number().int().min(0),
24098
+ brokenInternal: import_zod27.z.number().int().min(0),
24099
+ externalDomains: import_zod27.z.number().int().min(0)
23972
24100
  })
23973
24101
  };
23974
24102
  MapsPlaceIntelOutputSchema = {
@@ -24828,7 +24956,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
24828
24956
  }, async (input) => formatExtractSite(await executor.extractSite(input), input));
24829
24957
  server.registerTool("audit_site", {
24830
24958
  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."),
24959
+ 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 (inlinks/orphans/crawl-depth), a link analysis (top internal pages by inlinks, top external domains by link count, broken/orphan counts), 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, link-report.md, links-summary.json, external-domains.json, 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
24960
  inputSchema: AuditSiteInputSchema,
24833
24961
  outputSchema: AuditSiteOutputSchema,
24834
24962
  annotations: liveWebToolAnnotations("Technical SEO Audit")