@zenrows/mcp 2.2.4 → 2.4.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/dist/auth/claim-hint.d.ts +12 -0
- package/dist/auth/claim-hint.js +18 -0
- package/dist/batch-api.js +9 -1
- package/dist/server.d.ts +22 -0
- package/dist/server.js +71 -58
- package/dist/tools/account.js +7 -1
- package/dist/tools/batch.js +1 -1
- package/dist/tools/browser-fetch.js +17 -1
- package/package.json +1 -1
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-key credit cap: Fetch, Extract and Browser answer 402 AUTH014, Batch answers
|
|
3
|
+
* 402 `api_key_cap_reached`. The account still has credits and its other keys still
|
|
4
|
+
* work, so "buy credits" or "claim your account" would send the user the wrong way.
|
|
5
|
+
*/
|
|
6
|
+
export declare const KEY_CAP_CODES: ReadonlySet<string>;
|
|
7
|
+
export declare const KEY_CAP_NUDGE = "This API key reached one of its credit caps. The account's other API keys still work. Raise or remove the cap at https://app.zenrows.com/settings/api-keys, or wait until it resets (the error detail says when).";
|
|
8
|
+
export declare function isKeyCapError(opts?: {
|
|
9
|
+
body?: string;
|
|
10
|
+
code?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
}): boolean;
|
|
1
13
|
export declare function isQuotaOrPlanError(opts?: {
|
|
2
14
|
status?: number;
|
|
3
15
|
body?: string;
|
package/dist/auth/claim-hint.js
CHANGED
|
@@ -18,6 +18,13 @@ const CLAIM_NUDGE = "Claim your Free account to keep usage and upgrade: ";
|
|
|
18
18
|
* would otherwise lose the account along with its usage history.
|
|
19
19
|
*/
|
|
20
20
|
const SKIP_CODES = new Set(["AUTH006"]);
|
|
21
|
+
/**
|
|
22
|
+
* A per-key credit cap: Fetch, Extract and Browser answer 402 AUTH014, Batch answers
|
|
23
|
+
* 402 `api_key_cap_reached`. The account still has credits and its other keys still
|
|
24
|
+
* work, so "buy credits" or "claim your account" would send the user the wrong way.
|
|
25
|
+
*/
|
|
26
|
+
export const KEY_CAP_CODES = new Set(["AUTH014", "api_key_cap_reached", "BATCH_KEY_CAP_REACHED"]);
|
|
27
|
+
export const KEY_CAP_NUDGE = "This API key reached one of its credit caps. The account's other API keys still work. Raise or remove the cap at https://app.zenrows.com/settings/api-keys, or wait until it resets (the error detail says when).";
|
|
21
28
|
function extractCode(body, code) {
|
|
22
29
|
if (code && typeof code === "string")
|
|
23
30
|
return code;
|
|
@@ -35,7 +42,15 @@ function extractCode(body, code) {
|
|
|
35
42
|
return m?.[1];
|
|
36
43
|
}
|
|
37
44
|
}
|
|
45
|
+
export function isKeyCapError(opts = {}) {
|
|
46
|
+
const code = extractCode(opts.body, opts.code);
|
|
47
|
+
if (code && KEY_CAP_CODES.has(code))
|
|
48
|
+
return true;
|
|
49
|
+
return /\b(AUTH014|api_key_cap_reached)\b/.test(`${opts.message ?? ""} ${opts.body ?? ""}`);
|
|
50
|
+
}
|
|
38
51
|
export function isQuotaOrPlanError(opts = {}) {
|
|
52
|
+
if (isKeyCapError(opts))
|
|
53
|
+
return false;
|
|
39
54
|
const code = extractCode(opts.body, opts.code);
|
|
40
55
|
if (code && SKIP_CODES.has(code))
|
|
41
56
|
return false;
|
|
@@ -59,6 +74,9 @@ export function appendClaimHint(text, opts = {}) {
|
|
|
59
74
|
code: opts.code,
|
|
60
75
|
message: opts.message ?? text,
|
|
61
76
|
};
|
|
77
|
+
if (isKeyCapError(probe)) {
|
|
78
|
+
return text.includes(KEY_CAP_NUDGE) ? text : `${text}\n\n${KEY_CAP_NUDGE}`;
|
|
79
|
+
}
|
|
62
80
|
if (!isQuotaOrPlanError(probe))
|
|
63
81
|
return text;
|
|
64
82
|
const acct = readAccount();
|
package/dist/batch-api.js
CHANGED
|
@@ -9,7 +9,7 @@ export function batchBase() {
|
|
|
9
9
|
const base = env && env.trim() ? env.trim() : DEFAULT_BATCH_API_BASE;
|
|
10
10
|
return base.replace(/\/+$/, "");
|
|
11
11
|
}
|
|
12
|
-
export const TERMINAL_STATUSES = new Set(["completed", "stopped", "deleted"]);
|
|
12
|
+
export const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "deleted"]);
|
|
13
13
|
export class BatchError extends Error {
|
|
14
14
|
code;
|
|
15
15
|
status;
|
|
@@ -118,6 +118,14 @@ function problemToError(status, body, method, path) {
|
|
|
118
118
|
detail: cause,
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
|
+
if (status === 402 && serverCode === "api_key_cap_reached") {
|
|
122
|
+
return new BatchError({
|
|
123
|
+
code: "BATCH_KEY_CAP_REACHED",
|
|
124
|
+
message: "This API key reached one of its credit caps, so the Batch API refused the request. The account's other API keys still work. Raise or remove the cap at https://app.zenrows.com/settings/api-keys, or wait until it resets (see detail).",
|
|
125
|
+
status,
|
|
126
|
+
detail: cause,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
121
129
|
if (status === 402) {
|
|
122
130
|
return new BatchError({
|
|
123
131
|
code: "BATCH_QUOTA_EXCEEDED",
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export interface ScrapeParams {
|
|
3
|
+
url: string;
|
|
4
|
+
js_render?: boolean | null;
|
|
5
|
+
premium_proxy?: boolean | null;
|
|
6
|
+
proxy_country?: string | null;
|
|
7
|
+
response_type?: "markdown" | "plaintext" | "pdf" | "html" | null;
|
|
8
|
+
autoparse?: boolean | null;
|
|
9
|
+
css_extractor?: string | null;
|
|
10
|
+
wait_for?: string | null;
|
|
11
|
+
wait?: number | null;
|
|
12
|
+
js_instructions?: string | null;
|
|
13
|
+
outputs?: string | null;
|
|
14
|
+
screenshot?: boolean | null;
|
|
15
|
+
screenshot_fullpage?: boolean | null;
|
|
16
|
+
screenshot_selector?: string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Builds the Fetch query string. Uses Adaptive Stealth Mode (mode=auto) unless the
|
|
20
|
+
* agent or the env forces js_render / premium_proxy, which the API won't combine
|
|
21
|
+
* with mode=auto. Mirrors the CLI's defaultMode "auto".
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildScrapeParams(apiKey: string, params: ScrapeParams): URLSearchParams;
|
|
2
24
|
export declare function createServer(apiKey: string, clientName?: string): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,54 @@ const ZENROWS_API_URL = "https://api.zenrows.com/v1/";
|
|
|
13
13
|
const DEFAULT_JS_RENDER = process.env.ZENROWS_JS_RENDER === "true";
|
|
14
14
|
const DEFAULT_PREMIUM_PROXY = process.env.ZENROWS_PREMIUM_PROXY === "true";
|
|
15
15
|
const DEFAULT_RESPONSE_TYPE = process.env.ZENROWS_RESPONSE_TYPE ?? "markdown";
|
|
16
|
+
/**
|
|
17
|
+
* Builds the Fetch query string. Uses Adaptive Stealth Mode (mode=auto) unless the
|
|
18
|
+
* agent or the env forces js_render / premium_proxy, which the API won't combine
|
|
19
|
+
* with mode=auto. Mirrors the CLI's defaultMode "auto".
|
|
20
|
+
*/
|
|
21
|
+
export function buildScrapeParams(apiKey, params) {
|
|
22
|
+
const searchParams = new URLSearchParams({
|
|
23
|
+
apikey: apiKey,
|
|
24
|
+
url: params.url,
|
|
25
|
+
});
|
|
26
|
+
const isScreenshot = !!(params.screenshot || params.screenshot_fullpage || params.screenshot_selector);
|
|
27
|
+
const jsRender = !!params.js_render || DEFAULT_JS_RENDER;
|
|
28
|
+
const premiumProxy = !!params.premium_proxy || DEFAULT_PREMIUM_PROXY;
|
|
29
|
+
if (!jsRender && !premiumProxy) {
|
|
30
|
+
searchParams.set("mode", "auto");
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
if (jsRender || isScreenshot)
|
|
34
|
+
searchParams.set("js_render", "true");
|
|
35
|
+
if (premiumProxy)
|
|
36
|
+
searchParams.set("premium_proxy", "true");
|
|
37
|
+
}
|
|
38
|
+
if (params.proxy_country)
|
|
39
|
+
searchParams.set("proxy_country", params.proxy_country.toUpperCase());
|
|
40
|
+
if (params.autoparse)
|
|
41
|
+
searchParams.set("autoparse", "true");
|
|
42
|
+
if (params.css_extractor)
|
|
43
|
+
searchParams.set("css_extractor", params.css_extractor);
|
|
44
|
+
if (params.wait_for)
|
|
45
|
+
searchParams.set("wait_for", params.wait_for);
|
|
46
|
+
if (params.wait != null)
|
|
47
|
+
searchParams.set("wait", String(params.wait));
|
|
48
|
+
if (params.js_instructions)
|
|
49
|
+
searchParams.set("js_instructions", params.js_instructions);
|
|
50
|
+
if (params.outputs)
|
|
51
|
+
searchParams.set("outputs", params.outputs);
|
|
52
|
+
if (isScreenshot)
|
|
53
|
+
searchParams.set("screenshot", "true");
|
|
54
|
+
if (params.screenshot_fullpage)
|
|
55
|
+
searchParams.set("screenshot_fullpage", "true");
|
|
56
|
+
if (params.screenshot_selector)
|
|
57
|
+
searchParams.set("screenshot_selector", params.screenshot_selector);
|
|
58
|
+
const effectiveType = params.response_type ?? DEFAULT_RESPONSE_TYPE;
|
|
59
|
+
if (!params.autoparse && !params.css_extractor && !params.outputs && !isScreenshot && effectiveType !== "html") {
|
|
60
|
+
searchParams.set("response_type", effectiveType);
|
|
61
|
+
}
|
|
62
|
+
return searchParams;
|
|
63
|
+
}
|
|
16
64
|
export function createServer(apiKey, clientName) {
|
|
17
65
|
const server = new McpServer({
|
|
18
66
|
name: "zenrows",
|
|
@@ -34,35 +82,38 @@ Use for full-page content (markdown/HTML/PDF/screenshot). For structured JSON
|
|
|
34
82
|
fields (products, articles, listings), prefer the extract tool when it fits —
|
|
35
83
|
it returns parsed fields instead of a full page body.
|
|
36
84
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
By default the request uses Adaptive Stealth Mode: Zenrows picks JS rendering
|
|
86
|
+
and premium proxies per page, escalates only when the site blocks, and charges
|
|
87
|
+
only for the configuration that succeeds. Pass just the URL for protected,
|
|
88
|
+
dynamic, or blocked pages; do not turn on js_render or premium_proxy to get past
|
|
89
|
+
a block.
|
|
90
|
+
|
|
91
|
+
Set js_render or premium_proxy only to force a fixed configuration. Doing so
|
|
92
|
+
switches off Adaptive Stealth Mode, and every request is billed at that
|
|
93
|
+
configuration's cost (premium_proxy with js_render is 25x a basic request).
|
|
42
94
|
|
|
43
95
|
Examples:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
96
|
+
Default: { url: "https://example.com" }
|
|
97
|
+
Geo: { url: "https://example.com", proxy_country: "US" }
|
|
98
|
+
Forced: { url: "https://spa.com", js_render: true }`,
|
|
47
99
|
inputSchema: {
|
|
48
100
|
url: z.string().url().describe("The webpage URL to scrape"),
|
|
49
101
|
js_render: z
|
|
50
102
|
.boolean()
|
|
51
103
|
.nullish()
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
"(React, Vue, Angular) and pages that load content dynamically."),
|
|
104
|
+
.describe("Force JavaScript rendering on every request. Overrides Adaptive Stealth Mode, " +
|
|
105
|
+
"which already renders JavaScript when a page needs it. Leave unset unless you need a fixed configuration."),
|
|
55
106
|
premium_proxy: z
|
|
56
107
|
.boolean()
|
|
57
108
|
.nullish()
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
"
|
|
109
|
+
.describe("Force premium residential proxies on every request (10x credit cost). Overrides " +
|
|
110
|
+
"Adaptive Stealth Mode, which already escalates to premium proxies when a site blocks. " +
|
|
111
|
+
"Leave unset unless you need a fixed configuration."),
|
|
61
112
|
proxy_country: z
|
|
62
113
|
.string()
|
|
63
114
|
.nullish()
|
|
64
115
|
.describe("Country for geo-targeted scraping. ISO 3166-1 alpha-2 code (e.g. 'US', 'GB', 'DE'). " +
|
|
65
|
-
"
|
|
116
|
+
"Works in Adaptive Stealth Mode; if you set js_render without premium_proxy, it requires premium_proxy=true."),
|
|
66
117
|
response_type: z
|
|
67
118
|
.enum(["markdown", "plaintext", "pdf", "html"])
|
|
68
119
|
.nullish()
|
|
@@ -87,7 +138,7 @@ Examples:
|
|
|
87
138
|
.string()
|
|
88
139
|
.nullish()
|
|
89
140
|
.describe("CSS selector to wait for before capturing. Use when key content loads " +
|
|
90
|
-
"after the initial page render.
|
|
141
|
+
"after the initial page render. Works in Adaptive Stealth Mode or with js_render=true."),
|
|
91
142
|
wait: z
|
|
92
143
|
.number()
|
|
93
144
|
.int()
|
|
@@ -95,11 +146,11 @@ Examples:
|
|
|
95
146
|
.max(30000)
|
|
96
147
|
.nullish()
|
|
97
148
|
.describe("Milliseconds to wait after page load before capturing content. " +
|
|
98
|
-
"Max 30000 (30s).
|
|
149
|
+
"Max 30000 (30s). Works in Adaptive Stealth Mode or with js_render=true."),
|
|
99
150
|
js_instructions: z
|
|
100
151
|
.string()
|
|
101
152
|
.nullish()
|
|
102
|
-
.describe("JSON array of browser interactions to run before scraping.
|
|
153
|
+
.describe("JSON array of browser interactions to run before scraping. Works in Adaptive Stealth Mode or with js_render=true. " +
|
|
103
154
|
'Example: [{"click":"#load-more"},{"wait":1000},{"wait_for":".results"}]'),
|
|
104
155
|
outputs: z
|
|
105
156
|
.string()
|
|
@@ -124,45 +175,7 @@ Examples:
|
|
|
124
175
|
'Example: ".product-card". Returns an image instead of text content.'),
|
|
125
176
|
},
|
|
126
177
|
}, async (params) => {
|
|
127
|
-
const searchParams =
|
|
128
|
-
apikey: apiKey,
|
|
129
|
-
url: params.url,
|
|
130
|
-
});
|
|
131
|
-
if (params.js_render ||
|
|
132
|
-
DEFAULT_JS_RENDER ||
|
|
133
|
-
params.screenshot ||
|
|
134
|
-
params.screenshot_fullpage ||
|
|
135
|
-
params.screenshot_selector)
|
|
136
|
-
searchParams.set("js_render", "true");
|
|
137
|
-
if (params.premium_proxy || DEFAULT_PREMIUM_PROXY)
|
|
138
|
-
searchParams.set("premium_proxy", "true");
|
|
139
|
-
if (params.proxy_country)
|
|
140
|
-
searchParams.set("proxy_country", params.proxy_country.toUpperCase());
|
|
141
|
-
if (params.autoparse)
|
|
142
|
-
searchParams.set("autoparse", "true");
|
|
143
|
-
if (params.css_extractor)
|
|
144
|
-
searchParams.set("css_extractor", params.css_extractor);
|
|
145
|
-
if (params.wait_for)
|
|
146
|
-
searchParams.set("wait_for", params.wait_for);
|
|
147
|
-
if (params.wait != null)
|
|
148
|
-
searchParams.set("wait", String(params.wait));
|
|
149
|
-
if (params.js_instructions)
|
|
150
|
-
searchParams.set("js_instructions", params.js_instructions);
|
|
151
|
-
if (params.outputs)
|
|
152
|
-
searchParams.set("outputs", params.outputs);
|
|
153
|
-
if (params.screenshot || params.screenshot_fullpage || params.screenshot_selector)
|
|
154
|
-
searchParams.set("screenshot", "true");
|
|
155
|
-
if (params.screenshot_fullpage)
|
|
156
|
-
searchParams.set("screenshot_fullpage", "true");
|
|
157
|
-
if (params.screenshot_selector)
|
|
158
|
-
searchParams.set("screenshot_selector", params.screenshot_selector);
|
|
159
|
-
// response_type is mutually exclusive with autoparse, css_extractor, outputs, and screenshot params.
|
|
160
|
-
// 'html' is the Zenrows default (no param); all other values are passed through.
|
|
161
|
-
const isScreenshot = params.screenshot || params.screenshot_fullpage || params.screenshot_selector;
|
|
162
|
-
const effectiveType = params.response_type ?? DEFAULT_RESPONSE_TYPE;
|
|
163
|
-
if (!params.autoparse && !params.css_extractor && !params.outputs && !isScreenshot && effectiveType !== "html") {
|
|
164
|
-
searchParams.set("response_type", effectiveType);
|
|
165
|
-
}
|
|
178
|
+
const searchParams = buildScrapeParams(apiKey, params);
|
|
166
179
|
let response;
|
|
167
180
|
try {
|
|
168
181
|
response = await fetch(`${ZENROWS_API_URL}?${searchParams}`, {
|
|
@@ -265,7 +278,7 @@ Examples:
|
|
|
265
278
|
role: "user",
|
|
266
279
|
content: {
|
|
267
280
|
type: "text",
|
|
268
|
-
text: `Scrape ${url} using the Zenrows MCP scrape tool
|
|
281
|
+
text: `Scrape ${url} using the Zenrows MCP scrape tool. Pass only the URL: Adaptive Stealth Mode renders JavaScript when the page needs it. Return the full rendered content in markdown format.`,
|
|
269
282
|
},
|
|
270
283
|
},
|
|
271
284
|
],
|
package/dist/tools/account.js
CHANGED
|
@@ -92,7 +92,13 @@ human does not want to wait for the renewal, relay the way to continue now: add
|
|
|
92
92
|
credit pack at https://app.zenrows.com/billing?topup=open (opens the purchase
|
|
93
93
|
directly) or upgrade at https://app.zenrows.com/plans. Prices are per plan; quote them
|
|
94
94
|
only from this tool's response, never from memory.
|
|
95
|
-
AUTH006 is the concurrency limit, which is a different thing entirely
|
|
95
|
+
AUTH006 is the concurrency limit, which is a different thing entirely.
|
|
96
|
+
|
|
97
|
+
AUTH014 (Batch: api_key_cap_reached) is different from AUTH004: the account still has
|
|
98
|
+
credits, but this API key hit one of its own credit caps. Other keys keep working. The
|
|
99
|
+
response's api_key.caps lists the calling key's caps: window, credits, used_credits,
|
|
100
|
+
remaining_credits, resets_at, and unavailable (usage could not be read). Relay the
|
|
101
|
+
resets_at time, or raise or remove the cap at https://app.zenrows.com/settings/api-keys.`,
|
|
96
102
|
inputSchema: {},
|
|
97
103
|
}, async () => runAccountUsage(apiKey, opts));
|
|
98
104
|
}
|
package/dist/tools/batch.js
CHANGED
|
@@ -215,7 +215,7 @@ Download result_url soon — presigned links expire.`,
|
|
|
215
215
|
});
|
|
216
216
|
server.registerTool("batch_wait", {
|
|
217
217
|
annotations: { title: "Wait for Batch Job", readOnlyHint: true, destructiveHint: false },
|
|
218
|
-
description: "Poll batch_status until the job reaches a terminal state (completed, stopped, or deleted).",
|
|
218
|
+
description: "Poll batch_status until the job reaches a terminal state (completed, failed, stopped, or deleted). A run that hits an API key credit cap ends as failed with failure_reason api_key_cap_reached.",
|
|
219
219
|
inputSchema: {
|
|
220
220
|
job_id: z.string().describe("Batch job id"),
|
|
221
221
|
timeout_ms: z
|
|
@@ -19,12 +19,28 @@ export async function browserFetch(method, path, apiKey, browserUrl, body, clien
|
|
|
19
19
|
let data;
|
|
20
20
|
const contentType = response.headers.get("content-type") ?? "";
|
|
21
21
|
const text = await response.text();
|
|
22
|
-
|
|
22
|
+
// Errors come back as application/problem+json (e.g. 402 AUTH014), so match any JSON type.
|
|
23
|
+
data = text;
|
|
24
|
+
if (text && /[/+]json\b/.test(contentType)) {
|
|
25
|
+
try {
|
|
26
|
+
data = JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// keep the raw text
|
|
30
|
+
}
|
|
31
|
+
}
|
|
23
32
|
return { ok: response.ok, status: response.status, data };
|
|
24
33
|
}
|
|
25
34
|
export function browserError(result) {
|
|
26
35
|
if (typeof result.data === "object" && result.data !== null && "error" in result.data) {
|
|
27
36
|
return String(result.data.error);
|
|
28
37
|
}
|
|
38
|
+
if (typeof result.data === "object" && result.data !== null && "code" in result.data) {
|
|
39
|
+
const d = result.data;
|
|
40
|
+
return `HTTP ${result.status} (${String(d.code)}): ${String(d.detail ?? d.title ?? "")}`.trim();
|
|
41
|
+
}
|
|
42
|
+
if (typeof result.data === "string" && result.data.trim()) {
|
|
43
|
+
return `HTTP ${result.status}: ${result.data.trim().slice(0, 300)}`;
|
|
44
|
+
}
|
|
29
45
|
return `HTTP ${result.status}`;
|
|
30
46
|
}
|