@zenrows/mcp 2.2.3 → 2.2.4

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/server.js CHANGED
@@ -48,24 +48,24 @@ Examples:
48
48
  url: z.string().url().describe("The webpage URL to scrape"),
49
49
  js_render: z
50
50
  .boolean()
51
- .optional()
51
+ .nullish()
52
52
  .default(false)
53
53
  .describe("Enable JavaScript rendering via headless browser. Required for SPAs " +
54
54
  "(React, Vue, Angular) and pages that load content dynamically."),
55
55
  premium_proxy: z
56
56
  .boolean()
57
- .optional()
57
+ .nullish()
58
58
  .default(false)
59
59
  .describe("Use premium residential proxies to bypass anti-bot protection. " +
60
60
  "Required for heavily protected sites. Implies higher credit cost."),
61
61
  proxy_country: z
62
62
  .string()
63
- .optional()
63
+ .nullish()
64
64
  .describe("Country for geo-targeted scraping. ISO 3166-1 alpha-2 code (e.g. 'US', 'GB', 'DE'). " +
65
65
  "Requires premium_proxy=true."),
66
66
  response_type: z
67
67
  .enum(["markdown", "plaintext", "pdf", "html"])
68
- .optional()
68
+ .nullish()
69
69
  .default("markdown")
70
70
  .describe("Output format. 'markdown' (default) preserves structure and is ideal for LLMs. " +
71
71
  "'plaintext' strips all formatting for pure text extraction. " +
@@ -74,18 +74,18 @@ Examples:
74
74
  "Ignored when autoparse, css_extractor, outputs, or screenshot params are set."),
75
75
  autoparse: z
76
76
  .boolean()
77
- .optional()
77
+ .nullish()
78
78
  .describe("Automatically extract structured data from the page into JSON. " +
79
79
  "Best for product pages, articles, and listings."),
80
80
  css_extractor: z
81
81
  .string()
82
- .optional()
82
+ .nullish()
83
83
  .describe("Extract specific elements using CSS selectors. " +
84
84
  'JSON object mapping names to selectors, e.g. \'{"title":"h1","price":".price-tag"}\'. ' +
85
85
  "Returns JSON instead of full page content."),
86
86
  wait_for: z
87
87
  .string()
88
- .optional()
88
+ .nullish()
89
89
  .describe("CSS selector to wait for before capturing. Use when key content loads " +
90
90
  "after the initial page render. Requires js_render=true."),
91
91
  wait: z
@@ -93,33 +93,33 @@ Examples:
93
93
  .int()
94
94
  .min(0)
95
95
  .max(30000)
96
- .optional()
96
+ .nullish()
97
97
  .describe("Milliseconds to wait after page load before capturing content. " +
98
98
  "Max 30000 (30s). Requires js_render=true."),
99
99
  js_instructions: z
100
100
  .string()
101
- .optional()
101
+ .nullish()
102
102
  .describe("JSON array of browser interactions to run before scraping. Requires js_render=true. " +
103
103
  'Example: [{"click":"#load-more"},{"wait":1000},{"wait_for":".results"}]'),
104
104
  outputs: z
105
105
  .string()
106
- .optional()
106
+ .nullish()
107
107
  .describe("Comma-separated list of data types to extract as structured JSON. " +
108
108
  "Available: emails, headings, links, menus, images, videos, audios. " +
109
109
  "Use '*' for all types. Returns JSON instead of full page content."),
110
110
  screenshot: z
111
111
  .boolean()
112
- .optional()
112
+ .nullish()
113
113
  .describe("Capture an above-the-fold screenshot of the page. " +
114
114
  "Returns an image instead of text content. Useful for visual verification or debugging."),
115
115
  screenshot_fullpage: z
116
116
  .boolean()
117
- .optional()
117
+ .nullish()
118
118
  .describe("Capture a full-page screenshot including content below the fold. " +
119
119
  "Returns an image instead of text content."),
120
120
  screenshot_selector: z
121
121
  .string()
122
- .optional()
122
+ .nullish()
123
123
  .describe("Capture a screenshot of a specific element using a CSS selector. " +
124
124
  'Example: ".product-card". Returns an image instead of text content.'),
125
125
  },
@@ -49,11 +49,11 @@ function normalizeParams(obj) {
49
49
  }
50
50
  const taskSchema = z.object({
51
51
  url: z.string().url().describe("Target URL for this task"),
52
- external_id: z.string().optional().describe("Optional stable id echoed back on results"),
53
- metadata: z.unknown().optional().describe("Opaque per-task metadata carried through to results"),
52
+ external_id: z.string().nullish().describe("Optional stable id echoed back on results"),
53
+ metadata: z.unknown().nullish().describe("Opaque per-task metadata carried through to results"),
54
54
  zenrows_params: z
55
55
  .record(z.union([z.string(), z.number(), z.boolean()]))
56
- .optional()
56
+ .nullish()
57
57
  .describe("Per-task Zenrows scrape params (js_render, premium_proxy, extract, autoparse, …)"),
58
58
  });
59
59
  export function registerBatchTools(server, apiKey) {
@@ -71,30 +71,30 @@ If you get BATCH_ACCESS_DENIED, the account lacks Batch beta access.`,
71
71
  inputSchema: {
72
72
  tasks: z
73
73
  .array(taskSchema)
74
- .optional()
74
+ .nullish()
75
75
  .describe("List of tasks (each needs a url). Prefer this over urls when you need per-task params."),
76
76
  urls: z
77
77
  .array(z.string().url())
78
- .optional()
78
+ .nullish()
79
79
  .describe("Shorthand: list of URLs (converted to tasks). Ignored when tasks is provided."),
80
- js_render: z.boolean().optional().describe("Job-level js_render for all tasks"),
81
- premium_proxy: z.boolean().optional().describe("Job-level premium_proxy for all tasks"),
80
+ js_render: z.boolean().nullish().describe("Job-level js_render for all tasks"),
81
+ premium_proxy: z.boolean().nullish().describe("Job-level premium_proxy for all tasks"),
82
82
  proxy_country: z
83
83
  .string()
84
- .optional()
84
+ .nullish()
85
85
  .describe("Job-level ISO country code (requires premium_proxy or mode=auto)"),
86
- response_type: z.enum(["markdown", "plaintext", "html", "pdf"]).optional().describe("Job-level response_type"),
86
+ response_type: z.enum(["markdown", "plaintext", "html", "pdf"]).nullish().describe("Job-level response_type"),
87
87
  zenrows_params: z
88
88
  .record(z.union([z.string(), z.number(), z.boolean()]))
89
- .optional()
89
+ .nullish()
90
90
  .describe("Additional job-level zenrows_params merged with the flags above"),
91
- wait: z.boolean().optional().describe("If true, poll until the job reaches a terminal state before returning"),
91
+ wait: z.boolean().nullish().describe("If true, poll until the job reaches a terminal state before returning"),
92
92
  wait_timeout_ms: z
93
93
  .number()
94
94
  .int()
95
95
  .min(1000)
96
96
  .max(3_600_000)
97
- .optional()
97
+ .nullish()
98
98
  .describe("Max wait time when wait=true (default 600000)"),
99
99
  },
100
100
  }, async (params) => {
@@ -121,7 +121,7 @@ If you get BATCH_ACCESS_DENIED, the account lacks Batch beta access.`,
121
121
  const task = { url: t.url };
122
122
  if (t.external_id)
123
123
  task.external_id = t.external_id;
124
- if (t.metadata !== undefined)
124
+ if (t.metadata != null)
125
125
  task.metadata = t.metadata;
126
126
  if (t.zenrows_params)
127
127
  task.zenrows_params = normalizeParams(t.zenrows_params);
@@ -180,11 +180,11 @@ Each row may include task_id, external_id, status, and a short-lived result_url
180
180
  Download result_url soon — presigned links expire.`,
181
181
  inputSchema: {
182
182
  job_id: z.string().describe("Batch job id"),
183
- status: z.enum(["successful", "failed", "all"]).optional().describe("Filter results by status (default: all)"),
183
+ status: z.enum(["successful", "failed", "all"]).nullish().describe("Filter results by status (default: all)"),
184
184
  },
185
185
  }, async ({ job_id, status }) => {
186
186
  try {
187
- const results = await listResults(job_id, { ...call, status });
187
+ const results = await listResults(job_id, { ...call, status: status ?? undefined });
188
188
  return json({ ok: true, job_id, count: results.length, results });
189
189
  }
190
190
  catch (e) {
@@ -223,7 +223,7 @@ Download result_url soon — presigned links expire.`,
223
223
  .int()
224
224
  .min(1000)
225
225
  .max(3_600_000)
226
- .optional()
226
+ .nullish()
227
227
  .describe("Max wait time in ms (default 600000)"),
228
228
  },
229
229
  }, async ({ job_id, timeout_ms }) => {
@@ -38,11 +38,11 @@ When to use options:
38
38
  url: z.string().url().describe("The URL to navigate to"),
39
39
  proxy_country: z
40
40
  .string()
41
- .optional()
41
+ .nullish()
42
42
  .describe("ISO 3166-1 alpha-2 country code for geo-targeted proxy (e.g. 'US', 'GB', 'DE')"),
43
43
  proxy_region: z
44
44
  .string()
45
- .optional()
45
+ .nullish()
46
46
  .describe("World region code for geo-targeted proxy (eu=Europe, na=North America, ap=Asia Pacific, sa=South America, af=Africa, me=Middle East)"),
47
47
  },
48
48
  }, async (params) => {
@@ -194,7 +194,7 @@ When to use options:
194
194
  session_id: sessionId,
195
195
  selector: z.string().describe("CSS selector of the input element"),
196
196
  text: z.string().describe("Text to type"),
197
- clear_first: z.boolean().optional().describe("Clear existing content before typing (default false)"),
197
+ clear_first: z.boolean().nullish().describe("Clear existing content before typing (default false)"),
198
198
  },
199
199
  }, async ({ session_id, selector, text, clear_first }) => {
200
200
  const fetch = tfetch("browser_type");
@@ -340,7 +340,7 @@ When to use options:
340
340
  inputSchema: {
341
341
  session_id: sessionId,
342
342
  direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
343
- distance: z.number().int().positive().optional().describe("Pixels to scroll (default 500)"),
343
+ distance: z.number().int().positive().nullish().describe("Pixels to scroll (default 500)"),
344
344
  },
345
345
  }, async ({ session_id, direction, distance }) => {
346
346
  const fetch = tfetch("browser_scroll");
@@ -442,7 +442,7 @@ labels, and states — everything needed to drive browser interactions.`,
442
442
  description: "Get the visible text content of an element or the entire page body.",
443
443
  inputSchema: {
444
444
  session_id: sessionId,
445
- selector: z.string().optional().describe("CSS selector of the element (omit for full page body text)"),
445
+ selector: z.string().nullish().describe("CSS selector of the element (omit for full page body text)"),
446
446
  },
447
447
  }, async ({ session_id, selector }) => {
448
448
  const fetch = tfetch("browser_get_text");
@@ -487,7 +487,7 @@ labels, and states — everything needed to drive browser interactions.`,
487
487
  description: "Get the HTML source of an element or the full page.",
488
488
  inputSchema: {
489
489
  session_id: sessionId,
490
- selector: z.string().optional().describe("CSS selector of the element (omit for full page HTML)"),
490
+ selector: z.string().nullish().describe("CSS selector of the element (omit for full page HTML)"),
491
491
  },
492
492
  }, async ({ session_id, selector }) => {
493
493
  const fetch = tfetch("browser_get_html");
@@ -537,11 +537,11 @@ labels, and states — everything needed to drive browser interactions.`,
537
537
  session_id: sessionId,
538
538
  full_page: z
539
539
  .boolean()
540
- .optional()
540
+ .nullish()
541
541
  .describe("Capture full page including content below the fold (default false)"),
542
542
  selector: z
543
543
  .string()
544
- .optional()
544
+ .nullish()
545
545
  .describe("CSS selector to capture only a specific element (overrides full_page)"),
546
546
  },
547
547
  }, async ({ session_id, full_page, selector }) => {
@@ -573,9 +573,9 @@ labels, and states — everything needed to drive browser interactions.`,
573
573
  description: "Render the current page as a PDF document.",
574
574
  inputSchema: {
575
575
  session_id: sessionId,
576
- print_background: z.boolean().optional().describe("Print background graphics (default false)"),
577
- landscape: z.boolean().optional().describe("Landscape orientation (default false)"),
578
- scale: z.number().min(0.1).max(2).optional().describe("Page scale factor (default 1)"),
576
+ print_background: z.boolean().nullish().describe("Print background graphics (default false)"),
577
+ landscape: z.boolean().nullish().describe("Landscape orientation (default false)"),
578
+ scale: z.number().min(0.1).max(2).nullish().describe("Page scale factor (default 1)"),
579
579
  },
580
580
  }, async ({ session_id, print_background, landscape, scale }) => {
581
581
  const fetch = tfetch("browser_generate_pdf");
@@ -614,7 +614,7 @@ labels, and states — everything needed to drive browser interactions.`,
614
614
  selector: z.string().describe("CSS selector to wait for"),
615
615
  visible: z
616
616
  .boolean()
617
- .optional()
617
+ .nullish()
618
618
  .describe("Also require the element to be visible, not just present in the DOM (default false)"),
619
619
  },
620
620
  }, async ({ session_id, selector, visible }) => {
@@ -642,7 +642,7 @@ labels, and states — everything needed to drive browser interactions.`,
642
642
  .int()
643
643
  .min(1000)
644
644
  .max(60000)
645
- .optional()
645
+ .nullish()
646
646
  .describe("How long to wait in milliseconds (default 30000, max 60000)"),
647
647
  },
648
648
  }, async ({ session_id, timeout_ms }) => {
@@ -722,11 +722,11 @@ labels, and states — everything needed to drive browser interactions.`,
722
722
  .array(z.object({
723
723
  name: z.string(),
724
724
  value: z.string(),
725
- domain: z.string().optional(),
726
- path: z.string().optional(),
727
- expires: z.number().optional().describe("Unix timestamp"),
728
- http_only: z.boolean().optional(),
729
- secure: z.boolean().optional(),
725
+ domain: z.string().nullish(),
726
+ path: z.string().nullish(),
727
+ expires: z.number().nullish().describe("Unix timestamp"),
728
+ http_only: z.boolean().nullish(),
729
+ secure: z.boolean().nullish(),
730
730
  }))
731
731
  .describe("Array of cookie objects to set"),
732
732
  },
@@ -769,8 +769,8 @@ labels, and states — everything needed to drive browser interactions.`,
769
769
  inputSchema: {
770
770
  session_id: sessionId,
771
771
  action: z.enum(["get", "set", "clear"]).describe("Operation: get a value, set a value, or clear all"),
772
- key: z.string().optional().describe("Storage key (required for get and set)"),
773
- value: z.string().optional().describe("Value to store (required for set)"),
772
+ key: z.string().nullish().describe("Storage key (required for get and set)"),
773
+ value: z.string().nullish().describe("Value to store (required for set)"),
774
774
  },
775
775
  }, async ({ session_id, action, key, value }) => {
776
776
  const fetch = tfetch("browser_local_storage");
@@ -794,7 +794,7 @@ labels, and states — everything needed to drive browser interactions.`,
794
794
  description: "Open a new browser tab. Returns a tab_id to use with browser_switch_tab.",
795
795
  inputSchema: {
796
796
  session_id: sessionId,
797
- url: z.string().url().optional().describe("URL to open in the new tab (opens blank tab if omitted)"),
797
+ url: z.string().url().nullish().describe("URL to open in the new tab (opens blank tab if omitted)"),
798
798
  },
799
799
  }, async ({ session_id, url }) => {
800
800
  const fetch = tfetch("browser_new_tab");
@@ -841,7 +841,7 @@ labels, and states — everything needed to drive browser interactions.`,
841
841
  type: z.literal("type"),
842
842
  selector: z.string(),
843
843
  text: z.string(),
844
- clear_first: z.boolean().optional(),
844
+ clear_first: z.boolean().nullish(),
845
845
  }),
846
846
  z.object({ type: z.literal("fill"), selector: z.string(), value: z.string() }),
847
847
  z.object({ type: z.literal("select"), selector: z.string(), value: z.string() }),
@@ -852,25 +852,25 @@ labels, and states — everything needed to drive browser interactions.`,
852
852
  z.object({
853
853
  type: z.literal("scroll"),
854
854
  direction: z.enum(["up", "down", "left", "right"]),
855
- distance: z.number().optional(),
855
+ distance: z.number().nullish(),
856
856
  }),
857
857
  z.object({ type: z.literal("drag"), source_selector: z.string(), target_selector: z.string() }),
858
858
  z.object({ type: z.literal("get_accessibility_tree") }),
859
859
  z.object({ type: z.literal("get_url") }),
860
860
  z.object({ type: z.literal("get_title") }),
861
- z.object({ type: z.literal("get_text"), selector: z.string().optional() }),
861
+ z.object({ type: z.literal("get_text"), selector: z.string().nullish() }),
862
862
  z.object({ type: z.literal("get_attribute"), selector: z.string(), attribute: z.string() }),
863
- z.object({ type: z.literal("get_html"), selector: z.string().optional() }),
863
+ z.object({ type: z.literal("get_html"), selector: z.string().nullish() }),
864
864
  z.object({ type: z.literal("query_selector_all"), selector: z.string() }),
865
865
  z.object({
866
866
  type: z.literal("screenshot"),
867
- full_page: z.boolean().optional(),
868
- selector: z.string().optional(),
867
+ full_page: z.boolean().nullish(),
868
+ selector: z.string().nullish(),
869
869
  }),
870
870
  z.object({
871
871
  type: z.literal("wait_for_selector"),
872
872
  selector: z.string(),
873
- visible: z.boolean().optional(),
873
+ visible: z.boolean().nullish(),
874
874
  }),
875
875
  z.object({ type: z.literal("wait_for_navigation") }),
876
876
  z.object({ type: z.literal("wait"), ms: z.number().int().min(0).max(30000) }),
@@ -1112,7 +1112,7 @@ For proper image rendering, use browser_screenshot directly.`,
1112
1112
  .min(1)
1113
1113
  .max(50)
1114
1114
  .describe("Ordered list of actions to perform. Each action has a 'type' field plus type-specific parameters."),
1115
- stop_on_error: z.boolean().optional().describe("Stop executing on the first failed action (default true)"),
1115
+ stop_on_error: z.boolean().nullish().describe("Stop executing on the first failed action (default true)"),
1116
1116
  },
1117
1117
  }, async ({ session_id, actions, stop_on_error = true }) => {
1118
1118
  const fetch = tfetch("browser_batch");
@@ -161,6 +161,9 @@ export async function runExtract(apiKey, params, options = {}) {
161
161
  ...(html !== undefined ? { html } : {}),
162
162
  };
163
163
  }
164
+ function withoutNulls(params) {
165
+ return Object.fromEntries(Object.entries(params).filter(([, v]) => v !== null));
166
+ }
164
167
  export function registerExtractTool(server, apiKey, getClientName) {
165
168
  server.registerTool("extract", {
166
169
  annotations: {
@@ -184,39 +187,39 @@ For full-page markdown/HTML/screenshots, use scrape instead.`,
184
187
  url: z.string().url().describe("The webpage URL to extract from"),
185
188
  mode: z
186
189
  .enum(["auto", "autoparse", "css"])
187
- .optional()
190
+ .nullish()
188
191
  .default("auto")
189
192
  .describe("Extraction mode: auto (extract=auto, default), autoparse, or css (requires css_extractor)"),
190
193
  css_extractor: z
191
194
  .string()
192
- .optional()
195
+ .nullish()
193
196
  .describe('Required when mode=css. JSON map of field→selector, e.g. \'{"title":"h1","price":".price"}\''),
194
- js_render: z.boolean().optional().describe("Enable headless JS rendering (SPAs / dynamic content)"),
197
+ js_render: z.boolean().nullish().describe("Enable headless JS rendering (SPAs / dynamic content)"),
195
198
  premium_proxy: z
196
199
  .boolean()
197
- .optional()
200
+ .nullish()
198
201
  .describe("Use premium residential proxies (anti-bot). Higher credit cost."),
199
202
  proxy_country: z
200
203
  .string()
201
- .optional()
204
+ .nullish()
202
205
  .describe("ISO 3166-1 alpha-2 country code. Requires premium_proxy or mode_auto."),
203
- mode_auto: z.boolean().optional().describe("Enable Adaptive Stealth Mode (mode=auto) for tougher sites"),
204
- wait_for: z.string().optional().describe("CSS selector to wait for before extracting. Requires js_render."),
206
+ mode_auto: z.boolean().nullish().describe("Enable Adaptive Stealth Mode (mode=auto) for tougher sites"),
207
+ wait_for: z.string().nullish().describe("CSS selector to wait for before extracting. Requires js_render."),
205
208
  wait: z
206
209
  .number()
207
210
  .int()
208
211
  .min(0)
209
212
  .max(30000)
210
- .optional()
213
+ .nullish()
211
214
  .describe("Milliseconds to wait after load. Requires js_render."),
212
215
  fallback_autoparse: z
213
216
  .boolean()
214
- .optional()
217
+ .nullish()
215
218
  .default(true)
216
219
  .describe("When mode=auto and the domain is not in Extract open beta (AUTH010), retry once with autoparse (default true)"),
217
220
  },
218
221
  }, async (params) => {
219
- const outcome = await runExtract(apiKey, params, { getClientName });
222
+ const outcome = await runExtract(apiKey, withoutNulls(params), { getClientName });
220
223
  if (!outcome.ok)
221
224
  return err(outcome.errorText);
222
225
  return json({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenrows/mcp",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Zenrows MCP server — Fetch, Extract, Batch, and Browser Sessions for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {