mcp-scraper 0.3.14 → 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 +6 -6
- 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 +425 -44
- 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-AX7UBYLG.js → chunk-WJ2LWHPA.js} +7 -7
- package/dist/chunk-WJ2LWHPA.js.map +1 -0
- package/dist/{chunk-HM6FHV5U.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-ILIVSPNY.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-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-3QHZPR4U.js +0 -7
- package/dist/chunk-3QHZPR4U.js.map +0 -1
- package/dist/chunk-AX7UBYLG.js.map +0 -1
- package/dist/chunk-DBQDG7EH.js.map +0 -1
- package/dist/chunk-HM6FHV5U.js.map +0 -1
- package/dist/chunk-KPXMPAJ3.js.map +0 -1
- package/dist/server-ILIVSPNY.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
|
@@ -300,7 +300,7 @@ var import_node_fs2 = require("fs");
|
|
|
300
300
|
var import_node_path2 = require("path");
|
|
301
301
|
|
|
302
302
|
// src/version.ts
|
|
303
|
-
var PACKAGE_VERSION = "0.3.
|
|
303
|
+
var PACKAGE_VERSION = "0.3.15";
|
|
304
304
|
|
|
305
305
|
// src/mcp/mcp-response-formatter.ts
|
|
306
306
|
var import_node_fs = require("fs");
|
|
@@ -427,6 +427,191 @@ function suggestWorkflowRecipes(goal, limit = 3) {
|
|
|
427
427
|
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);
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
// src/api/seo-link-graph.ts
|
|
431
|
+
function normalize2(u) {
|
|
432
|
+
return u.split("#")[0].replace(/\/$/, "");
|
|
433
|
+
}
|
|
434
|
+
function buildLinkGraph(pages, startUrl) {
|
|
435
|
+
const statusByUrl = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const p of pages) statusByUrl.set(normalize2(p.url), p.status);
|
|
437
|
+
const edges = [];
|
|
438
|
+
for (const p of pages) {
|
|
439
|
+
for (const l of p.outlinks ?? []) {
|
|
440
|
+
const nofollow = (l.rel ?? "").split(/\s+/).includes("nofollow");
|
|
441
|
+
edges.push({
|
|
442
|
+
from: p.url,
|
|
443
|
+
to: l.href,
|
|
444
|
+
anchor: l.anchor,
|
|
445
|
+
rel: l.rel,
|
|
446
|
+
internal: l.internal,
|
|
447
|
+
nofollow,
|
|
448
|
+
targetStatus: statusByUrl.get(normalize2(l.href)) ?? null
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const inboundFrom = /* @__PURE__ */ new Map();
|
|
453
|
+
const inboundAnchors = /* @__PURE__ */ new Map();
|
|
454
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const e of edges) {
|
|
456
|
+
if (!e.internal) continue;
|
|
457
|
+
const to = normalize2(e.to);
|
|
458
|
+
const from = normalize2(e.from);
|
|
459
|
+
if (!inboundFrom.has(to)) inboundFrom.set(to, /* @__PURE__ */ new Set());
|
|
460
|
+
inboundFrom.get(to).add(from);
|
|
461
|
+
if (e.anchor) {
|
|
462
|
+
if (!inboundAnchors.has(to)) inboundAnchors.set(to, []);
|
|
463
|
+
inboundAnchors.get(to).push(e.anchor);
|
|
464
|
+
}
|
|
465
|
+
if (!e.nofollow && (statusByUrl.get(to) === 200 || !statusByUrl.has(to))) {
|
|
466
|
+
if (!adjacency.has(from)) adjacency.set(from, /* @__PURE__ */ new Set());
|
|
467
|
+
adjacency.get(from).add(to);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const depth = /* @__PURE__ */ new Map();
|
|
471
|
+
const start = normalize2(startUrl);
|
|
472
|
+
const queue = [start];
|
|
473
|
+
depth.set(start, 0);
|
|
474
|
+
while (queue.length > 0) {
|
|
475
|
+
const cur = queue.shift();
|
|
476
|
+
const d = depth.get(cur);
|
|
477
|
+
for (const next of adjacency.get(cur) ?? []) {
|
|
478
|
+
if (!depth.has(next)) {
|
|
479
|
+
depth.set(next, d + 1);
|
|
480
|
+
queue.push(next);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const topAnchorsFor = (url) => {
|
|
485
|
+
const counts = /* @__PURE__ */ new Map();
|
|
486
|
+
for (const a of inboundAnchors.get(url) ?? []) counts.set(a, (counts.get(a) ?? 0) + 1);
|
|
487
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map((e) => e[0]);
|
|
488
|
+
};
|
|
489
|
+
const metrics = /* @__PURE__ */ new Map();
|
|
490
|
+
for (const p of pages) {
|
|
491
|
+
const key = normalize2(p.url);
|
|
492
|
+
const inbound = inboundFrom.get(key) ?? /* @__PURE__ */ new Set();
|
|
493
|
+
metrics.set(p.url, {
|
|
494
|
+
url: p.url,
|
|
495
|
+
inlinks: inbound.size,
|
|
496
|
+
uniqueInlinks: inbound.size,
|
|
497
|
+
outlinksInternal: (p.outlinks ?? []).filter((l) => l.internal).length,
|
|
498
|
+
outlinksExternal: (p.outlinks ?? []).filter((l) => !l.internal).length,
|
|
499
|
+
crawlDepth: depth.has(key) ? depth.get(key) : null,
|
|
500
|
+
orphan: inbound.size === 0 && key !== start,
|
|
501
|
+
topAnchors: topAnchorsFor(key)
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return { edges, metrics };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/api/seo-issues.ts
|
|
508
|
+
var THIN_WORDS = 200;
|
|
509
|
+
var TITLE_MAX = 60;
|
|
510
|
+
var TITLE_PX_MAX = 561;
|
|
511
|
+
var TITLE_MIN = 30;
|
|
512
|
+
var META_MAX = 155;
|
|
513
|
+
var META_MIN = 70;
|
|
514
|
+
var H1_MAX = 70;
|
|
515
|
+
var URL_MAX = 115;
|
|
516
|
+
function dupes(pages, key) {
|
|
517
|
+
const groups = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const p of pages) {
|
|
519
|
+
const k = key(p);
|
|
520
|
+
if (k === null || k === void 0 || k === "") continue;
|
|
521
|
+
const ks = String(k);
|
|
522
|
+
if (!groups.has(ks)) groups.set(ks, []);
|
|
523
|
+
groups.get(ks).push(p.url);
|
|
524
|
+
}
|
|
525
|
+
return [...groups.values()].filter((g) => g.length > 1).flat();
|
|
526
|
+
}
|
|
527
|
+
function computeIssues(pages, metrics) {
|
|
528
|
+
const group = (urls) => ({ count: urls.length, urls: urls.slice(0, 200) });
|
|
529
|
+
const where = (fn) => pages.filter(fn).map((p) => p.url);
|
|
530
|
+
const ok = (p) => p.status === 200;
|
|
531
|
+
const pathname = (u) => {
|
|
532
|
+
try {
|
|
533
|
+
return new URL(u).pathname;
|
|
534
|
+
} catch {
|
|
535
|
+
return "";
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const report = {
|
|
539
|
+
"title.missing": group(where((p) => ok(p) && !p.title)),
|
|
540
|
+
"title.duplicate": group(dupes(pages.filter(ok), (p) => p.title)),
|
|
541
|
+
"title.tooLong": group(where((p) => (p.titleLength ?? 0) > TITLE_MAX || (p.titlePixels ?? 0) > TITLE_PX_MAX)),
|
|
542
|
+
"title.tooShort": group(where((p) => p.title != null && (p.titleLength ?? 0) < TITLE_MIN)),
|
|
543
|
+
"title.sameAsH1": group(where((p) => !!p.title && !!p.h1 && p.title.trim() === p.h1.trim())),
|
|
544
|
+
"meta.missing": group(where((p) => ok(p) && !p.metaDescription)),
|
|
545
|
+
"meta.duplicate": group(dupes(pages.filter(ok), (p) => p.metaDescription)),
|
|
546
|
+
"meta.tooLong": group(where((p) => (p.metaDescLength ?? 0) > META_MAX)),
|
|
547
|
+
"meta.tooShort": group(where((p) => p.metaDescription != null && (p.metaDescLength ?? 0) < META_MIN)),
|
|
548
|
+
"h1.missing": group(where((p) => ok(p) && !p.h1)),
|
|
549
|
+
"h1.multiple": group(where((p) => !!p.h1_2)),
|
|
550
|
+
"h1.duplicate": group(dupes(pages.filter(ok), (p) => p.h1)),
|
|
551
|
+
"h1.tooLong": group(where((p) => (p.h1?.length ?? 0) > H1_MAX)),
|
|
552
|
+
"h2.missing": group(where((p) => ok(p) && p.h2Count === 0)),
|
|
553
|
+
"indexability.nonIndexable": group(where((p) => !p.indexable)),
|
|
554
|
+
"indexability.noindex": group(where((p) => p.indexabilityReason === "noindex")),
|
|
555
|
+
"canonical.missing": group(where((p) => ok(p) && !p.canonicalUrl)),
|
|
556
|
+
"canonical.canonicalised": group(where((p) => p.indexabilityReason === "canonicalised")),
|
|
557
|
+
"response.broken4xx": group(where((p) => (p.status ?? 0) >= 400 && (p.status ?? 0) < 500)),
|
|
558
|
+
"response.error5xx": group(where((p) => (p.status ?? 0) >= 500)),
|
|
559
|
+
"response.redirect3xx": group(where((p) => (p.status ?? 0) >= 300 && (p.status ?? 0) < 400)),
|
|
560
|
+
"response.noResponse": group(where((p) => p.status === null)),
|
|
561
|
+
"content.thin": group(where((p) => ok(p) && p.wordCount > 0 && p.wordCount < THIN_WORDS)),
|
|
562
|
+
"content.exactDuplicate": group(dupes(pages.filter((p) => ok(p) && p.contentHash), (p) => p.contentHash)),
|
|
563
|
+
"images.missingAlt": group(where((p) => p.imagesMissingAlt > 0)),
|
|
564
|
+
"schema.missing": group(where((p) => ok(p) && p.schemaTypes.length === 0)),
|
|
565
|
+
"url.tooLong": group(where((p) => p.url.length > URL_MAX)),
|
|
566
|
+
"url.uppercase": group(where((p) => /[A-Z]/.test(pathname(p.url)))),
|
|
567
|
+
"url.underscores": group(where((p) => pathname(p.url).includes("_"))),
|
|
568
|
+
"links.orphan": group([...metrics.values()].filter((m) => m.orphan).map((m) => m.url))
|
|
569
|
+
};
|
|
570
|
+
const normUrl = (u) => {
|
|
571
|
+
try {
|
|
572
|
+
const x = new URL(u);
|
|
573
|
+
x.hash = "";
|
|
574
|
+
return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
|
|
575
|
+
} catch {
|
|
576
|
+
return u.replace(/#.*$/, "").replace(/\/+$/, "");
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const statusByUrl = new Map(pages.map((p) => [normUrl(p.url), p.status]));
|
|
580
|
+
const brokenLinkPages = /* @__PURE__ */ new Set();
|
|
581
|
+
for (const p of pages) {
|
|
582
|
+
for (const l of p.outlinks ?? []) {
|
|
583
|
+
if (!l.internal) continue;
|
|
584
|
+
const st = statusByUrl.get(normUrl(l.href));
|
|
585
|
+
if (st != null && st >= 400) brokenLinkPages.add(p.url);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
report["links.brokenInternal"] = group([...brokenLinkPages]);
|
|
589
|
+
return report;
|
|
590
|
+
}
|
|
591
|
+
function renderIssueReport(siteUrl, pages, report, metrics) {
|
|
592
|
+
const total = pages.length;
|
|
593
|
+
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}";
|
|
594
|
+
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" : ""} |`);
|
|
595
|
+
const depths = [...metrics.values()].map((m) => m.crawlDepth).filter((d) => d != null);
|
|
596
|
+
const maxDepth = depths.length ? Math.max(...depths) : 0;
|
|
597
|
+
const orphans = [...metrics.values()].filter((m) => m.orphan).length;
|
|
598
|
+
const topLinked = [...metrics.values()].sort((a, b) => b.inlinks - a.inlinks).slice(0, 5);
|
|
599
|
+
return [
|
|
600
|
+
`# SEO Crawl Report: ${siteUrl}`,
|
|
601
|
+
`**${total} pages crawled** \xB7 max crawl depth ${maxDepth} \xB7 ${orphans} orphan page(s)`,
|
|
602
|
+
`
|
|
603
|
+
## Issues
|
|
604
|
+
| | Issue | Count | Examples |
|
|
605
|
+
|---|-------|-------|----------|
|
|
606
|
+
${rows.join("\n") || "| \u2705 | none | 0 | \u2014 |"}`,
|
|
607
|
+
`
|
|
608
|
+
## Most-linked pages
|
|
609
|
+
${topLinked.map((m) => `- ${m.inlinks} inlinks \xB7 depth ${m.crawlDepth ?? "\u2014"} \xB7 ${m.url}`).join("\n")}`,
|
|
610
|
+
`
|
|
611
|
+
_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._`
|
|
612
|
+
].join("\n");
|
|
613
|
+
}
|
|
614
|
+
|
|
430
615
|
// src/mcp/mcp-response-formatter.ts
|
|
431
616
|
var reportSavingEnabled = true;
|
|
432
617
|
function sanitizeVendorText(text) {
|
|
@@ -457,6 +642,85 @@ function saveFullReport(full) {
|
|
|
457
642
|
return null;
|
|
458
643
|
}
|
|
459
644
|
}
|
|
645
|
+
var BULK_PAGE_THRESHOLD = 25;
|
|
646
|
+
var BULK_URL_THRESHOLD = 500;
|
|
647
|
+
function reportSavingActive() {
|
|
648
|
+
return reportSavingEnabled && process.env.MCP_SCRAPER_SAVE_REPORTS !== "false";
|
|
649
|
+
}
|
|
650
|
+
function saveBulkSite(siteUrl, pages, seo) {
|
|
651
|
+
if (!reportSavingActive()) return null;
|
|
652
|
+
try {
|
|
653
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
654
|
+
const dir = (0, import_node_path.join)(outputBaseDir(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
|
|
655
|
+
const pagesDir = (0, import_node_path.join)(dir, "pages");
|
|
656
|
+
(0, import_node_fs.mkdirSync)(pagesDir, { recursive: true });
|
|
657
|
+
const indexRows = pages.map((p, i) => {
|
|
658
|
+
const num = String(i + 1).padStart(4, "0");
|
|
659
|
+
const slug = slugifyReportName(p.url.replace(/^https?:\/\//, "")).slice(0, 60) || "page";
|
|
660
|
+
const fname = `${num}-${slug}.md`;
|
|
661
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
662
|
+
const content = [
|
|
663
|
+
`# ${p.title ?? "Untitled"}`,
|
|
664
|
+
`- **URL:** ${p.url}`,
|
|
665
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
666
|
+
p.schemaTypes?.length ? `- **Schema:** ${p.schemaTypes.join(", ")}` : "",
|
|
667
|
+
"",
|
|
668
|
+
body || "_(no content extracted)_"
|
|
669
|
+
].filter(Boolean).join("\n");
|
|
670
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(pagesDir, fname), content, "utf8");
|
|
671
|
+
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
672
|
+
});
|
|
673
|
+
const index = [
|
|
674
|
+
`# Site Extract: ${siteUrl}`,
|
|
675
|
+
`**${pages.length} pages** \u2014 each saved as an individual Markdown file under \`pages/\`.`,
|
|
676
|
+
`
|
|
677
|
+
## Pages
|
|
678
|
+
| # | Title | URL | File |
|
|
679
|
+
|---|-------|-----|------|
|
|
680
|
+
${indexRows.join("\n")}`
|
|
681
|
+
].join("\n");
|
|
682
|
+
const indexFile = (0, import_node_path.join)(dir, "index.md");
|
|
683
|
+
(0, import_node_fs.writeFileSync)(indexFile, index, "utf8");
|
|
684
|
+
let seoFiles;
|
|
685
|
+
if (seo) {
|
|
686
|
+
const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
687
|
+
const pagesJsonl = (0, import_node_path.join)(dir, "pages.jsonl");
|
|
688
|
+
const linksJsonl = (0, import_node_path.join)(dir, "links.jsonl");
|
|
689
|
+
const metricsJsonl = (0, import_node_path.join)(dir, "link-metrics.jsonl");
|
|
690
|
+
const issuesFile = (0, import_node_path.join)(dir, "issues.json");
|
|
691
|
+
const reportFile = (0, import_node_path.join)(dir, "report.md");
|
|
692
|
+
(0, import_node_fs.writeFileSync)(pagesJsonl, toJsonl(seo.pageRows), "utf8");
|
|
693
|
+
(0, import_node_fs.writeFileSync)(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
694
|
+
(0, import_node_fs.writeFileSync)(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
695
|
+
(0, import_node_fs.writeFileSync)(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
696
|
+
(0, import_node_fs.writeFileSync)(reportFile, seo.reportMd, "utf8");
|
|
697
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
|
|
698
|
+
if (seo.branding) {
|
|
699
|
+
const brandingFile = (0, import_node_path.join)(dir, "branding.json");
|
|
700
|
+
(0, import_node_fs.writeFileSync)(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
701
|
+
seoFiles.push(brandingFile);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { dir, indexFile, fileCount: pages.length, seoFiles };
|
|
705
|
+
} catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function saveUrlInventory(siteUrl, urls) {
|
|
710
|
+
if (!reportSavingActive()) return null;
|
|
711
|
+
try {
|
|
712
|
+
const outDir = outputBaseDir();
|
|
713
|
+
(0, import_node_fs.mkdirSync)(outDir, { recursive: true });
|
|
714
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
715
|
+
const file = (0, import_node_path.join)(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
|
|
716
|
+
const csv = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
717
|
+
const rows = ["url,status", ...urls.map((u) => `${csv(u.url)},${u.status ?? ""}`)];
|
|
718
|
+
(0, import_node_fs.writeFileSync)(file, rows.join("\n"), "utf8");
|
|
719
|
+
return file;
|
|
720
|
+
} catch {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
460
724
|
function persistScreenshotLocally(base64, url) {
|
|
461
725
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
462
726
|
try {
|
|
@@ -471,8 +735,8 @@ function persistScreenshotLocally(base64, url) {
|
|
|
471
735
|
return null;
|
|
472
736
|
}
|
|
473
737
|
}
|
|
474
|
-
function oneBlock(content) {
|
|
475
|
-
const filePath = saveFullReport(content);
|
|
738
|
+
function oneBlock(content, diskContent) {
|
|
739
|
+
const filePath = saveFullReport(diskContent ?? content);
|
|
476
740
|
const text = filePath ? `${content}
|
|
477
741
|
|
|
478
742
|
\u{1F4C4} Saved: \`${filePath}\`` : content;
|
|
@@ -741,7 +1005,12 @@ ${[h1Lines, h2Lines].filter(Boolean).join("\n")}` : "";
|
|
|
741
1005
|
].filter(Boolean).join("\n") : "";
|
|
742
1006
|
const bodySection = bodyMd ? `
|
|
743
1007
|
## Page Content
|
|
744
|
-
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ?
|
|
1008
|
+
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
|
|
1009
|
+
|
|
1010
|
+
*(truncated to 3,000 of ${bodyMd.length.toLocaleString()} chars \u2014 full content in the saved report)*` : ""}` : "";
|
|
1011
|
+
const bodySectionFull = bodyMd ? `
|
|
1012
|
+
## Page Content
|
|
1013
|
+
${bodyMd}` : "";
|
|
745
1014
|
const screenshotSection = screenshotMeta ? `
|
|
746
1015
|
## Screenshot
|
|
747
1016
|
- **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
|
|
@@ -772,7 +1041,10 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
|
|
|
772
1041
|
const full = `# URL Extract: ${url}
|
|
773
1042
|
**${title}**
|
|
774
1043
|
${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
|
|
775
|
-
const
|
|
1044
|
+
const diskReport = `# URL Extract: ${url}
|
|
1045
|
+
**${title}**
|
|
1046
|
+
${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
|
|
1047
|
+
const textResult = oneBlock(full, diskReport);
|
|
776
1048
|
const structuredContent = {
|
|
777
1049
|
url,
|
|
778
1050
|
title: d.title ?? null,
|
|
@@ -805,7 +1077,16 @@ function formatMapSiteUrls(raw, input) {
|
|
|
805
1077
|
const ok = urls.filter((u) => (u.status ?? 0) >= 200 && (u.status ?? 0) < 300);
|
|
806
1078
|
const broken = urls.filter((u) => u.status !== null && u.status >= 400);
|
|
807
1079
|
const redirects = urls.filter((u) => u.status !== null && u.status >= 300 && u.status < 400);
|
|
808
|
-
const
|
|
1080
|
+
const isBulk = urls.length > BULK_URL_THRESHOLD;
|
|
1081
|
+
const inventoryFile = isBulk ? saveUrlInventory(input.url, urls.map((u) => ({ url: u.url, status: u.status ?? null }))) : null;
|
|
1082
|
+
const inlineCount = isBulk ? BULK_URL_THRESHOLD : 200;
|
|
1083
|
+
const urlRows = urls.slice(0, inlineCount).map((u, i) => `| ${i + 1} | ${u.url} | ${u.status ?? "\u2014"} |`).join("\n");
|
|
1084
|
+
const inventoryNote = isBulk ? inventoryFile ? `
|
|
1085
|
+
## \u{1F4C1} Full inventory saved
|
|
1086
|
+
- **File:** \`${inventoryFile}\` (CSV: url, status \u2014 all ${urls.length} URLs)
|
|
1087
|
+
Showing the first ${inlineCount} below; read the file for the complete list.` : `
|
|
1088
|
+
## \u26A0\uFE0F Full inventory not saved
|
|
1089
|
+
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.` : "";
|
|
809
1090
|
const full = [
|
|
810
1091
|
`# URL Map: ${input.url}`,
|
|
811
1092
|
`**${d.totalFound} URLs** \xB7 ${(d.durationMs / 1e3).toFixed(1)}s${d.truncated ? " \xB7 *truncated*" : ""}`,
|
|
@@ -814,12 +1095,13 @@ function formatMapSiteUrls(raw, input) {
|
|
|
814
1095
|
- \u2705 2xx: ${ok.length}
|
|
815
1096
|
- \u{1F500} 3xx: ${redirects.length}
|
|
816
1097
|
- \u274C 4xx+: ${broken.length}`,
|
|
1098
|
+
inventoryNote,
|
|
817
1099
|
`
|
|
818
|
-
## URL Inventory
|
|
1100
|
+
## URL Inventory${isBulk ? ` (first ${inlineCount} of ${urls.length})` : ""}
|
|
819
1101
|
| # | URL | Status |
|
|
820
1102
|
|---|-----|--------|
|
|
821
1103
|
${urlRows}`,
|
|
822
|
-
broken.length ? `
|
|
1104
|
+
!isBulk && broken.length ? `
|
|
823
1105
|
## Broken URLs
|
|
824
1106
|
${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
825
1107
|
`
|
|
@@ -837,47 +1119,116 @@ ${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
|
837
1119
|
okCount: ok.length,
|
|
838
1120
|
redirectCount: redirects.length,
|
|
839
1121
|
brokenCount: broken.length,
|
|
1122
|
+
inventoryFile: inventoryFile ?? null,
|
|
840
1123
|
urls: urls.map((u) => ({ url: u.url, status: u.status ?? null })),
|
|
841
1124
|
durationMs: d.durationMs ?? 0
|
|
842
1125
|
}
|
|
843
1126
|
};
|
|
844
1127
|
}
|
|
1128
|
+
function buildSeoExport(siteUrl, pages, branding) {
|
|
1129
|
+
const { edges, metrics } = buildLinkGraph(pages, siteUrl);
|
|
1130
|
+
const pageRows = pages.map((p) => {
|
|
1131
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
1132
|
+
const m = metrics.get(p.url);
|
|
1133
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
1134
|
+
});
|
|
1135
|
+
const issues = computeIssues(pages, metrics);
|
|
1136
|
+
return {
|
|
1137
|
+
pageRows,
|
|
1138
|
+
edges,
|
|
1139
|
+
metrics: [...metrics.values()],
|
|
1140
|
+
issues,
|
|
1141
|
+
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
1142
|
+
branding: branding ?? void 0
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
845
1145
|
function formatExtractSite(raw, input) {
|
|
846
1146
|
const parsed = parseData(raw);
|
|
847
1147
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
848
1148
|
const d = parsed.data;
|
|
849
1149
|
const pages = d.pages ?? [];
|
|
850
|
-
const
|
|
851
|
-
|
|
1150
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
1151
|
+
const pageRow = (p, i) => {
|
|
1152
|
+
const schemaInfo = schemaTypesOf(p).join(", ") || "\u2014";
|
|
852
1153
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | ${schemaInfo} |`;
|
|
853
|
-
}
|
|
854
|
-
const
|
|
1154
|
+
};
|
|
1155
|
+
const tips = `
|
|
1156
|
+
---
|
|
1157
|
+
\u{1F4A1} **Tips**
|
|
1158
|
+
- Map URLs first: use \`map_site_urls\`
|
|
1159
|
+
- Inspect a single page: use \`extract_url\``;
|
|
1160
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
1161
|
+
const structuredContent = {
|
|
1162
|
+
url: input.url,
|
|
1163
|
+
pageCount: pages.length,
|
|
1164
|
+
pages: pages.map((p) => ({
|
|
1165
|
+
url: String(p.url ?? ""),
|
|
1166
|
+
title: p.title ?? null,
|
|
1167
|
+
schemaTypes: schemaTypesOf(p)
|
|
1168
|
+
})),
|
|
1169
|
+
durationMs: d.durationMs ?? 0
|
|
1170
|
+
};
|
|
1171
|
+
if (pages.length > BULK_PAGE_THRESHOLD) {
|
|
1172
|
+
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
1173
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
1174
|
+
url: String(p.url ?? ""),
|
|
1175
|
+
title: p.title ?? null,
|
|
1176
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
1177
|
+
metaDescription: p.metaDescription ?? null,
|
|
1178
|
+
schemaTypes: schemaTypesOf(p)
|
|
1179
|
+
})), seo);
|
|
1180
|
+
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
1181
|
+
const seoLine = bulk?.seoFiles?.length ? `
|
|
1182
|
+
- **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)` : "";
|
|
1183
|
+
const location = bulk ? `
|
|
1184
|
+
## \u{1F4C1} Bulk scrape saved
|
|
1185
|
+
- **Folder:** \`${bulk.dir}\`
|
|
1186
|
+
- **Index:** \`${bulk.indexFile}\`
|
|
1187
|
+
- **Files:** ${bulk.fileCount} page files under \`pages/\`${seoLine}
|
|
1188
|
+
|
|
1189
|
+
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.` : `
|
|
1190
|
+
## \u26A0\uFE0F Bulk scrape not saved
|
|
1191
|
+
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.`;
|
|
1192
|
+
const full2 = [
|
|
1193
|
+
`# Site Extract: ${input.url}`,
|
|
1194
|
+
durationLine,
|
|
1195
|
+
location,
|
|
1196
|
+
`
|
|
1197
|
+
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
1198
|
+
| # | Title | URL | Schema |
|
|
1199
|
+
|---|-------|-----|--------|
|
|
1200
|
+
${preview}`,
|
|
1201
|
+
tips
|
|
1202
|
+
].join("\n");
|
|
1203
|
+
return { content: [{ type: "text", text: full2 }], structuredContent: { ...structuredContent, bulkFolder: bulk?.dir ?? null } };
|
|
1204
|
+
}
|
|
1205
|
+
const header = [
|
|
855
1206
|
`# Site Extract: ${input.url}`,
|
|
856
|
-
|
|
1207
|
+
durationLine,
|
|
857
1208
|
`
|
|
858
1209
|
## Pages
|
|
859
1210
|
| # | Title | URL | Schema |
|
|
860
1211
|
|---|-------|-----|--------|
|
|
861
|
-
${
|
|
862
|
-
`
|
|
863
|
-
---
|
|
864
|
-
\u{1F4A1} **Tips**
|
|
865
|
-
- Map URLs first: use \`map_site_urls\`
|
|
866
|
-
- Inspect a single page: use \`extract_url\``
|
|
1212
|
+
${pages.map(pageRow).join("\n")}`
|
|
867
1213
|
].join("\n");
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
}
|
|
1214
|
+
const full = `${header}${tips}`;
|
|
1215
|
+
const pageDetails = pages.map((p, i) => {
|
|
1216
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
1217
|
+
return [
|
|
1218
|
+
`
|
|
1219
|
+
## ${i + 1}. ${p.title ?? "Untitled"}`,
|
|
1220
|
+
`- **URL:** ${p.url}`,
|
|
1221
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
1222
|
+
body ? `
|
|
1223
|
+
${body}` : "_(no content extracted)_"
|
|
1224
|
+
].filter(Boolean).join("\n");
|
|
1225
|
+
}).join("\n");
|
|
1226
|
+
const diskReport = `${header}
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
# Full Page Content
|
|
1230
|
+
${pageDetails}${tips}`;
|
|
1231
|
+
return { ...oneBlock(full, diskReport), structuredContent };
|
|
881
1232
|
}
|
|
882
1233
|
function formatYoutubeHarvest(raw, input) {
|
|
883
1234
|
const parsed = parseData(raw);
|
|
@@ -1964,6 +2315,33 @@ ${limitations.map((l) => `- ${l}`).join("\n")}` : "",
|
|
|
1964
2315
|
};
|
|
1965
2316
|
}
|
|
1966
2317
|
|
|
2318
|
+
// src/mcp/server-instructions.ts
|
|
2319
|
+
var SERVER_INSTRUCTIONS = `
|
|
2320
|
+
This server scrapes and analyzes web, social, search, maps, and site data. Pick a tool by NAME using
|
|
2321
|
+
this routing map, then load its schema before calling.
|
|
2322
|
+
|
|
2323
|
+
ROUTING
|
|
2324
|
+
- One web page -> extract_url. Whole site (crawl + SEO report) -> extract_site. Just the URL list -> map_site_urls.
|
|
2325
|
+
- Web search (organic SERP) -> search_serp. Full SERP + People-Also-Ask / AI-Overview detail -> harvest_paa.
|
|
2326
|
+
- Google Maps: find places -> maps_search; one place deep-dive + reviews -> maps_place_intel.
|
|
2327
|
+
- YouTube: find or list videos -> youtube_harvest; transcribe one video -> youtube_transcribe.
|
|
2328
|
+
- Facebook: find ads -> facebook_ad_search; transcribe an ad -> facebook_ad_transcribe; transcribe a
|
|
2329
|
+
video -> facebook_video_transcribe; page profile/intel -> facebook_page_intel.
|
|
2330
|
+
- Instagram: profile inventory -> instagram_profile_content; one post or reel -> instagram_media_download.
|
|
2331
|
+
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
2332
|
+
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
2333
|
+
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
2334
|
+
(browser_open, then navigate/read); save/reuse logins via browser_profile_*.
|
|
2335
|
+
|
|
2336
|
+
NOTES
|
|
2337
|
+
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
2338
|
+
It discovers URLs and returns a saved folder/artifact plus a summary, not the full content inline.
|
|
2339
|
+
- Tools that open a browser session or transcribe media cost credits and take longer. Prefer the
|
|
2340
|
+
cheapest tool that answers the question (plain search/extract before browser agents).
|
|
2341
|
+
- Large results are saved to disk or an artifact and returned as a summary plus a path; read the path
|
|
2342
|
+
for full detail rather than expecting the whole payload inline.
|
|
2343
|
+
`.trim();
|
|
2344
|
+
|
|
1967
2345
|
// src/mcp/mcp-tool-schemas.ts
|
|
1968
2346
|
var import_zod2 = require("zod");
|
|
1969
2347
|
|
|
@@ -2088,11 +2466,14 @@ var ExtractUrlInputSchema = {
|
|
|
2088
2466
|
};
|
|
2089
2467
|
var MapSiteUrlsInputSchema = {
|
|
2090
2468
|
url: import_zod2.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."),
|
|
2091
|
-
maxUrls: import_zod2.z.number().int().min(1).max(
|
|
2469
|
+
maxUrls: import_zod2.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.")
|
|
2092
2470
|
};
|
|
2093
2471
|
var ExtractSiteInputSchema = {
|
|
2094
2472
|
url: import_zod2.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."),
|
|
2095
|
-
maxPages: import_zod2.z.number().int().min(1).max(
|
|
2473
|
+
maxPages: import_zod2.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."),
|
|
2474
|
+
rotateProxies: import_zod2.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."),
|
|
2475
|
+
rotateProxyEvery: import_zod2.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."),
|
|
2476
|
+
formats: import_zod2.z.array(import_zod2.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.")
|
|
2096
2477
|
};
|
|
2097
2478
|
var YoutubeHarvestInputSchema = {
|
|
2098
2479
|
mode: import_zod2.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
@@ -3197,7 +3578,7 @@ function registerSavedReportResources(server2) {
|
|
|
3197
3578
|
);
|
|
3198
3579
|
}
|
|
3199
3580
|
function buildPaaExtractorMcpServer(executor2, options = {}) {
|
|
3200
|
-
const server2 = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
3581
|
+
const server2 = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
3201
3582
|
registerPaaExtractorMcpTools(server2, executor2, options);
|
|
3202
3583
|
return server2;
|
|
3203
3584
|
}
|
|
@@ -3229,14 +3610,14 @@ function registerPaaExtractorMcpTools(server2, executor2, options = {}) {
|
|
|
3229
3610
|
}, async (input) => formatExtractUrl(await executor2.extractUrl(input), input));
|
|
3230
3611
|
server2.registerTool("map_site_urls", {
|
|
3231
3612
|
title: "Site URL Map",
|
|
3232
|
-
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."),
|
|
3613
|
+
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."),
|
|
3233
3614
|
inputSchema: MapSiteUrlsInputSchema,
|
|
3234
3615
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
3235
3616
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
3236
3617
|
}, async (input) => formatMapSiteUrls(await executor2.mapSiteUrls(input), input));
|
|
3237
3618
|
server2.registerTool("extract_site", {
|
|
3238
3619
|
title: "Multi-Page Site Extract",
|
|
3239
|
-
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."),
|
|
3620
|
+
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."),
|
|
3240
3621
|
inputSchema: ExtractSiteInputSchema,
|
|
3241
3622
|
outputSchema: ExtractSiteOutputSchema,
|
|
3242
3623
|
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
@@ -3360,7 +3741,7 @@ function registerPaaExtractorMcpTools(server2, executor2, options = {}) {
|
|
|
3360
3741
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
3361
3742
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
3362
3743
|
}, async (input) => formatWorkflowArtifactRead(await executor2.workflowArtifactRead(input), input));
|
|
3363
|
-
server2.registerTool("
|
|
3744
|
+
server2.registerTool("rank_tracker_workflow", {
|
|
3364
3745
|
title: "Rank Tracker Blueprint Builder",
|
|
3365
3746
|
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.",
|
|
3366
3747
|
inputSchema: RankTrackerBlueprintInputSchema,
|