mcp-scraper 0.3.13 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/api-server.cjs +1991 -508
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +9 -9
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +428 -47
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +6 -5
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +419 -38
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/chunk-2MX5WOII.js +7 -0
- package/dist/chunk-2MX5WOII.js.map +1 -0
- package/dist/{chunk-DBQDG7EH.js → chunk-5H22TOXQ.js} +28 -1
- package/dist/chunk-5H22TOXQ.js.map +1 -0
- package/dist/{chunk-KPXMPAJ3.js → chunk-7Y6JDBML.js} +2 -2
- package/dist/chunk-7Y6JDBML.js.map +1 -0
- package/dist/{chunk-QAOIH5U2.js → chunk-WJ2LWHPA.js} +10 -10
- package/dist/chunk-WJ2LWHPA.js.map +1 -0
- package/dist/{chunk-YGV2XZDT.js → chunk-WQAIOY2D.js} +424 -39
- package/dist/chunk-WQAIOY2D.js.map +1 -0
- package/dist/{chunk-AZ5PKU4F.js → chunk-ZAUMSBV3.js} +2 -2
- package/dist/{db-BE4JVB3V.js → db-P76GVIFN.js} +2 -2
- package/dist/{server-LX65PB7D.js → server-UOQ2JIUQ.js} +1106 -93
- package/dist/server-UOQ2JIUQ.js.map +1 -0
- package/dist/{worker-FG7ZWEGA.js → worker-2WVKKCC7.js} +3 -3
- package/docs/final-tooling-spec.md +206 -0
- package/docs/mcp-tool-craft-lint.generated.md +1 -1
- package/docs/mcp-tool-design-guide.md +225 -0
- package/docs/mcp-tool-manifest.generated.json +13 -13
- package/docs/seo-crawl-report-spec.md +287 -0
- package/docs/tool-catalog-spec.md +388 -0
- package/package.json +2 -1
- package/dist/chunk-BEZJ4G32.js +0 -7
- package/dist/chunk-BEZJ4G32.js.map +0 -1
- package/dist/chunk-DBQDG7EH.js.map +0 -1
- package/dist/chunk-KPXMPAJ3.js.map +0 -1
- package/dist/chunk-QAOIH5U2.js.map +0 -1
- package/dist/chunk-YGV2XZDT.js.map +0 -1
- package/dist/server-LX65PB7D.js.map +0 -1
- /package/dist/{chunk-AZ5PKU4F.js.map → chunk-ZAUMSBV3.js.map} +0 -0
- /package/dist/{db-BE4JVB3V.js.map → db-P76GVIFN.js.map} +0 -0
- /package/dist/{worker-FG7ZWEGA.js.map → worker-2WVKKCC7.js.map} +0 -0
|
@@ -453,7 +453,7 @@ render();
|
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
// src/version.ts
|
|
456
|
-
var PACKAGE_VERSION = "0.3.
|
|
456
|
+
var PACKAGE_VERSION = "0.3.15";
|
|
457
457
|
|
|
458
458
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
459
459
|
var import_zod = require("zod");
|
|
@@ -604,7 +604,7 @@ var FanoutSourceOutput = import_zod.z.object({
|
|
|
604
604
|
});
|
|
605
605
|
var BrowserCaptureFanoutOutputSchema = {
|
|
606
606
|
...BrowserBaseOutput,
|
|
607
|
-
tool: import_zod.z.literal("
|
|
607
|
+
tool: import_zod.z.literal("query_fanout_workflow"),
|
|
608
608
|
platform: import_zod.z.enum(["ChatGPT", "Claude"]).describe("Which AI-search surface the fan-out was captured from."),
|
|
609
609
|
captured_at: import_zod.z.string(),
|
|
610
610
|
prompt: import_zod.z.string().describe("The user prompt that triggered the captured fan-out, when recoverable."),
|
|
@@ -1145,13 +1145,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1145
1145
|
const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
|
|
1146
1146
|
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
1147
1147
|
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1148
|
-
async function req(method, path, body) {
|
|
1148
|
+
async function req(method, path, body, requestTimeoutMs = timeoutMs) {
|
|
1149
1149
|
try {
|
|
1150
1150
|
const res = await fetch(`${baseUrl2}${path}`, {
|
|
1151
1151
|
method,
|
|
1152
1152
|
headers: { "Content-Type": "application/json", "x-api-key": opts.apiKey },
|
|
1153
1153
|
body: body ? JSON.stringify(body) : void 0,
|
|
1154
|
-
signal: AbortSignal.timeout(
|
|
1154
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
1155
1155
|
});
|
|
1156
1156
|
const data = await res.json().catch(() => ({}));
|
|
1157
1157
|
return { ok: res.ok, data };
|
|
@@ -1703,7 +1703,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1703
1703
|
}
|
|
1704
1704
|
);
|
|
1705
1705
|
server2.registerTool(
|
|
1706
|
-
"
|
|
1706
|
+
"query_fanout_workflow",
|
|
1707
1707
|
{
|
|
1708
1708
|
title: "Capture AI Search Fan-Out",
|
|
1709
1709
|
description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): the exact sub-queries the model issued, every researched URL split into cited vs browsed-only (with citation frequency and snippet), each source tagged by category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. Set export=true for durable JSON, CSV, TSV, and HTML artifacts; export paths are returned relative to MCP_SCRAPER_OUTPUT_DIR. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already connected or if the profile disconnected: call browser_profile_onboard, give the user the watch_url, let them complete login, then call browser_profile_status until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
|
|
@@ -1714,7 +1714,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1714
1714
|
async (input) => {
|
|
1715
1715
|
const emit = (result, exports3) => structuredResult({
|
|
1716
1716
|
ok: true,
|
|
1717
|
-
tool: "
|
|
1717
|
+
tool: "query_fanout_workflow",
|
|
1718
1718
|
session_id: input.session_id,
|
|
1719
1719
|
platform: result.platform,
|
|
1720
1720
|
captured_at: result.capturedAt,
|
|
@@ -1737,8 +1737,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1737
1737
|
first_party_domain: input.first_party_domain,
|
|
1738
1738
|
reset: input.reset,
|
|
1739
1739
|
export: input.export
|
|
1740
|
-
});
|
|
1741
|
-
if (!res.ok) return errorResult("
|
|
1740
|
+
}, Math.max(timeoutMs, (input.wait_ms ?? (input.prompt ? 9e4 : 8e3)) + 3e4));
|
|
1741
|
+
if (!res.ok) return errorResult("query_fanout_workflow", res.data, input.session_id);
|
|
1742
1742
|
const hosted = res.data?.result ?? res.data;
|
|
1743
1743
|
let exports2 = res.data?.exports ?? null;
|
|
1744
1744
|
if (input.export && !exports2 && isEnrichedFanout(hosted)) {
|
|
@@ -1746,7 +1746,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1746
1746
|
exports2 = exportFanout(hosted);
|
|
1747
1747
|
} catch (err) {
|
|
1748
1748
|
return errorResult(
|
|
1749
|
-
"
|
|
1749
|
+
"query_fanout_workflow",
|
|
1750
1750
|
{
|
|
1751
1751
|
error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1752
1752
|
result: hosted
|
|
@@ -1890,6 +1890,191 @@ function suggestWorkflowRecipes(goal, limit = 3) {
|
|
|
1890
1890
|
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);
|
|
1891
1891
|
}
|
|
1892
1892
|
|
|
1893
|
+
// src/api/seo-link-graph.ts
|
|
1894
|
+
function normalize2(u) {
|
|
1895
|
+
return u.split("#")[0].replace(/\/$/, "");
|
|
1896
|
+
}
|
|
1897
|
+
function buildLinkGraph(pages, startUrl) {
|
|
1898
|
+
const statusByUrl = /* @__PURE__ */ new Map();
|
|
1899
|
+
for (const p of pages) statusByUrl.set(normalize2(p.url), p.status);
|
|
1900
|
+
const edges = [];
|
|
1901
|
+
for (const p of pages) {
|
|
1902
|
+
for (const l of p.outlinks ?? []) {
|
|
1903
|
+
const nofollow = (l.rel ?? "").split(/\s+/).includes("nofollow");
|
|
1904
|
+
edges.push({
|
|
1905
|
+
from: p.url,
|
|
1906
|
+
to: l.href,
|
|
1907
|
+
anchor: l.anchor,
|
|
1908
|
+
rel: l.rel,
|
|
1909
|
+
internal: l.internal,
|
|
1910
|
+
nofollow,
|
|
1911
|
+
targetStatus: statusByUrl.get(normalize2(l.href)) ?? null
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
const inboundFrom = /* @__PURE__ */ new Map();
|
|
1916
|
+
const inboundAnchors = /* @__PURE__ */ new Map();
|
|
1917
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1918
|
+
for (const e of edges) {
|
|
1919
|
+
if (!e.internal) continue;
|
|
1920
|
+
const to = normalize2(e.to);
|
|
1921
|
+
const from = normalize2(e.from);
|
|
1922
|
+
if (!inboundFrom.has(to)) inboundFrom.set(to, /* @__PURE__ */ new Set());
|
|
1923
|
+
inboundFrom.get(to).add(from);
|
|
1924
|
+
if (e.anchor) {
|
|
1925
|
+
if (!inboundAnchors.has(to)) inboundAnchors.set(to, []);
|
|
1926
|
+
inboundAnchors.get(to).push(e.anchor);
|
|
1927
|
+
}
|
|
1928
|
+
if (!e.nofollow && (statusByUrl.get(to) === 200 || !statusByUrl.has(to))) {
|
|
1929
|
+
if (!adjacency.has(from)) adjacency.set(from, /* @__PURE__ */ new Set());
|
|
1930
|
+
adjacency.get(from).add(to);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const depth = /* @__PURE__ */ new Map();
|
|
1934
|
+
const start = normalize2(startUrl);
|
|
1935
|
+
const queue = [start];
|
|
1936
|
+
depth.set(start, 0);
|
|
1937
|
+
while (queue.length > 0) {
|
|
1938
|
+
const cur = queue.shift();
|
|
1939
|
+
const d = depth.get(cur);
|
|
1940
|
+
for (const next of adjacency.get(cur) ?? []) {
|
|
1941
|
+
if (!depth.has(next)) {
|
|
1942
|
+
depth.set(next, d + 1);
|
|
1943
|
+
queue.push(next);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const topAnchorsFor = (url) => {
|
|
1948
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1949
|
+
for (const a of inboundAnchors.get(url) ?? []) counts.set(a, (counts.get(a) ?? 0) + 1);
|
|
1950
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map((e) => e[0]);
|
|
1951
|
+
};
|
|
1952
|
+
const metrics = /* @__PURE__ */ new Map();
|
|
1953
|
+
for (const p of pages) {
|
|
1954
|
+
const key = normalize2(p.url);
|
|
1955
|
+
const inbound = inboundFrom.get(key) ?? /* @__PURE__ */ new Set();
|
|
1956
|
+
metrics.set(p.url, {
|
|
1957
|
+
url: p.url,
|
|
1958
|
+
inlinks: inbound.size,
|
|
1959
|
+
uniqueInlinks: inbound.size,
|
|
1960
|
+
outlinksInternal: (p.outlinks ?? []).filter((l) => l.internal).length,
|
|
1961
|
+
outlinksExternal: (p.outlinks ?? []).filter((l) => !l.internal).length,
|
|
1962
|
+
crawlDepth: depth.has(key) ? depth.get(key) : null,
|
|
1963
|
+
orphan: inbound.size === 0 && key !== start,
|
|
1964
|
+
topAnchors: topAnchorsFor(key)
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
return { edges, metrics };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/api/seo-issues.ts
|
|
1971
|
+
var THIN_WORDS = 200;
|
|
1972
|
+
var TITLE_MAX = 60;
|
|
1973
|
+
var TITLE_PX_MAX = 561;
|
|
1974
|
+
var TITLE_MIN = 30;
|
|
1975
|
+
var META_MAX = 155;
|
|
1976
|
+
var META_MIN = 70;
|
|
1977
|
+
var H1_MAX = 70;
|
|
1978
|
+
var URL_MAX = 115;
|
|
1979
|
+
function dupes(pages, key) {
|
|
1980
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1981
|
+
for (const p of pages) {
|
|
1982
|
+
const k = key(p);
|
|
1983
|
+
if (k === null || k === void 0 || k === "") continue;
|
|
1984
|
+
const ks = String(k);
|
|
1985
|
+
if (!groups.has(ks)) groups.set(ks, []);
|
|
1986
|
+
groups.get(ks).push(p.url);
|
|
1987
|
+
}
|
|
1988
|
+
return [...groups.values()].filter((g) => g.length > 1).flat();
|
|
1989
|
+
}
|
|
1990
|
+
function computeIssues(pages, metrics) {
|
|
1991
|
+
const group = (urls) => ({ count: urls.length, urls: urls.slice(0, 200) });
|
|
1992
|
+
const where = (fn) => pages.filter(fn).map((p) => p.url);
|
|
1993
|
+
const ok = (p) => p.status === 200;
|
|
1994
|
+
const pathname = (u) => {
|
|
1995
|
+
try {
|
|
1996
|
+
return new URL(u).pathname;
|
|
1997
|
+
} catch {
|
|
1998
|
+
return "";
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
const report = {
|
|
2002
|
+
"title.missing": group(where((p) => ok(p) && !p.title)),
|
|
2003
|
+
"title.duplicate": group(dupes(pages.filter(ok), (p) => p.title)),
|
|
2004
|
+
"title.tooLong": group(where((p) => (p.titleLength ?? 0) > TITLE_MAX || (p.titlePixels ?? 0) > TITLE_PX_MAX)),
|
|
2005
|
+
"title.tooShort": group(where((p) => p.title != null && (p.titleLength ?? 0) < TITLE_MIN)),
|
|
2006
|
+
"title.sameAsH1": group(where((p) => !!p.title && !!p.h1 && p.title.trim() === p.h1.trim())),
|
|
2007
|
+
"meta.missing": group(where((p) => ok(p) && !p.metaDescription)),
|
|
2008
|
+
"meta.duplicate": group(dupes(pages.filter(ok), (p) => p.metaDescription)),
|
|
2009
|
+
"meta.tooLong": group(where((p) => (p.metaDescLength ?? 0) > META_MAX)),
|
|
2010
|
+
"meta.tooShort": group(where((p) => p.metaDescription != null && (p.metaDescLength ?? 0) < META_MIN)),
|
|
2011
|
+
"h1.missing": group(where((p) => ok(p) && !p.h1)),
|
|
2012
|
+
"h1.multiple": group(where((p) => !!p.h1_2)),
|
|
2013
|
+
"h1.duplicate": group(dupes(pages.filter(ok), (p) => p.h1)),
|
|
2014
|
+
"h1.tooLong": group(where((p) => (p.h1?.length ?? 0) > H1_MAX)),
|
|
2015
|
+
"h2.missing": group(where((p) => ok(p) && p.h2Count === 0)),
|
|
2016
|
+
"indexability.nonIndexable": group(where((p) => !p.indexable)),
|
|
2017
|
+
"indexability.noindex": group(where((p) => p.indexabilityReason === "noindex")),
|
|
2018
|
+
"canonical.missing": group(where((p) => ok(p) && !p.canonicalUrl)),
|
|
2019
|
+
"canonical.canonicalised": group(where((p) => p.indexabilityReason === "canonicalised")),
|
|
2020
|
+
"response.broken4xx": group(where((p) => (p.status ?? 0) >= 400 && (p.status ?? 0) < 500)),
|
|
2021
|
+
"response.error5xx": group(where((p) => (p.status ?? 0) >= 500)),
|
|
2022
|
+
"response.redirect3xx": group(where((p) => (p.status ?? 0) >= 300 && (p.status ?? 0) < 400)),
|
|
2023
|
+
"response.noResponse": group(where((p) => p.status === null)),
|
|
2024
|
+
"content.thin": group(where((p) => ok(p) && p.wordCount > 0 && p.wordCount < THIN_WORDS)),
|
|
2025
|
+
"content.exactDuplicate": group(dupes(pages.filter((p) => ok(p) && p.contentHash), (p) => p.contentHash)),
|
|
2026
|
+
"images.missingAlt": group(where((p) => p.imagesMissingAlt > 0)),
|
|
2027
|
+
"schema.missing": group(where((p) => ok(p) && p.schemaTypes.length === 0)),
|
|
2028
|
+
"url.tooLong": group(where((p) => p.url.length > URL_MAX)),
|
|
2029
|
+
"url.uppercase": group(where((p) => /[A-Z]/.test(pathname(p.url)))),
|
|
2030
|
+
"url.underscores": group(where((p) => pathname(p.url).includes("_"))),
|
|
2031
|
+
"links.orphan": group([...metrics.values()].filter((m) => m.orphan).map((m) => m.url))
|
|
2032
|
+
};
|
|
2033
|
+
const normUrl = (u) => {
|
|
2034
|
+
try {
|
|
2035
|
+
const x = new URL(u);
|
|
2036
|
+
x.hash = "";
|
|
2037
|
+
return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
|
|
2038
|
+
} catch {
|
|
2039
|
+
return u.replace(/#.*$/, "").replace(/\/+$/, "");
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
const statusByUrl = new Map(pages.map((p) => [normUrl(p.url), p.status]));
|
|
2043
|
+
const brokenLinkPages = /* @__PURE__ */ new Set();
|
|
2044
|
+
for (const p of pages) {
|
|
2045
|
+
for (const l of p.outlinks ?? []) {
|
|
2046
|
+
if (!l.internal) continue;
|
|
2047
|
+
const st = statusByUrl.get(normUrl(l.href));
|
|
2048
|
+
if (st != null && st >= 400) brokenLinkPages.add(p.url);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
report["links.brokenInternal"] = group([...brokenLinkPages]);
|
|
2052
|
+
return report;
|
|
2053
|
+
}
|
|
2054
|
+
function renderIssueReport(siteUrl, pages, report, metrics) {
|
|
2055
|
+
const total = pages.length;
|
|
2056
|
+
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}";
|
|
2057
|
+
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" : ""} |`);
|
|
2058
|
+
const depths = [...metrics.values()].map((m) => m.crawlDepth).filter((d) => d != null);
|
|
2059
|
+
const maxDepth = depths.length ? Math.max(...depths) : 0;
|
|
2060
|
+
const orphans = [...metrics.values()].filter((m) => m.orphan).length;
|
|
2061
|
+
const topLinked = [...metrics.values()].sort((a, b) => b.inlinks - a.inlinks).slice(0, 5);
|
|
2062
|
+
return [
|
|
2063
|
+
`# SEO Crawl Report: ${siteUrl}`,
|
|
2064
|
+
`**${total} pages crawled** \xB7 max crawl depth ${maxDepth} \xB7 ${orphans} orphan page(s)`,
|
|
2065
|
+
`
|
|
2066
|
+
## Issues
|
|
2067
|
+
| | Issue | Count | Examples |
|
|
2068
|
+
|---|-------|-------|----------|
|
|
2069
|
+
${rows.join("\n") || "| \u2705 | none | 0 | \u2014 |"}`,
|
|
2070
|
+
`
|
|
2071
|
+
## Most-linked pages
|
|
2072
|
+
${topLinked.map((m) => `- ${m.inlinks} inlinks \xB7 depth ${m.crawlDepth ?? "\u2014"} \xB7 ${m.url}`).join("\n")}`,
|
|
2073
|
+
`
|
|
2074
|
+
_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._`
|
|
2075
|
+
].join("\n");
|
|
2076
|
+
}
|
|
2077
|
+
|
|
1893
2078
|
// src/mcp/mcp-response-formatter.ts
|
|
1894
2079
|
var reportSavingEnabled = true;
|
|
1895
2080
|
function sanitizeVendorText(text) {
|
|
@@ -1920,6 +2105,85 @@ function saveFullReport(full) {
|
|
|
1920
2105
|
return null;
|
|
1921
2106
|
}
|
|
1922
2107
|
}
|
|
2108
|
+
var BULK_PAGE_THRESHOLD = 25;
|
|
2109
|
+
var BULK_URL_THRESHOLD = 500;
|
|
2110
|
+
function reportSavingActive() {
|
|
2111
|
+
return reportSavingEnabled && process.env.MCP_SCRAPER_SAVE_REPORTS !== "false";
|
|
2112
|
+
}
|
|
2113
|
+
function saveBulkSite(siteUrl, pages, seo) {
|
|
2114
|
+
if (!reportSavingActive()) return null;
|
|
2115
|
+
try {
|
|
2116
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2117
|
+
const dir = (0, import_node_path4.join)(outputBaseDir3(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
|
|
2118
|
+
const pagesDir = (0, import_node_path4.join)(dir, "pages");
|
|
2119
|
+
(0, import_node_fs3.mkdirSync)(pagesDir, { recursive: true });
|
|
2120
|
+
const indexRows = pages.map((p, i) => {
|
|
2121
|
+
const num = String(i + 1).padStart(4, "0");
|
|
2122
|
+
const slug = slugifyReportName(p.url.replace(/^https?:\/\//, "")).slice(0, 60) || "page";
|
|
2123
|
+
const fname = `${num}-${slug}.md`;
|
|
2124
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
2125
|
+
const content = [
|
|
2126
|
+
`# ${p.title ?? "Untitled"}`,
|
|
2127
|
+
`- **URL:** ${p.url}`,
|
|
2128
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
2129
|
+
p.schemaTypes?.length ? `- **Schema:** ${p.schemaTypes.join(", ")}` : "",
|
|
2130
|
+
"",
|
|
2131
|
+
body || "_(no content extracted)_"
|
|
2132
|
+
].filter(Boolean).join("\n");
|
|
2133
|
+
(0, import_node_fs3.writeFileSync)((0, import_node_path4.join)(pagesDir, fname), content, "utf8");
|
|
2134
|
+
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
2135
|
+
});
|
|
2136
|
+
const index = [
|
|
2137
|
+
`# Site Extract: ${siteUrl}`,
|
|
2138
|
+
`**${pages.length} pages** \u2014 each saved as an individual Markdown file under \`pages/\`.`,
|
|
2139
|
+
`
|
|
2140
|
+
## Pages
|
|
2141
|
+
| # | Title | URL | File |
|
|
2142
|
+
|---|-------|-----|------|
|
|
2143
|
+
${indexRows.join("\n")}`
|
|
2144
|
+
].join("\n");
|
|
2145
|
+
const indexFile = (0, import_node_path4.join)(dir, "index.md");
|
|
2146
|
+
(0, import_node_fs3.writeFileSync)(indexFile, index, "utf8");
|
|
2147
|
+
let seoFiles;
|
|
2148
|
+
if (seo) {
|
|
2149
|
+
const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
2150
|
+
const pagesJsonl = (0, import_node_path4.join)(dir, "pages.jsonl");
|
|
2151
|
+
const linksJsonl = (0, import_node_path4.join)(dir, "links.jsonl");
|
|
2152
|
+
const metricsJsonl = (0, import_node_path4.join)(dir, "link-metrics.jsonl");
|
|
2153
|
+
const issuesFile = (0, import_node_path4.join)(dir, "issues.json");
|
|
2154
|
+
const reportFile = (0, import_node_path4.join)(dir, "report.md");
|
|
2155
|
+
(0, import_node_fs3.writeFileSync)(pagesJsonl, toJsonl(seo.pageRows), "utf8");
|
|
2156
|
+
(0, import_node_fs3.writeFileSync)(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
2157
|
+
(0, import_node_fs3.writeFileSync)(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
2158
|
+
(0, import_node_fs3.writeFileSync)(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
2159
|
+
(0, import_node_fs3.writeFileSync)(reportFile, seo.reportMd, "utf8");
|
|
2160
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
|
|
2161
|
+
if (seo.branding) {
|
|
2162
|
+
const brandingFile = (0, import_node_path4.join)(dir, "branding.json");
|
|
2163
|
+
(0, import_node_fs3.writeFileSync)(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
2164
|
+
seoFiles.push(brandingFile);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return { dir, indexFile, fileCount: pages.length, seoFiles };
|
|
2168
|
+
} catch {
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function saveUrlInventory(siteUrl, urls) {
|
|
2173
|
+
if (!reportSavingActive()) return null;
|
|
2174
|
+
try {
|
|
2175
|
+
const outDir = outputBaseDir3();
|
|
2176
|
+
(0, import_node_fs3.mkdirSync)(outDir, { recursive: true });
|
|
2177
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2178
|
+
const file = (0, import_node_path4.join)(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
|
|
2179
|
+
const csv = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
2180
|
+
const rows = ["url,status", ...urls.map((u) => `${csv(u.url)},${u.status ?? ""}`)];
|
|
2181
|
+
(0, import_node_fs3.writeFileSync)(file, rows.join("\n"), "utf8");
|
|
2182
|
+
return file;
|
|
2183
|
+
} catch {
|
|
2184
|
+
return null;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
1923
2187
|
function persistScreenshotLocally(base64, url) {
|
|
1924
2188
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
1925
2189
|
try {
|
|
@@ -1934,8 +2198,8 @@ function persistScreenshotLocally(base64, url) {
|
|
|
1934
2198
|
return null;
|
|
1935
2199
|
}
|
|
1936
2200
|
}
|
|
1937
|
-
function oneBlock(content) {
|
|
1938
|
-
const filePath = saveFullReport(content);
|
|
2201
|
+
function oneBlock(content, diskContent) {
|
|
2202
|
+
const filePath = saveFullReport(diskContent ?? content);
|
|
1939
2203
|
const text = filePath ? `${content}
|
|
1940
2204
|
|
|
1941
2205
|
\u{1F4C4} Saved: \`${filePath}\`` : content;
|
|
@@ -2204,7 +2468,12 @@ ${[h1Lines, h2Lines].filter(Boolean).join("\n")}` : "";
|
|
|
2204
2468
|
].filter(Boolean).join("\n") : "";
|
|
2205
2469
|
const bodySection = bodyMd ? `
|
|
2206
2470
|
## Page Content
|
|
2207
|
-
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ?
|
|
2471
|
+
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
|
|
2472
|
+
|
|
2473
|
+
*(truncated to 3,000 of ${bodyMd.length.toLocaleString()} chars \u2014 full content in the saved report)*` : ""}` : "";
|
|
2474
|
+
const bodySectionFull = bodyMd ? `
|
|
2475
|
+
## Page Content
|
|
2476
|
+
${bodyMd}` : "";
|
|
2208
2477
|
const screenshotSection = screenshotMeta ? `
|
|
2209
2478
|
## Screenshot
|
|
2210
2479
|
- **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
|
|
@@ -2235,7 +2504,10 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
|
|
|
2235
2504
|
const full = `# URL Extract: ${url}
|
|
2236
2505
|
**${title}**
|
|
2237
2506
|
${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
|
|
2238
|
-
const
|
|
2507
|
+
const diskReport = `# URL Extract: ${url}
|
|
2508
|
+
**${title}**
|
|
2509
|
+
${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
|
|
2510
|
+
const textResult = oneBlock(full, diskReport);
|
|
2239
2511
|
const structuredContent = {
|
|
2240
2512
|
url,
|
|
2241
2513
|
title: d.title ?? null,
|
|
@@ -2268,7 +2540,16 @@ function formatMapSiteUrls(raw, input) {
|
|
|
2268
2540
|
const ok = urls.filter((u) => (u.status ?? 0) >= 200 && (u.status ?? 0) < 300);
|
|
2269
2541
|
const broken = urls.filter((u) => u.status !== null && u.status >= 400);
|
|
2270
2542
|
const redirects = urls.filter((u) => u.status !== null && u.status >= 300 && u.status < 400);
|
|
2271
|
-
const
|
|
2543
|
+
const isBulk = urls.length > BULK_URL_THRESHOLD;
|
|
2544
|
+
const inventoryFile = isBulk ? saveUrlInventory(input.url, urls.map((u) => ({ url: u.url, status: u.status ?? null }))) : null;
|
|
2545
|
+
const inlineCount = isBulk ? BULK_URL_THRESHOLD : 200;
|
|
2546
|
+
const urlRows = urls.slice(0, inlineCount).map((u, i) => `| ${i + 1} | ${u.url} | ${u.status ?? "\u2014"} |`).join("\n");
|
|
2547
|
+
const inventoryNote = isBulk ? inventoryFile ? `
|
|
2548
|
+
## \u{1F4C1} Full inventory saved
|
|
2549
|
+
- **File:** \`${inventoryFile}\` (CSV: url, status \u2014 all ${urls.length} URLs)
|
|
2550
|
+
Showing the first ${inlineCount} below; read the file for the complete list.` : `
|
|
2551
|
+
## \u26A0\uFE0F Full inventory not saved
|
|
2552
|
+
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.` : "";
|
|
2272
2553
|
const full = [
|
|
2273
2554
|
`# URL Map: ${input.url}`,
|
|
2274
2555
|
`**${d.totalFound} URLs** \xB7 ${(d.durationMs / 1e3).toFixed(1)}s${d.truncated ? " \xB7 *truncated*" : ""}`,
|
|
@@ -2277,12 +2558,13 @@ function formatMapSiteUrls(raw, input) {
|
|
|
2277
2558
|
- \u2705 2xx: ${ok.length}
|
|
2278
2559
|
- \u{1F500} 3xx: ${redirects.length}
|
|
2279
2560
|
- \u274C 4xx+: ${broken.length}`,
|
|
2561
|
+
inventoryNote,
|
|
2280
2562
|
`
|
|
2281
|
-
## URL Inventory
|
|
2563
|
+
## URL Inventory${isBulk ? ` (first ${inlineCount} of ${urls.length})` : ""}
|
|
2282
2564
|
| # | URL | Status |
|
|
2283
2565
|
|---|-----|--------|
|
|
2284
2566
|
${urlRows}`,
|
|
2285
|
-
broken.length ? `
|
|
2567
|
+
!isBulk && broken.length ? `
|
|
2286
2568
|
## Broken URLs
|
|
2287
2569
|
${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
2288
2570
|
`
|
|
@@ -2300,47 +2582,116 @@ ${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
|
2300
2582
|
okCount: ok.length,
|
|
2301
2583
|
redirectCount: redirects.length,
|
|
2302
2584
|
brokenCount: broken.length,
|
|
2585
|
+
inventoryFile: inventoryFile ?? null,
|
|
2303
2586
|
urls: urls.map((u) => ({ url: u.url, status: u.status ?? null })),
|
|
2304
2587
|
durationMs: d.durationMs ?? 0
|
|
2305
2588
|
}
|
|
2306
2589
|
};
|
|
2307
2590
|
}
|
|
2591
|
+
function buildSeoExport(siteUrl, pages, branding) {
|
|
2592
|
+
const { edges, metrics } = buildLinkGraph(pages, siteUrl);
|
|
2593
|
+
const pageRows = pages.map((p) => {
|
|
2594
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
2595
|
+
const m = metrics.get(p.url);
|
|
2596
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
2597
|
+
});
|
|
2598
|
+
const issues = computeIssues(pages, metrics);
|
|
2599
|
+
return {
|
|
2600
|
+
pageRows,
|
|
2601
|
+
edges,
|
|
2602
|
+
metrics: [...metrics.values()],
|
|
2603
|
+
issues,
|
|
2604
|
+
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
2605
|
+
branding: branding ?? void 0
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2308
2608
|
function formatExtractSite(raw, input) {
|
|
2309
2609
|
const parsed = parseData(raw);
|
|
2310
2610
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
2311
2611
|
const d = parsed.data;
|
|
2312
2612
|
const pages = d.pages ?? [];
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2613
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
2614
|
+
const pageRow = (p, i) => {
|
|
2615
|
+
const schemaInfo = schemaTypesOf(p).join(", ") || "\u2014";
|
|
2315
2616
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | ${schemaInfo} |`;
|
|
2316
|
-
}
|
|
2317
|
-
const
|
|
2617
|
+
};
|
|
2618
|
+
const tips = `
|
|
2619
|
+
---
|
|
2620
|
+
\u{1F4A1} **Tips**
|
|
2621
|
+
- Map URLs first: use \`map_site_urls\`
|
|
2622
|
+
- Inspect a single page: use \`extract_url\``;
|
|
2623
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
2624
|
+
const structuredContent = {
|
|
2625
|
+
url: input.url,
|
|
2626
|
+
pageCount: pages.length,
|
|
2627
|
+
pages: pages.map((p) => ({
|
|
2628
|
+
url: String(p.url ?? ""),
|
|
2629
|
+
title: p.title ?? null,
|
|
2630
|
+
schemaTypes: schemaTypesOf(p)
|
|
2631
|
+
})),
|
|
2632
|
+
durationMs: d.durationMs ?? 0
|
|
2633
|
+
};
|
|
2634
|
+
if (pages.length > BULK_PAGE_THRESHOLD) {
|
|
2635
|
+
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
2636
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
2637
|
+
url: String(p.url ?? ""),
|
|
2638
|
+
title: p.title ?? null,
|
|
2639
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
2640
|
+
metaDescription: p.metaDescription ?? null,
|
|
2641
|
+
schemaTypes: schemaTypesOf(p)
|
|
2642
|
+
})), seo);
|
|
2643
|
+
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
2644
|
+
const seoLine = bulk?.seoFiles?.length ? `
|
|
2645
|
+
- **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)` : "";
|
|
2646
|
+
const location = bulk ? `
|
|
2647
|
+
## \u{1F4C1} Bulk scrape saved
|
|
2648
|
+
- **Folder:** \`${bulk.dir}\`
|
|
2649
|
+
- **Index:** \`${bulk.indexFile}\`
|
|
2650
|
+
- **Files:** ${bulk.fileCount} page files under \`pages/\`${seoLine}
|
|
2651
|
+
|
|
2652
|
+
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.` : `
|
|
2653
|
+
## \u26A0\uFE0F Bulk scrape not saved
|
|
2654
|
+
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.`;
|
|
2655
|
+
const full2 = [
|
|
2656
|
+
`# Site Extract: ${input.url}`,
|
|
2657
|
+
durationLine,
|
|
2658
|
+
location,
|
|
2659
|
+
`
|
|
2660
|
+
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
2661
|
+
| # | Title | URL | Schema |
|
|
2662
|
+
|---|-------|-----|--------|
|
|
2663
|
+
${preview}`,
|
|
2664
|
+
tips
|
|
2665
|
+
].join("\n");
|
|
2666
|
+
return { content: [{ type: "text", text: full2 }], structuredContent: { ...structuredContent, bulkFolder: bulk?.dir ?? null } };
|
|
2667
|
+
}
|
|
2668
|
+
const header = [
|
|
2318
2669
|
`# Site Extract: ${input.url}`,
|
|
2319
|
-
|
|
2670
|
+
durationLine,
|
|
2320
2671
|
`
|
|
2321
2672
|
## Pages
|
|
2322
2673
|
| # | Title | URL | Schema |
|
|
2323
2674
|
|---|-------|-----|--------|
|
|
2324
|
-
${
|
|
2325
|
-
`
|
|
2326
|
-
---
|
|
2327
|
-
\u{1F4A1} **Tips**
|
|
2328
|
-
- Map URLs first: use \`map_site_urls\`
|
|
2329
|
-
- Inspect a single page: use \`extract_url\``
|
|
2675
|
+
${pages.map(pageRow).join("\n")}`
|
|
2330
2676
|
].join("\n");
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
}
|
|
2677
|
+
const full = `${header}${tips}`;
|
|
2678
|
+
const pageDetails = pages.map((p, i) => {
|
|
2679
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
2680
|
+
return [
|
|
2681
|
+
`
|
|
2682
|
+
## ${i + 1}. ${p.title ?? "Untitled"}`,
|
|
2683
|
+
`- **URL:** ${p.url}`,
|
|
2684
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
2685
|
+
body ? `
|
|
2686
|
+
${body}` : "_(no content extracted)_"
|
|
2687
|
+
].filter(Boolean).join("\n");
|
|
2688
|
+
}).join("\n");
|
|
2689
|
+
const diskReport = `${header}
|
|
2690
|
+
|
|
2691
|
+
---
|
|
2692
|
+
# Full Page Content
|
|
2693
|
+
${pageDetails}${tips}`;
|
|
2694
|
+
return { ...oneBlock(full, diskReport), structuredContent };
|
|
2344
2695
|
}
|
|
2345
2696
|
function formatYoutubeHarvest(raw, input) {
|
|
2346
2697
|
const parsed = parseData(raw);
|
|
@@ -3427,6 +3778,33 @@ ${limitations.map((l) => `- ${l}`).join("\n")}` : "",
|
|
|
3427
3778
|
};
|
|
3428
3779
|
}
|
|
3429
3780
|
|
|
3781
|
+
// src/mcp/server-instructions.ts
|
|
3782
|
+
var SERVER_INSTRUCTIONS = `
|
|
3783
|
+
This server scrapes and analyzes web, social, search, maps, and site data. Pick a tool by NAME using
|
|
3784
|
+
this routing map, then load its schema before calling.
|
|
3785
|
+
|
|
3786
|
+
ROUTING
|
|
3787
|
+
- One web page -> extract_url. Whole site (crawl + SEO report) -> extract_site. Just the URL list -> map_site_urls.
|
|
3788
|
+
- Web search (organic SERP) -> search_serp. Full SERP + People-Also-Ask / AI-Overview detail -> harvest_paa.
|
|
3789
|
+
- Google Maps: find places -> maps_search; one place deep-dive + reviews -> maps_place_intel.
|
|
3790
|
+
- YouTube: find or list videos -> youtube_harvest; transcribe one video -> youtube_transcribe.
|
|
3791
|
+
- Facebook: find ads -> facebook_ad_search; transcribe an ad -> facebook_ad_transcribe; transcribe a
|
|
3792
|
+
video -> facebook_video_transcribe; page profile/intel -> facebook_page_intel.
|
|
3793
|
+
- Instagram: profile inventory -> instagram_profile_content; one post or reel -> instagram_media_download.
|
|
3794
|
+
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
3795
|
+
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
3796
|
+
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
3797
|
+
(browser_open, then navigate/read); save/reuse logins via browser_profile_*.
|
|
3798
|
+
|
|
3799
|
+
NOTES
|
|
3800
|
+
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
3801
|
+
It discovers URLs and returns a saved folder/artifact plus a summary, not the full content inline.
|
|
3802
|
+
- Tools that open a browser session or transcribe media cost credits and take longer. Prefer the
|
|
3803
|
+
cheapest tool that answers the question (plain search/extract before browser agents).
|
|
3804
|
+
- Large results are saved to disk or an artifact and returned as a summary plus a path; read the path
|
|
3805
|
+
for full detail rather than expecting the whole payload inline.
|
|
3806
|
+
`.trim();
|
|
3807
|
+
|
|
3430
3808
|
// src/mcp/mcp-tool-schemas.ts
|
|
3431
3809
|
var import_zod3 = require("zod");
|
|
3432
3810
|
|
|
@@ -3551,11 +3929,14 @@ var ExtractUrlInputSchema = {
|
|
|
3551
3929
|
};
|
|
3552
3930
|
var MapSiteUrlsInputSchema = {
|
|
3553
3931
|
url: import_zod3.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."),
|
|
3554
|
-
maxUrls: import_zod3.z.number().int().min(1).max(
|
|
3932
|
+
maxUrls: import_zod3.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.")
|
|
3555
3933
|
};
|
|
3556
3934
|
var ExtractSiteInputSchema = {
|
|
3557
3935
|
url: import_zod3.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."),
|
|
3558
|
-
maxPages: import_zod3.z.number().int().min(1).max(
|
|
3936
|
+
maxPages: import_zod3.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."),
|
|
3937
|
+
rotateProxies: import_zod3.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."),
|
|
3938
|
+
rotateProxyEvery: import_zod3.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."),
|
|
3939
|
+
formats: import_zod3.z.array(import_zod3.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.")
|
|
3559
3940
|
};
|
|
3560
3941
|
var YoutubeHarvestInputSchema = {
|
|
3561
3942
|
mode: import_zod3.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
@@ -4687,14 +5068,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
4687
5068
|
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
4688
5069
|
server2.registerTool("map_site_urls", {
|
|
4689
5070
|
title: "Site URL Map",
|
|
4690
|
-
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."),
|
|
5071
|
+
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."),
|
|
4691
5072
|
inputSchema: MapSiteUrlsInputSchema,
|
|
4692
5073
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
4693
5074
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
4694
5075
|
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
4695
5076
|
server2.registerTool("extract_site", {
|
|
4696
5077
|
title: "Multi-Page Site Extract",
|
|
4697
|
-
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."),
|
|
5078
|
+
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."),
|
|
4698
5079
|
inputSchema: ExtractSiteInputSchema,
|
|
4699
5080
|
outputSchema: ExtractSiteOutputSchema,
|
|
4700
5081
|
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
@@ -4818,7 +5199,7 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
4818
5199
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
4819
5200
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
4820
5201
|
}, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
|
|
4821
|
-
server2.registerTool("
|
|
5202
|
+
server2.registerTool("rank_tracker_workflow", {
|
|
4822
5203
|
title: "Rank Tracker Blueprint Builder",
|
|
4823
5204
|
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.",
|
|
4824
5205
|
inputSchema: RankTrackerBlueprintInputSchema,
|
|
@@ -4895,7 +5276,7 @@ function renderInstallTerminal(options) {
|
|
|
4895
5276
|
`${colorize("Tools", "cyan", color)} ${colorize("(44 MCP tools)", "muted", color)}`,
|
|
4896
5277
|
toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
|
|
4897
5278
|
toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "directory_workflow"], color),
|
|
4898
|
-
toolRow("build", ["
|
|
5279
|
+
toolRow("build", ["rank_tracker_workflow", "cron plan", "database prompt"], color),
|
|
4899
5280
|
toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe", "instagram_profile_content", "instagram_media_download"], color),
|
|
4900
5281
|
toolRow("browser", ["browser_open", "browser_profile_onboard", "browser_profile_status", "browser_close", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
|
|
4901
5282
|
toolRow("account", ["credits_info", "reports", "MCP resources"], color),
|
|
@@ -4971,7 +5352,7 @@ if (!apiKey) {
|
|
|
4971
5352
|
}
|
|
4972
5353
|
var baseUrl = process.env.MCP_SCRAPER_BASE_URL?.trim() || process.env.MCP_BASE_URL?.trim() || "https://mcpscraper.dev";
|
|
4973
5354
|
var consoleBaseUrl = process.env.BROWSER_AGENT_CONSOLE_URL?.trim() || baseUrl;
|
|
4974
|
-
var server = new import_mcp3.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
5355
|
+
var server = new import_mcp3.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
4975
5356
|
registerPaaExtractorMcpTools(server, new HttpMcpToolExecutor(baseUrl, apiKey));
|
|
4976
5357
|
registerBrowserAgentMcpTools(server, { baseUrl, apiKey, consoleBaseUrl });
|
|
4977
5358
|
var transport = new import_stdio.StdioServerTransport();
|