scrapeunblocker-mcp 0.4.0 → 0.5.0
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/README.md +4 -0
- package/dist/index.js +355 -53
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ You bring your **own** API key. Nothing is shared or proxied through us.
|
|
|
16
16
|
| `list_elements` | List a page's notable elements with a ready-to-use selector for each (selector discovery). |
|
|
17
17
|
| `fetch_parsed` | Fetch a page and return AI-parsed structured JSON. |
|
|
18
18
|
| `google_search` | Run a Google search and return organic results as JSON. |
|
|
19
|
+
| `fetch_markdown` | Fetch a page and return it as clean Markdown: the main content with absolute links, without scripts, styles, navigation or footer. Usually 2-25% the size of the HTML, so it is the cheap way for a model to read a page. |
|
|
20
|
+
| `search_and_read` | Search Google and read the top results in one call: Google's AI Overview (when there is one), the first 1-5 result pages as Markdown, and the other results as title, URL and snippet. Billed as one search plus one request per page read. |
|
|
19
21
|
| `list_plugins` | List the ready-made site plugins (100+: Amazon, Walmart, Zillow, Booking.com, Indeed, Google Trends, YouTube, App Store, Ahrefs, ...) with their parameters. Free - it calls nothing. |
|
|
20
22
|
| `run_plugin` | Run one site plugin and return its structured JSON. |
|
|
21
23
|
| `amazon_search`, `amazon_product`, `amazon_reviews`, `walmart_search`, `ebay_search`, `google_shopping`, `google_local`, `maps_place_details`, `google_images`, `youtube_search`, `youtube_transcript`, `zillow_search`, `indeed_jobs`, `trustpilot_reviews`, `booking_hotels` | The most-used site plugins as tools of their own, each with its parameters in the tool schema - one call, no `list_plugins` lookup. Same JSON as `run_plugin`. |
|
|
@@ -140,6 +142,8 @@ SCRAPEUNBLOCKER_KEY=your_api_key_here npx -y scrapeunblocker-mcp
|
|
|
140
142
|
- "This page keeps blocking me: <url>. Use fetch_html to get it."
|
|
141
143
|
- "List the elements on <url>, then use fetch_html steps to type 'laptop' into the search box, press Enter, wait for the results, and give me the HTML."
|
|
142
144
|
- "Search Google for 'best running shoes 2026' and give me the top 5 links."
|
|
145
|
+
- "Read https://en.wikipedia.org/wiki/Web_scraping as Markdown and summarise it in five bullets."
|
|
146
|
+
- "Search the web for the current population of Vilnius and cite the pages you read."
|
|
143
147
|
- "Find coffee makers on Target under $50 and compare them with Walmart's."
|
|
144
148
|
- "Show Google Trends for 'coffee' vs 'tea' in the US over the last year."
|
|
145
149
|
- "What are the newest App Store reviews of WhatsApp saying?"
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { z as
|
|
6
|
+
import { z as z3 } from "zod";
|
|
7
7
|
import { APIError, ScrapeUnblockerClient } from "scrapeunblocker";
|
|
8
8
|
|
|
9
9
|
// src/plugin-tools.ts
|
|
@@ -5686,8 +5686,300 @@ ${details(plugin)}`, true);
|
|
|
5686
5686
|
}
|
|
5687
5687
|
}
|
|
5688
5688
|
|
|
5689
|
+
// src/reading-tools.ts
|
|
5690
|
+
import { z as z2 } from "zod";
|
|
5691
|
+
|
|
5692
|
+
// src/markdown.ts
|
|
5693
|
+
import { parseHTML } from "linkedom";
|
|
5694
|
+
import TurndownService from "turndown";
|
|
5695
|
+
import { gfm } from "turndown-plugin-gfm";
|
|
5696
|
+
var ALWAYS_DROP = [
|
|
5697
|
+
"script",
|
|
5698
|
+
"style",
|
|
5699
|
+
"noscript",
|
|
5700
|
+
"template",
|
|
5701
|
+
"svg",
|
|
5702
|
+
"canvas",
|
|
5703
|
+
"iframe",
|
|
5704
|
+
"object",
|
|
5705
|
+
"embed",
|
|
5706
|
+
"link",
|
|
5707
|
+
"meta",
|
|
5708
|
+
"head",
|
|
5709
|
+
"button",
|
|
5710
|
+
"input",
|
|
5711
|
+
"select",
|
|
5712
|
+
"textarea",
|
|
5713
|
+
"dialog",
|
|
5714
|
+
"[hidden]",
|
|
5715
|
+
'[aria-hidden="true"]'
|
|
5716
|
+
].join(",");
|
|
5717
|
+
var CHROME = ["nav", "footer", "aside", '[role="navigation"]', '[role="contentinfo"]', '[role="complementary"]'].join(",");
|
|
5718
|
+
var MAIN_MIN_SHARE = 0.3;
|
|
5719
|
+
function textLength(el) {
|
|
5720
|
+
return (el?.textContent ?? "").replace(/\s+/g, " ").trim().length;
|
|
5721
|
+
}
|
|
5722
|
+
function absolute(href, base) {
|
|
5723
|
+
try {
|
|
5724
|
+
const url = new URL(href, base);
|
|
5725
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : null;
|
|
5726
|
+
} catch {
|
|
5727
|
+
return null;
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
function converter() {
|
|
5731
|
+
const td = new TurndownService({
|
|
5732
|
+
headingStyle: "atx",
|
|
5733
|
+
codeBlockStyle: "fenced",
|
|
5734
|
+
bulletListMarker: "-",
|
|
5735
|
+
emDelimiter: "_"
|
|
5736
|
+
});
|
|
5737
|
+
td.use(gfm);
|
|
5738
|
+
td.addRule("images", {
|
|
5739
|
+
filter: "img",
|
|
5740
|
+
replacement: (_content, node) => {
|
|
5741
|
+
const el = node;
|
|
5742
|
+
const alt = (el.getAttribute("alt") || "").replace(/\s+/g, " ").trim();
|
|
5743
|
+
const src = el.getAttribute("src") || "";
|
|
5744
|
+
return alt && /^https?:/i.test(src) ? `` : "";
|
|
5745
|
+
}
|
|
5746
|
+
});
|
|
5747
|
+
td.addRule("flatCells", {
|
|
5748
|
+
filter: ["th", "td"],
|
|
5749
|
+
replacement: (content, node) => {
|
|
5750
|
+
const cell = node;
|
|
5751
|
+
const first = Array.prototype.indexOf.call(cell.parentNode.childNodes, cell) === 0;
|
|
5752
|
+
const text3 = content.replace(/\s*\n+\s*/g, " ").replace(/\|/g, "\\|").trim();
|
|
5753
|
+
return `${first ? "| " : " "}${text3} |`;
|
|
5754
|
+
}
|
|
5755
|
+
});
|
|
5756
|
+
return td;
|
|
5757
|
+
}
|
|
5758
|
+
function normalizeTables(document) {
|
|
5759
|
+
for (const table of [...document.querySelectorAll("table")].reverse()) {
|
|
5760
|
+
const rows = [...table.querySelectorAll("tr")].filter((tr) => tr.closest("table") === table).map((tr) => [...tr.children].filter((c) => c.tagName === "TD" || c.tagName === "TH")).filter((cells) => cells.length > 0);
|
|
5761
|
+
if (rows.length === 0) {
|
|
5762
|
+
table.remove();
|
|
5763
|
+
continue;
|
|
5764
|
+
}
|
|
5765
|
+
if (rows.length === 1 || Math.max(...rows.map((cells) => cells.length)) === 1) {
|
|
5766
|
+
const blocks = document.createElement("div");
|
|
5767
|
+
for (const [cell] of rows) {
|
|
5768
|
+
const block = document.createElement("div");
|
|
5769
|
+
block.innerHTML = cell.innerHTML;
|
|
5770
|
+
blocks.appendChild(block);
|
|
5771
|
+
}
|
|
5772
|
+
table.replaceWith(blocks);
|
|
5773
|
+
continue;
|
|
5774
|
+
}
|
|
5775
|
+
const clean = document.createElement("table");
|
|
5776
|
+
rows.forEach((cells, i) => {
|
|
5777
|
+
const section = document.createElement(i === 0 ? "thead" : "tbody");
|
|
5778
|
+
const tr = document.createElement("tr");
|
|
5779
|
+
for (const cell of cells) {
|
|
5780
|
+
const out = document.createElement(i === 0 ? "th" : "td");
|
|
5781
|
+
out.innerHTML = cell.innerHTML.replace(/\s+/g, " ");
|
|
5782
|
+
tr.appendChild(out);
|
|
5783
|
+
}
|
|
5784
|
+
if (i === 0 || !clean.lastElementChild || clean.lastElementChild.tagName !== "TBODY") {
|
|
5785
|
+
section.appendChild(tr);
|
|
5786
|
+
clean.appendChild(section);
|
|
5787
|
+
} else {
|
|
5788
|
+
clean.lastElementChild.appendChild(tr);
|
|
5789
|
+
}
|
|
5790
|
+
});
|
|
5791
|
+
table.replaceWith(clean);
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
function htmlToMarkdown(html, url, options) {
|
|
5795
|
+
if (!/<[a-z!][\s\S]*>/i.test(html)) return { title: "", markdown: html.trim() };
|
|
5796
|
+
const { document } = parseHTML(html);
|
|
5797
|
+
const title = (document.querySelector("title")?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
5798
|
+
for (const el of document.querySelectorAll(ALWAYS_DROP)) el.remove();
|
|
5799
|
+
for (const a of document.querySelectorAll("a[href]")) {
|
|
5800
|
+
const href = absolute(a.getAttribute("href") || "", url);
|
|
5801
|
+
if (href) a.setAttribute("href", href);
|
|
5802
|
+
else a.removeAttribute("href");
|
|
5803
|
+
}
|
|
5804
|
+
for (const img of document.querySelectorAll("img")) {
|
|
5805
|
+
const src = absolute(img.getAttribute("src") || "", url);
|
|
5806
|
+
if (src) img.setAttribute("src", src);
|
|
5807
|
+
else img.removeAttribute("src");
|
|
5808
|
+
}
|
|
5809
|
+
const body = document.body ?? document.documentElement;
|
|
5810
|
+
let root = body;
|
|
5811
|
+
if (options.mainOnly) {
|
|
5812
|
+
const main2 = document.querySelector('main, [role="main"]');
|
|
5813
|
+
if (main2 && textLength(main2) >= MAIN_MIN_SHARE * textLength(body)) {
|
|
5814
|
+
root = main2;
|
|
5815
|
+
} else {
|
|
5816
|
+
for (const el of document.querySelectorAll(CHROME)) el.remove();
|
|
5817
|
+
for (const header of document.querySelectorAll('header, [role="banner"]')) {
|
|
5818
|
+
if (!header.closest("article, main")) header.remove();
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
normalizeTables(document);
|
|
5823
|
+
let converted;
|
|
5824
|
+
try {
|
|
5825
|
+
converted = converter().turndown(root.innerHTML);
|
|
5826
|
+
} catch {
|
|
5827
|
+
converted = (root.textContent ?? "").replace(/[ \t]+/g, " ").replace(/\s*\n\s*/g, "\n");
|
|
5828
|
+
}
|
|
5829
|
+
const markdown = converted.replace(/\[\s*\]\([^)]*\)/g, "").replace(/^(\s*)([-*+]|\d+\.) {2,}/gm, "$1$2 ").replace(/(\)|\*\*|_) ([.,;:!?])/g, "$1$2").replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
5830
|
+
return { title, markdown };
|
|
5831
|
+
}
|
|
5832
|
+
function capText(text3, max, hint) {
|
|
5833
|
+
if (text3.length <= max) return text3;
|
|
5834
|
+
return `${text3.slice(0, max)}
|
|
5835
|
+
|
|
5836
|
+
[Cut at ${max} of ${text3.length} characters. ${hint}]`;
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
// src/reading-tools.ts
|
|
5840
|
+
var PAGE_MAX_CHARS = 1e5;
|
|
5841
|
+
var SEARCH_PAGE_MAX_CHARS = 15e3;
|
|
5842
|
+
var SEARCH_DEFAULT_RESULTS = 3;
|
|
5843
|
+
var SEARCH_MAX_RESULTS = 5;
|
|
5844
|
+
var SEARCH_PAGE_DEADLINE_MS = 12e4;
|
|
5845
|
+
function text2(body, isError = false) {
|
|
5846
|
+
return isError ? { content: [{ type: "text", text: body }], isError } : { content: [{ type: "text", text: body }] };
|
|
5847
|
+
}
|
|
5848
|
+
function pageMarkdown(url, html, mainOnly, max) {
|
|
5849
|
+
const page = htmlToMarkdown(html, url, { mainOnly });
|
|
5850
|
+
const head = page.title ? `URL: ${url}
|
|
5851
|
+
Title: ${page.title}` : `URL: ${url}`;
|
|
5852
|
+
return capText(`${head}
|
|
5853
|
+
|
|
5854
|
+
${page.markdown}`, max, "Use fetch_html for the whole page.");
|
|
5855
|
+
}
|
|
5856
|
+
function organicResults(serp) {
|
|
5857
|
+
const organic = serp?.organic;
|
|
5858
|
+
if (!Array.isArray(organic)) return [];
|
|
5859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5860
|
+
const out = [];
|
|
5861
|
+
for (const item of organic) {
|
|
5862
|
+
const url = typeof item?.url === "string" ? item.url : "";
|
|
5863
|
+
if (!/^https?:\/\//i.test(url) || seen.has(url)) continue;
|
|
5864
|
+
seen.add(url);
|
|
5865
|
+
out.push({
|
|
5866
|
+
title: typeof item.title === "string" ? item.title : url,
|
|
5867
|
+
url,
|
|
5868
|
+
description: typeof item.description === "string" ? item.description : ""
|
|
5869
|
+
});
|
|
5870
|
+
}
|
|
5871
|
+
return out;
|
|
5872
|
+
}
|
|
5873
|
+
function aiOverview(serp) {
|
|
5874
|
+
const overview = serp?.aiOverview;
|
|
5875
|
+
if (!overview || typeof overview.text !== "string" || !overview.text.trim()) return null;
|
|
5876
|
+
const sources = Array.isArray(overview.sources) ? overview.sources.filter((s) => typeof s === "string") : [];
|
|
5877
|
+
return sources.length ? `${overview.text.trim()}
|
|
5878
|
+
|
|
5879
|
+
Sources: ${sources.join(", ")}` : overview.text.trim();
|
|
5880
|
+
}
|
|
5881
|
+
function withDeadline(work, ms) {
|
|
5882
|
+
let timer;
|
|
5883
|
+
const deadline = new Promise((_, reject) => {
|
|
5884
|
+
timer = setTimeout(() => reject(new Error(`no answer within ${ms / 1e3} s`)), ms);
|
|
5885
|
+
});
|
|
5886
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
5887
|
+
}
|
|
5888
|
+
var proxyCountry = z2.string().length(2).optional().describe("Optional ISO country code to route through, e.g. 'US'.");
|
|
5889
|
+
function registerReadingTools(server2, options) {
|
|
5890
|
+
const annotations = { readOnlyHint: true, openWorldHint: true, destructiveHint: false };
|
|
5891
|
+
server2.registerTool(
|
|
5892
|
+
"fetch_markdown",
|
|
5893
|
+
{
|
|
5894
|
+
title: "Fetch page as Markdown",
|
|
5895
|
+
annotations: { title: "Fetch page as Markdown", ...annotations },
|
|
5896
|
+
description: "Fetch a web page in a real browser through the ScrapeUnblocker API (https://docs.scrapeunblocker.com) and return it as clean Markdown: the main content with absolute links, without scripts, styles, navigation or footer. Use it to read, quote or summarise a page; it is usually 2-25% the size of the HTML. Works on JavaScript-rendered pages and on pages that refuse ordinary fetches (403/429, captcha). Use fetch_html instead when you need the markup, selectors or browser steps. Billed as one request.",
|
|
5897
|
+
inputSchema: {
|
|
5898
|
+
url: z2.string().url().describe("The absolute URL to read (http/https)."),
|
|
5899
|
+
proxy_country: proxyCountry,
|
|
5900
|
+
wait_method: z2.enum(["css", "js"]).optional().describe("Optional render-wait: 'css' selector or 'js' expression."),
|
|
5901
|
+
wait_value: z2.string().optional().describe("The selector/expression paired with wait_method."),
|
|
5902
|
+
sleep_seconds: z2.number().positive().optional().describe("Extra seconds to wait after load."),
|
|
5903
|
+
main_content_only: z2.boolean().optional().describe("Drop navigation, page header, footer and sidebars (default true). False keeps the whole page.")
|
|
5904
|
+
}
|
|
5905
|
+
},
|
|
5906
|
+
async (args) => {
|
|
5907
|
+
const blocked = options.blocked?.();
|
|
5908
|
+
if (blocked) return blocked;
|
|
5909
|
+
try {
|
|
5910
|
+
const html = await options.fetchHtml(args.url, {
|
|
5911
|
+
proxyCountry: args.proxy_country,
|
|
5912
|
+
method: args.wait_method,
|
|
5913
|
+
value: args.wait_value,
|
|
5914
|
+
timeSleep: args.sleep_seconds
|
|
5915
|
+
});
|
|
5916
|
+
return text2(pageMarkdown(args.url, html, args.main_content_only ?? true, PAGE_MAX_CHARS));
|
|
5917
|
+
} catch (err) {
|
|
5918
|
+
return options.fetchError(args.url, err);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
);
|
|
5922
|
+
server2.registerTool(
|
|
5923
|
+
"search_and_read",
|
|
5924
|
+
{
|
|
5925
|
+
title: "Search Google and read the top results",
|
|
5926
|
+
annotations: { title: "Search Google and read the top results", ...annotations },
|
|
5927
|
+
description: "Search Google through the ScrapeUnblocker API (https://docs.scrapeunblocker.com) and read the top results in one call: returns Google's AI Overview when there is one, each of the first `results` pages (default 3, max 5) as Markdown, and the other organic results as title, URL and snippet. Use it to answer a question from the live web. Billed as one request for the search plus one per page read.",
|
|
5928
|
+
inputSchema: {
|
|
5929
|
+
keyword: z2.string().min(1).describe("The search query."),
|
|
5930
|
+
proxy_country: z2.string().length(2).optional().describe("Optional ISO country code to search from and read the pages through, e.g. 'US'."),
|
|
5931
|
+
results: z2.number().int().min(1).max(SEARCH_MAX_RESULTS).optional().describe(`How many top results to read (default ${SEARCH_DEFAULT_RESULTS}, max ${SEARCH_MAX_RESULTS}).`)
|
|
5932
|
+
}
|
|
5933
|
+
},
|
|
5934
|
+
async (args) => {
|
|
5935
|
+
const blocked = options.blocked?.();
|
|
5936
|
+
if (blocked) return blocked;
|
|
5937
|
+
let serp;
|
|
5938
|
+
try {
|
|
5939
|
+
serp = await options.serp(args.keyword, { proxyCountry: args.proxy_country });
|
|
5940
|
+
} catch (err) {
|
|
5941
|
+
return text2(options.errorText(err), true);
|
|
5942
|
+
}
|
|
5943
|
+
const organic = organicResults(serp);
|
|
5944
|
+
const overview = aiOverview(serp);
|
|
5945
|
+
if (!organic.length && !overview) {
|
|
5946
|
+
return text2(`Google returned no organic results for "${args.keyword}". The search was billed as one request.`);
|
|
5947
|
+
}
|
|
5948
|
+
const toRead = organic.slice(0, args.results ?? SEARCH_DEFAULT_RESULTS);
|
|
5949
|
+
const pages = await Promise.allSettled(
|
|
5950
|
+
toRead.map(
|
|
5951
|
+
(r) => withDeadline(options.fetchHtml(r.url, { proxyCountry: args.proxy_country }), SEARCH_PAGE_DEADLINE_MS)
|
|
5952
|
+
)
|
|
5953
|
+
);
|
|
5954
|
+
const parts = [
|
|
5955
|
+
`Google results for "${args.keyword}": ${toRead.length} read below. Billed as one search plus one request per page read.`
|
|
5956
|
+
];
|
|
5957
|
+
if (overview) parts.push(`## Google AI Overview
|
|
5958
|
+
|
|
5959
|
+
${overview}`);
|
|
5960
|
+
toRead.forEach((r, i) => {
|
|
5961
|
+
const page = pages[i];
|
|
5962
|
+
const body = page.status === "fulfilled" ? pageMarkdown(r.url, page.value, true, SEARCH_PAGE_MAX_CHARS) : `URL: ${r.url}
|
|
5963
|
+
|
|
5964
|
+
Could not read this page: ${options.errorText(page.reason)}
|
|
5965
|
+
Snippet: ${r.description}`;
|
|
5966
|
+
parts.push(`## ${i + 1}. ${r.title}
|
|
5967
|
+
|
|
5968
|
+
${body}`);
|
|
5969
|
+
});
|
|
5970
|
+
const rest = organic.slice(toRead.length);
|
|
5971
|
+
if (rest.length) {
|
|
5972
|
+
parts.push(
|
|
5973
|
+
"## Other results\n\n" + rest.map((r) => `- [${r.title}](${r.url}): ${r.description}`).join("\n")
|
|
5974
|
+
);
|
|
5975
|
+
}
|
|
5976
|
+
return text2(parts.join("\n\n"));
|
|
5977
|
+
}
|
|
5978
|
+
);
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5689
5981
|
// src/index.ts
|
|
5690
|
-
var VERSION = "0.
|
|
5982
|
+
var VERSION = "0.5.0";
|
|
5691
5983
|
var DEFAULT_BASE_URL = "https://api.scrapeunblocker.com";
|
|
5692
5984
|
var API_KEY_HEADER = "x-scrapeunblocker-key";
|
|
5693
5985
|
function resolveApiKey() {
|
|
@@ -5765,11 +6057,11 @@ async function getPageSourceRaw(params) {
|
|
|
5765
6057
|
Accept: "*/*"
|
|
5766
6058
|
}
|
|
5767
6059
|
});
|
|
5768
|
-
const
|
|
6060
|
+
const text3 = await response.text();
|
|
5769
6061
|
return {
|
|
5770
6062
|
status: response.status,
|
|
5771
6063
|
ok: response.ok,
|
|
5772
|
-
text:
|
|
6064
|
+
text: text3,
|
|
5773
6065
|
originStatus: response.headers.get("x-origin-status")
|
|
5774
6066
|
};
|
|
5775
6067
|
}
|
|
@@ -5777,8 +6069,8 @@ var server = new McpServer({
|
|
|
5777
6069
|
name: "scrapeunblocker",
|
|
5778
6070
|
version: VERSION
|
|
5779
6071
|
});
|
|
5780
|
-
var selectorTypeSchema =
|
|
5781
|
-
var pressableKeySchema =
|
|
6072
|
+
var selectorTypeSchema = z3.enum(["css", "xPath", "className", "tagName"]).describe("How `selector` is interpreted. Defaults to 'css'.");
|
|
6073
|
+
var pressableKeySchema = z3.enum([
|
|
5782
6074
|
"Enter",
|
|
5783
6075
|
"Tab",
|
|
5784
6076
|
"Escape",
|
|
@@ -5794,71 +6086,72 @@ var pressableKeySchema = z2.enum([
|
|
|
5794
6086
|
"PageUp",
|
|
5795
6087
|
"PageDown"
|
|
5796
6088
|
]);
|
|
5797
|
-
var stepSchema =
|
|
5798
|
-
|
|
5799
|
-
action:
|
|
5800
|
-
selector:
|
|
6089
|
+
var stepSchema = z3.discriminatedUnion("action", [
|
|
6090
|
+
z3.object({
|
|
6091
|
+
action: z3.literal("wait_for"),
|
|
6092
|
+
selector: z3.string().describe("Selector to wait for (until present)."),
|
|
5801
6093
|
selector_type: selectorTypeSchema.optional(),
|
|
5802
|
-
timeout_ms:
|
|
6094
|
+
timeout_ms: z3.number().int().positive().optional()
|
|
5803
6095
|
}).describe("Wait until an element matching `selector` exists."),
|
|
5804
|
-
|
|
5805
|
-
action:
|
|
5806
|
-
value:
|
|
5807
|
-
timeout_ms:
|
|
6096
|
+
z3.object({
|
|
6097
|
+
action: z3.literal("wait_for_text"),
|
|
6098
|
+
value: z3.string().describe("Text to wait for anywhere on the page."),
|
|
6099
|
+
timeout_ms: z3.number().int().positive().optional()
|
|
5808
6100
|
}).describe("Wait until the given text appears on the page."),
|
|
5809
|
-
|
|
5810
|
-
action:
|
|
5811
|
-
value:
|
|
6101
|
+
z3.object({
|
|
6102
|
+
action: z3.literal("wait"),
|
|
6103
|
+
value: z3.number().int().nonnegative().describe("Fixed pause in milliseconds.")
|
|
5812
6104
|
}).describe("Pause for a fixed number of milliseconds."),
|
|
5813
|
-
|
|
5814
|
-
action:
|
|
5815
|
-
selector:
|
|
6105
|
+
z3.object({
|
|
6106
|
+
action: z3.literal("click"),
|
|
6107
|
+
selector: z3.string().describe("Selector of the element to click."),
|
|
5816
6108
|
selector_type: selectorTypeSchema.optional(),
|
|
5817
|
-
timeout_ms:
|
|
6109
|
+
timeout_ms: z3.number().int().positive().optional()
|
|
5818
6110
|
}).describe("Click the element matching `selector`."),
|
|
5819
|
-
|
|
5820
|
-
action:
|
|
5821
|
-
selector:
|
|
6111
|
+
z3.object({
|
|
6112
|
+
action: z3.literal("type"),
|
|
6113
|
+
selector: z3.string().describe("Selector of the field to type into."),
|
|
5822
6114
|
selector_type: selectorTypeSchema.optional(),
|
|
5823
|
-
value:
|
|
5824
|
-
clear:
|
|
5825
|
-
timeout_ms:
|
|
6115
|
+
value: z3.string().describe("Text to type (entered human-like)."),
|
|
6116
|
+
clear: z3.boolean().optional().describe("Clear the field before typing."),
|
|
6117
|
+
timeout_ms: z3.number().int().positive().optional()
|
|
5826
6118
|
}).describe("Type text into an input, character by character."),
|
|
5827
|
-
|
|
5828
|
-
action:
|
|
5829
|
-
selector:
|
|
6119
|
+
z3.object({
|
|
6120
|
+
action: z3.literal("select"),
|
|
6121
|
+
selector: z3.string().describe("Selector of the <select> element."),
|
|
5830
6122
|
selector_type: selectorTypeSchema.optional(),
|
|
5831
|
-
value:
|
|
5832
|
-
timeout_ms:
|
|
6123
|
+
value: z3.string().describe("The option value to select."),
|
|
6124
|
+
timeout_ms: z3.number().int().positive().optional()
|
|
5833
6125
|
}).describe("Choose an option in a <select> dropdown by value."),
|
|
5834
|
-
|
|
5835
|
-
action:
|
|
6126
|
+
z3.object({
|
|
6127
|
+
action: z3.literal("press_key"),
|
|
5836
6128
|
value: pressableKeySchema.describe("The key to press.")
|
|
5837
6129
|
}).describe("Press a single keyboard key."),
|
|
5838
|
-
|
|
5839
|
-
action:
|
|
5840
|
-
value:
|
|
6130
|
+
z3.object({
|
|
6131
|
+
action: z3.literal("scroll"),
|
|
6132
|
+
value: z3.union([z3.literal("bottom"), z3.number().int()]).describe("'bottom' to scroll to the end, or a pixel offset.")
|
|
5841
6133
|
}).describe("Scroll the page to the bottom or by a pixel amount.")
|
|
5842
6134
|
]);
|
|
5843
6135
|
server.registerTool(
|
|
5844
6136
|
"fetch_html",
|
|
5845
6137
|
{
|
|
5846
6138
|
title: "Fetch page HTML",
|
|
6139
|
+
annotations: { title: "Fetch page HTML", readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
5847
6140
|
description: "Fetch the fully rendered HTML of any web page through ScrapeUnblocker, bypassing anti-bot protection (Cloudflare, DataDome, PerimeterX, Akamai, Shape). Use this when a normal fetch is blocked (403/429, captcha, 'access denied') or when the page needs a real browser to render. Returns the raw HTML as text.\n\nFor pages that need interaction (accept a cookie banner, click a tab, type into a search box, scroll to trigger lazy loading) pass `steps`: an ordered list of browser actions run in a real browser AFTER the page loads, then the resulting HTML is returned. Workflow: first call the `list_elements` tool to discover the real selectors on the page, then build `steps` against them. Steps are NOT idempotent - they run once per call. If a step fails, this tool returns which step failed, why, and the page HTML at that moment so you can fix the selector and retry.",
|
|
5848
6141
|
inputSchema: {
|
|
5849
|
-
url:
|
|
5850
|
-
proxy_country:
|
|
6142
|
+
url: z3.string().url().describe("The absolute URL to fetch (http/https)."),
|
|
6143
|
+
proxy_country: z3.string().length(2).optional().describe(
|
|
5851
6144
|
"Optional ISO 3166-1 alpha-2 country code to route through, e.g. 'US', 'GB', 'DE'."
|
|
5852
6145
|
),
|
|
5853
|
-
wait_method:
|
|
6146
|
+
wait_method: z3.enum(["css", "js"]).optional().describe(
|
|
5854
6147
|
"Optional render-wait strategy: 'css' waits for a selector, 'js' waits for a JS expression to be truthy."
|
|
5855
6148
|
),
|
|
5856
|
-
wait_value:
|
|
6149
|
+
wait_value: z3.string().optional().describe(
|
|
5857
6150
|
`The CSS selector or JS expression paired with wait_method (e.g. '#price' or 'document.readyState==="complete"').`
|
|
5858
6151
|
),
|
|
5859
|
-
method_timeout_seconds:
|
|
5860
|
-
sleep_seconds:
|
|
5861
|
-
steps:
|
|
6152
|
+
method_timeout_seconds: z3.number().int().positive().optional().describe("Cap in seconds for the render-wait method."),
|
|
6153
|
+
sleep_seconds: z3.number().positive().optional().describe("Extra seconds to wait after load before capturing the HTML."),
|
|
6154
|
+
steps: z3.array(stepSchema).optional().describe(
|
|
5862
6155
|
"Optional ordered browser actions to run in a real browser after the page loads, before the HTML is captured. Use `list_elements` first to find selectors. Runs once (not idempotent)."
|
|
5863
6156
|
)
|
|
5864
6157
|
}
|
|
@@ -5929,10 +6222,11 @@ server.registerTool(
|
|
|
5929
6222
|
"list_elements",
|
|
5930
6223
|
{
|
|
5931
6224
|
title: "List page elements (selector discovery)",
|
|
6225
|
+
annotations: { title: "List page elements (selector discovery)", readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
5932
6226
|
description: "Load a page through ScrapeUnblocker and return a JSON list of its notable elements (links, inputs, buttons, selects, etc.) with a ready-to-use `selector` for each, plus tag, text and useful attributes (name, id, type, placeholder, aria_label, href, ...). Read-only and does not interact with the page. This is the discovery half of interactive scraping: call `list_elements` to find the selectors you need, then pass matching `steps` to `fetch_html` to click/type/select and capture the resulting HTML.",
|
|
5933
6227
|
inputSchema: {
|
|
5934
|
-
url:
|
|
5935
|
-
proxy_country:
|
|
6228
|
+
url: z3.string().url().describe("The absolute URL whose elements you want to list."),
|
|
6229
|
+
proxy_country: z3.string().length(2).optional().describe("Optional ISO country code to route through, e.g. 'US'.")
|
|
5936
6230
|
}
|
|
5937
6231
|
},
|
|
5938
6232
|
async (args) => {
|
|
@@ -5972,11 +6266,12 @@ server.registerTool(
|
|
|
5972
6266
|
"fetch_parsed",
|
|
5973
6267
|
{
|
|
5974
6268
|
title: "Fetch AI-parsed page data",
|
|
6269
|
+
annotations: { title: "Fetch AI-parsed page data", readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
5975
6270
|
description: "Fetch a web page through ScrapeUnblocker and return AI-parsed structured JSON instead of raw HTML (e.g. product details, article content). Best for extracting fields from product, listing or article pages without writing your own HTML parsing. If the page holds no structured data, the result says so and returns the rendered HTML instead (billed like fetch_html).",
|
|
5976
6271
|
inputSchema: {
|
|
5977
|
-
url:
|
|
5978
|
-
proxy_country:
|
|
5979
|
-
rules_hint:
|
|
6272
|
+
url: z3.string().url().describe("The absolute URL to fetch and parse."),
|
|
6273
|
+
proxy_country: z3.string().length(2).optional().describe("Optional ISO country code to route through, e.g. 'US'."),
|
|
6274
|
+
rules_hint: z3.string().optional().describe(
|
|
5980
6275
|
"Optional natural-language hint about what to extract, to guide parsing."
|
|
5981
6276
|
)
|
|
5982
6277
|
}
|
|
@@ -6003,11 +6298,12 @@ server.registerTool(
|
|
|
6003
6298
|
"google_search",
|
|
6004
6299
|
{
|
|
6005
6300
|
title: "Google search results",
|
|
6301
|
+
annotations: { title: "Google search results", readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
6006
6302
|
description: "Run a Google search through ScrapeUnblocker and return the organic results as structured JSON. Use this to discover URLs before fetching them.",
|
|
6007
6303
|
inputSchema: {
|
|
6008
|
-
keyword:
|
|
6009
|
-
proxy_country:
|
|
6010
|
-
pages_to_check:
|
|
6304
|
+
keyword: z3.string().min(1).describe("The search query."),
|
|
6305
|
+
proxy_country: z3.string().length(2).optional().describe("Optional ISO country code to search from, e.g. 'US'."),
|
|
6306
|
+
pages_to_check: z3.number().int().positive().max(10).optional().describe("How many result pages to collect (default 1).")
|
|
6011
6307
|
}
|
|
6012
6308
|
},
|
|
6013
6309
|
async (args) => {
|
|
@@ -6024,6 +6320,12 @@ server.registerTool(
|
|
|
6024
6320
|
}
|
|
6025
6321
|
}
|
|
6026
6322
|
);
|
|
6323
|
+
registerReadingTools(server, {
|
|
6324
|
+
fetchHtml: (url, options) => client().getPageSource(url, options),
|
|
6325
|
+
serp: (keyword, options) => client().serp(keyword, options),
|
|
6326
|
+
fetchError: errorResult,
|
|
6327
|
+
errorText
|
|
6328
|
+
});
|
|
6027
6329
|
registerPluginTools(server, {
|
|
6028
6330
|
call: (path, params) => client().plugin(path, params),
|
|
6029
6331
|
errorText
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapeunblocker-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "com.scrapeunblocker/scrapeunblocker-mcp",
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for ScrapeUnblocker - lets Claude fetch any web page's HTML (or AI-parsed JSON) and run 100+ site plugins that return structured JSON, through the ScrapeUnblocker anti-bot API, using your own API key.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,11 +43,15 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
46
|
+
"linkedom": "^0.18.13",
|
|
46
47
|
"scrapeunblocker": "^0.10.0",
|
|
48
|
+
"turndown": "^7.2.4",
|
|
49
|
+
"turndown-plugin-gfm": "^1.0.2",
|
|
47
50
|
"zod": "^3.23.8"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"@types/node": "^20.19.43",
|
|
54
|
+
"@types/turndown": "^5.0.6",
|
|
51
55
|
"tsup": "^8.0.0",
|
|
52
56
|
"tsx": "^4.16.0",
|
|
53
57
|
"typescript": "^5.4.0"
|