@zenrows/mcp 2.2.4 → 2.3.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.
@@ -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;
@@ -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",
@@ -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
  }
@@ -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
- data = contentType.includes("application/json") && text ? JSON.parse(text) : text;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenrows/mcp",
3
- "version": "2.2.4",
3
+ "version": "2.3.0",
4
4
  "description": "Zenrows MCP server — Fetch, Extract, Batch, and Browser Sessions for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {