scrapeunblocker-mcp 0.2.0 → 0.2.2
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/index.js +61 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { ScrapeUnblockerClient } from "scrapeunblocker";
|
|
8
|
-
var VERSION = "0.2.
|
|
7
|
+
import { APIError, ScrapeUnblockerClient } from "scrapeunblocker";
|
|
8
|
+
var VERSION = "0.2.2";
|
|
9
9
|
var DEFAULT_BASE_URL = "https://api.scrapeunblocker.com";
|
|
10
10
|
var API_KEY_HEADER = "x-scrapeunblocker-key";
|
|
11
11
|
function resolveApiKey() {
|
|
@@ -28,6 +28,46 @@ function errorText(err) {
|
|
|
28
28
|
if (err instanceof Error) return err.message;
|
|
29
29
|
return String(err);
|
|
30
30
|
}
|
|
31
|
+
var TARGET_GONE = /* @__PURE__ */ new Set([404, 410]);
|
|
32
|
+
function targetGoneStatus(status, originStatus) {
|
|
33
|
+
const origin = Number(originStatus);
|
|
34
|
+
if (TARGET_GONE.has(origin)) return origin;
|
|
35
|
+
return TARGET_GONE.has(status) ? status : null;
|
|
36
|
+
}
|
|
37
|
+
function targetGoneText(url, code, page) {
|
|
38
|
+
const reason = code === 410 ? "Gone" : "Not Found";
|
|
39
|
+
const head = `The target page does not exist: ${url} answered HTTP ${code} (${reason}). This is the website's own answer, not a block or an API failure. The call was billed, and retrying returns the same result - check the URL instead.`;
|
|
40
|
+
return page.trim() ? `${head}
|
|
41
|
+
|
|
42
|
+
The target's own ${code} page follows:
|
|
43
|
+
|
|
44
|
+
${page}` : `${head}
|
|
45
|
+
|
|
46
|
+
The target sent no page body.`;
|
|
47
|
+
}
|
|
48
|
+
function isNoDataExtracted(status, body) {
|
|
49
|
+
if (status !== 422 || !body) return false;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(body)?.error === "no_data_extracted";
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function noDataExtractedText(url) {
|
|
57
|
+
return `No structured data could be extracted from ${url}: the page loaded, but nothing on it matched a structured shape. This call was not billed. To get the page itself, call fetch_html for the same URL.`;
|
|
58
|
+
}
|
|
59
|
+
function errorResult(url, err) {
|
|
60
|
+
if (err instanceof APIError) {
|
|
61
|
+
const gone = targetGoneStatus(err.statusCode);
|
|
62
|
+
if (gone !== null) {
|
|
63
|
+
return { content: [{ type: "text", text: targetGoneText(url, gone, err.body ?? "") }] };
|
|
64
|
+
}
|
|
65
|
+
if (isNoDataExtracted(err.statusCode, err.body)) {
|
|
66
|
+
return { content: [{ type: "text", text: noDataExtractedText(url) }] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { content: [{ type: "text", text: errorText(err) }], isError: true };
|
|
70
|
+
}
|
|
31
71
|
async function getPageSourceRaw(params) {
|
|
32
72
|
const apiKey = resolveApiKey();
|
|
33
73
|
if (!apiKey) throw new Error(MISSING_KEY_MESSAGE);
|
|
@@ -46,7 +86,12 @@ async function getPageSourceRaw(params) {
|
|
|
46
86
|
}
|
|
47
87
|
});
|
|
48
88
|
const text = await response.text();
|
|
49
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
status: response.status,
|
|
91
|
+
ok: response.ok,
|
|
92
|
+
text,
|
|
93
|
+
originStatus: response.headers.get("x-origin-status")
|
|
94
|
+
};
|
|
50
95
|
}
|
|
51
96
|
var server = new McpServer({
|
|
52
97
|
name: "scrapeunblocker",
|
|
@@ -159,6 +204,10 @@ server.registerTool(
|
|
|
159
204
|
time_sleep: args.sleep_seconds,
|
|
160
205
|
steps: JSON.stringify(args.steps)
|
|
161
206
|
});
|
|
207
|
+
const gone = targetGoneStatus(res.status, res.originStatus);
|
|
208
|
+
if (gone !== null) {
|
|
209
|
+
return { content: [{ type: "text", text: targetGoneText(args.url, gone, res.text) }] };
|
|
210
|
+
}
|
|
162
211
|
if (res.ok) {
|
|
163
212
|
return { content: [{ type: "text", text: res.text }] };
|
|
164
213
|
}
|
|
@@ -192,7 +241,7 @@ Fix the selector or step (use list_elements to inspect the page) and retry. The
|
|
|
192
241
|
isError: true
|
|
193
242
|
};
|
|
194
243
|
} catch (err) {
|
|
195
|
-
return
|
|
244
|
+
return errorResult(args.url, err);
|
|
196
245
|
}
|
|
197
246
|
}
|
|
198
247
|
);
|
|
@@ -213,7 +262,8 @@ server.registerTool(
|
|
|
213
262
|
list_elements: true,
|
|
214
263
|
proxy_country: args.proxy_country
|
|
215
264
|
});
|
|
216
|
-
|
|
265
|
+
const gone = targetGoneStatus(res.status, res.originStatus);
|
|
266
|
+
if (!res.ok && gone === null) {
|
|
217
267
|
return {
|
|
218
268
|
content: [
|
|
219
269
|
{
|
|
@@ -229,9 +279,12 @@ server.registerTool(
|
|
|
229
279
|
out = JSON.stringify(JSON.parse(res.text), null, 2);
|
|
230
280
|
} catch {
|
|
231
281
|
}
|
|
282
|
+
if (gone !== null) {
|
|
283
|
+
return { content: [{ type: "text", text: targetGoneText(args.url, gone, out) }] };
|
|
284
|
+
}
|
|
232
285
|
return { content: [{ type: "text", text: out }] };
|
|
233
286
|
} catch (err) {
|
|
234
|
-
return
|
|
287
|
+
return errorResult(args.url, err);
|
|
235
288
|
}
|
|
236
289
|
}
|
|
237
290
|
);
|
|
@@ -239,7 +292,7 @@ server.registerTool(
|
|
|
239
292
|
"fetch_parsed",
|
|
240
293
|
{
|
|
241
294
|
title: "Fetch AI-parsed page data",
|
|
242
|
-
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.",
|
|
295
|
+
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 (that call is not billed) - use fetch_html for the page itself.",
|
|
243
296
|
inputSchema: {
|
|
244
297
|
url: z.string().url().describe("The absolute URL to fetch and parse."),
|
|
245
298
|
proxy_country: z.string().length(2).optional().describe("Optional ISO country code to route through, e.g. 'US'."),
|
|
@@ -258,7 +311,7 @@ server.registerTool(
|
|
|
258
311
|
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
|
|
259
312
|
};
|
|
260
313
|
} catch (err) {
|
|
261
|
-
return
|
|
314
|
+
return errorResult(args.url, err);
|
|
262
315
|
}
|
|
263
316
|
}
|
|
264
317
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapeunblocker-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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) through the ScrapeUnblocker anti-bot API, using your own API key.",
|
|
6
6
|
"keywords": [
|