pagebolt-mcp 1.2.0 → 1.5.1

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.
Files changed (3) hide show
  1. package/README.md +32 -4
  2. package/package.json +1 -1
  3. package/src/index.mjs +535 -95
package/README.md CHANGED
@@ -8,10 +8,6 @@ Take screenshots, generate PDFs, create OG images, inspect pages, and record dem
8
8
 
9
9
  **Works with Claude Desktop, Cursor, Windsurf, Cline, and any MCP-compatible client.**
10
10
 
11
- <p align="center">
12
- <img src="https://pagebolt.dev/og-image-default.png" alt="PageBolt" width="600" />
13
- </p>
14
-
15
11
  ---
16
12
 
17
13
  ## What It Does
@@ -212,6 +208,38 @@ Check your current API usage and plan limits.
212
208
 
213
209
  ---
214
210
 
211
+ ## Prompts
212
+
213
+ Pre-built prompt templates for common workflows. In clients that support MCP prompts, these appear as slash commands.
214
+
215
+ ### `/capture-page`
216
+
217
+ Capture a clean screenshot of any URL with sensible defaults (blocks banners, ads, chats, trackers).
218
+
219
+ **Arguments:** `url` (required), `device`, `dark_mode`, `full_page`
220
+
221
+ ### `/record-demo`
222
+
223
+ Record a professional demo video. The agent inspects the page first to discover selectors, then builds a video recording sequence.
224
+
225
+ **Arguments:** `url` (required), `description` (required — what the demo should show), `pace`, `format`
226
+
227
+ ### `/audit-page`
228
+
229
+ Inspect a page and get a structured analysis of its elements, forms, links, headings, and potential issues.
230
+
231
+ **Arguments:** `url` (required)
232
+
233
+ ---
234
+
235
+ ## Resources
236
+
237
+ ### `pagebolt://api-docs`
238
+
239
+ The full PageBolt API reference as a text resource. AI agents that support MCP resources can read this for detailed parameter documentation beyond what fits in tool descriptions. Content is fetched from the live `llms-full.txt` endpoint.
240
+
241
+ ---
242
+
215
243
  ## Configuration
216
244
 
217
245
  | Environment Variable | Required | Default | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagebolt-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.5.1",
4
4
  "description": "MCP server for PageBolt — take screenshots, generate PDFs, create OG images, inspect pages, and record demo videos from AI coding assistants like Claude, Cursor, and Windsurf.",
5
5
  "main": "src/index.mjs",
6
6
  "module": "src/index.mjs",
package/src/index.mjs CHANGED
@@ -1,29 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * PageBolt MCP Server
4
+ * PageBolt MCP Server — COMPLETE API coverage
5
5
  *
6
- * A Model Context Protocol (MCP) server that exposes PageBolt's
7
- * screenshot, PDF, and OG image APIs as tools for AI coding assistants
8
- * (Claude Desktop, Cursor, Windsurf, Cline, etc.).
6
+ * A Model Context Protocol (MCP) server that exposes 100% of PageBolt's
7
+ * API as tools for AI coding assistants (Claude, Cursor, Windsurf, Cline).
8
+ *
9
+ * Every parameter from every endpoint is exposed. Nothing is hidden.
9
10
  *
10
11
  * Get your free API key at https://pagebolt.dev
11
12
  *
12
13
  * Configuration (environment variables):
13
14
  * PAGEBOLT_API_KEY — Required. Your PageBolt API key.
14
15
  * PAGEBOLT_BASE_URL — Optional. Defaults to https://pagebolt.dev
15
- *
16
- * Usage:
17
- * npx pagebolt-mcp
18
- * # or after global install:
19
- * pagebolt-mcp
20
16
  */
21
17
 
22
18
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
19
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24
20
  import { z } from 'zod';
25
21
  import { writeFileSync } from 'node:fs';
26
- import { resolve } from 'node:path';
22
+ import { resolve, relative, isAbsolute } from 'node:path';
23
+
24
+ /**
25
+ * Validate that a saveTo path stays within the current working directory.
26
+ * Prevents path traversal attacks (e.g., saveTo: "/etc/cron.d/malicious").
27
+ */
28
+ function safePath(userPath, defaultName) {
29
+ const resolved = resolve(userPath || defaultName);
30
+ const rel = relative(process.cwd(), resolved);
31
+ if (isAbsolute(rel) || rel.startsWith('..')) {
32
+ throw new Error(
33
+ `saveTo path must be within the current working directory. ` +
34
+ `Got "${userPath}", which resolves outside CWD (${process.cwd()}).`
35
+ );
36
+ }
37
+ return resolved;
38
+ }
27
39
 
28
40
  // ─── Configuration ───────────────────────────────────────────────
29
41
  const API_KEY = process.env.PAGEBOLT_API_KEY;
@@ -45,7 +57,7 @@ async function callApi(endpoint, options = {}) {
45
57
  const method = options.method || 'GET';
46
58
  const headers = {
47
59
  'x-api-key': API_KEY,
48
- 'user-agent': 'pagebolt-mcp/1.0.0',
60
+ 'user-agent': 'pagebolt-mcp/1.5.1',
49
61
  ...(options.body ? { 'Content-Type': 'application/json' } : {}),
50
62
  };
51
63
 
@@ -75,13 +87,137 @@ function imageMimeType(format) {
75
87
  return map[format] || 'image/png';
76
88
  }
77
89
 
90
+ // ─── Reusable Zod schemas ────────────────────────────────────────
91
+ // These are shared across multiple tools.
92
+
93
+ const cookieSchema = z.union([
94
+ z.string(),
95
+ z.object({
96
+ name: z.string(),
97
+ value: z.string(),
98
+ domain: z.string().optional(),
99
+ }),
100
+ ]);
101
+
102
+ /** Screenshot style / theme options (frame, background, shadow, etc.) */
103
+ const styleSchema = z.object({
104
+ theme: z.enum([
105
+ 'notion', 'paper', 'vercel', 'glass', 'ocean', 'sunset',
106
+ 'linear', 'arc', 'glassDark', 'glassWarm', 'spotlight',
107
+ 'neonBlue', 'neonPurple', 'neonGreen', 'lavender', 'ember', 'dots', 'grid',
108
+ ]).optional().describe(
109
+ 'One-click theme preset. Applies curated frame + background + shadow + padding. ' +
110
+ 'Free themes: notion, paper, vercel, glass, ocean, sunset. ' +
111
+ 'Paid (Starter+): linear, arc, glassDark, glassWarm, spotlight, neonBlue, neonPurple, neonGreen, lavender, ember, dots, grid. ' +
112
+ 'Individual properties below override the theme defaults.'
113
+ ),
114
+ frame: z.enum(['macos', 'windows', 'minimal', 'none']).optional().describe('Window chrome style. macos = traffic lights, windows = min/max/close, minimal = dots only, none = no frame.'),
115
+ frameTheme: z.enum(['light', 'dark', 'auto']).optional().describe('Frame color theme (default: auto)'),
116
+ background: z.enum([
117
+ 'ocean', 'sunset', 'forest', 'midnight', 'aurora', 'lavender', 'peach', 'arctic', 'ember', 'slate', 'neon',
118
+ 'glass', 'solid', 'spotlight', 'dots', 'grid', 'noise', 'none',
119
+ ]).optional().describe(
120
+ 'Background style. Gradients: ocean, sunset, forest, midnight, aurora, lavender, peach, arctic, ember, slate, neon. ' +
121
+ 'Special: glass (frosted glass effect), solid, spotlight, dots, grid, noise. none = transparent.'
122
+ ),
123
+ bgColor: z.string().optional().describe('Background color as hex (e.g. "#1e3a5f"). Used for solid backgrounds or as base for patterns.'),
124
+ bgColors: z.array(z.string()).optional().describe('Array of 2 hex colors for custom gradient (e.g. ["#1e3a5f", "#7c3aed"])'),
125
+ padding: z.number().int().min(0).max(120).optional().describe('Padding around screenshot in pixels (default: 40)'),
126
+ borderRadius: z.number().int().min(0).max(40).optional().describe('Corner radius in pixels (default: 12)'),
127
+ shadow: z.enum(['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl']).optional().describe('Drop shadow size (default: md)'),
128
+ }).optional().describe(
129
+ 'Screenshot styling options — add a macOS/Windows frame, gradient/glass background, shadow, and rounded corners. ' +
130
+ 'Use the "theme" shortcut for one-click presets, or customize individual properties.'
131
+ );
132
+
133
+ // ─── Server Instructions ────────────────────────────────────────
134
+ const SERVER_INSTRUCTIONS = `
135
+ PageBolt gives you 8 tools for web capture and browser automation. All tools use your API key automatically.
136
+
137
+ ## Tools Overview
138
+
139
+ | Tool | What it does | Cost |
140
+ |------|-------------|------|
141
+ | take_screenshot | Capture a URL, HTML, or Markdown as PNG/JPEG/WebP | 1 request |
142
+ | generate_pdf | Convert a URL or HTML to PDF, saves to disk | 1 request |
143
+ | create_og_image | Generate social card images from templates or custom HTML | 1 request |
144
+ | run_sequence | Multi-step browser automation with multiple screenshot/PDF outputs | 1 request per output |
145
+ | record_video | Record browser automation as MP4/WebM/GIF with cursor effects | 3 requests |
146
+ | inspect_page | Get structured map of page elements with CSS selectors | 1 request |
147
+ | list_devices | List 25+ device presets (iPhone, iPad, MacBook, etc.) | 0 (free) |
148
+ | check_usage | Check current API usage and plan limits | 0 (free) |
149
+
150
+ ## Key Workflow: Inspect Before You Interact
151
+
152
+ When building sequences or videos, ALWAYS use inspect_page first to discover reliable CSS selectors:
153
+
154
+ 1. inspect_page — returns buttons, inputs, forms, links, headings with unique selectors
155
+ 2. run_sequence or record_video — use the selectors from step 1
156
+
157
+ This avoids guessing selectors like "#submit" when the actual element is "#submitBtn".
158
+
159
+ ## Styling Screenshots
160
+
161
+ Use the "style" parameter on take_screenshot for beautiful styled captures:
162
+ - Quick: style.theme = "glass" or "ocean" or "linear" for one-click presets
163
+ - Custom: style.frame = "macos", style.background = "glass", style.shadow = "lg"
164
+
165
+ ## Video Recording Features
166
+
167
+ record_video supports polished video output:
168
+ - frame: { enabled: true, style: "macos" } — browser chrome around the video
169
+ - background: { enabled: true, type: "gradient", gradient: "ocean" } — gradient/glass background with padding
170
+ - cursor: { style: "classic", persist: true } — always-visible cursor
171
+ - Per-step zoom: add zoom: { enabled: true } on click steps
172
+ - **Step notes (IMPORTANT)**: Add a "note" field to EVERY action step for guided-tour-style tooltip annotations. Notes appear as beautiful styled tooltips near the element being interacted with. Example: { action: "click", selector: "#btn", note: "Click here to open settings" }. The only steps that should NOT have notes are wait/wait_for pauses.
173
+ - **Live wait steps**: Add live: true to wait steps to capture animated content (transitions, loading spinners) instead of freezing the last frame.
174
+ - **Variables**: Pass variables: { "base_url": "https://example.com" } and use {{base_url}} in step URLs/values for reusable recordings.
175
+
176
+ ## Common Parameters (available on most tools)
177
+
178
+ - blockBanners: true — hides cookie consent banners (GDPR popups, OneTrust, CookieBot, etc.)
179
+ - blockAds: true — blocks advertisements
180
+ - blockChats: true — blocks live chat widgets (Intercom, Crisp, Drift)
181
+ - blockTrackers: true — blocks analytics trackers (GA, Hotjar, Segment)
182
+ - darkMode: true — emulates dark color scheme (prefers-color-scheme: dark)
183
+ - viewportDevice: "iphone_14_pro" — emulates a specific device (use list_devices to see all 25+)
184
+
185
+ Use blockBanners on almost every request to get clean captures. Combine blockAds + blockChats + blockTrackers for completely clean screenshots.
186
+
187
+ ## Tips
188
+
189
+ - For screenshots of pages behind auth: use cookies, headers, or authorization params
190
+ - extractMetadata: true on take_screenshot returns title, description, OG tags, HTTP status
191
+ - response_type: "json" returns base64 data instead of binary (useful for programmatic use)
192
+ - record_video pace presets: "fast" (0.5x), "normal" (1x), "slow" (2x), "dramatic" (3x), "cinematic" (4.5x)
193
+ - record_video cursor styles: "highlight", "circle", "spotlight", "dot", "classic"
194
+ - run_sequence requires at least 1 screenshot or pdf step as output
195
+ - record_video does NOT allow screenshot/pdf steps — the whole sequence IS the video
196
+ - Max 2 evaluate (JavaScript) steps per sequence/video
197
+ - fullPage: true on screenshots captures the entire scrollable page
198
+ - fullPageScroll: true triggers lazy-loaded images before capture
199
+
200
+ ## Cost Summary
201
+
202
+ | Action | Cost |
203
+ |--------|------|
204
+ | Screenshot, PDF, OG image, Inspect | 1 request each |
205
+ | Sequence | 1 request per output (screenshot/pdf) |
206
+ | Video recording | 3 requests flat |
207
+ | list_devices, check_usage | Free |
208
+ `.trim();
209
+
78
210
  // ─── Create MCP Server ──────────────────────────────────────────
79
211
  function createConfiguredServer() {
80
212
  const srv = new McpServer({
81
213
  name: 'pagebolt',
82
- version: '1.0.0',
214
+ version: '1.5.0',
215
+ }, {
216
+ instructions: SERVER_INSTRUCTIONS,
83
217
  });
84
218
  registerTools(srv);
219
+ registerPrompts(srv);
220
+ registerResources(srv);
85
221
  return srv;
86
222
  }
87
223
 
@@ -89,10 +225,12 @@ const server = createConfiguredServer();
89
225
 
90
226
  function registerTools(server) {
91
227
 
92
- // ─── Tool: take_screenshot ──────────────────────────────────────
228
+ // ═══════════════════════════════════════════════════════════════════
229
+ // Tool: take_screenshot — COMPLETE coverage
230
+ // ═══════════════════════════════════════════════════════════════════
93
231
  server.tool(
94
232
  'take_screenshot',
95
- 'Capture a screenshot of a URL, HTML, or Markdown content. 30+ parameters including device emulation, ad/chat/tracker blocking, metadata extraction, geolocation, timezone, and more. Returns an image (PNG, JPEG, or WebP).',
233
+ 'Capture a screenshot of a URL, HTML, or Markdown content. Supports device emulation, ad/chat/tracker blocking, metadata extraction, geolocation, timezone, styling (macOS/Windows frames, gradient/glass backgrounds, shadows), and more. Returns an image (PNG, JPEG, or WebP).',
96
234
  {
97
235
  // ── Source ──
98
236
  url: z.string().url().optional().describe('URL to capture (required if no html/markdown)'),
@@ -104,6 +242,7 @@ server.tool(
104
242
  viewportDevice: z.string().optional().describe('Device preset for viewport emulation (e.g. "iphone_14_pro", "macbook_pro_14"). Use list_devices to see all presets.'),
105
243
  viewportMobile: z.boolean().optional().describe('Enable mobile meta viewport emulation'),
106
244
  viewportHasTouch: z.boolean().optional().describe('Enable touch event emulation'),
245
+ viewportLandscape: z.boolean().optional().describe('Landscape orientation'),
107
246
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio, use 2 for retina (default: 1)'),
108
247
  // ── Output format ──
109
248
  format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format (default: png)'),
@@ -112,6 +251,8 @@ server.tool(
112
251
  // ── Capture region ──
113
252
  fullPage: z.boolean().optional().describe('Capture the full scrollable page (default: false)'),
114
253
  fullPageScroll: z.boolean().optional().describe('Auto-scroll page before capture to trigger lazy-loaded images'),
254
+ fullPageScrollDelay: z.number().int().min(0).max(2000).optional().describe('Delay between scroll steps in ms (default: 400)'),
255
+ fullPageScrollBy: z.number().int().optional().describe('Pixels to scroll per step (default: viewport height)'),
115
256
  fullPageMaxHeight: z.number().int().optional().describe('Maximum pixel height cap for full-page captures'),
116
257
  selector: z.string().optional().describe('CSS selector to capture a specific element'),
117
258
  clip: z.object({
@@ -121,9 +262,10 @@ server.tool(
121
262
  height: z.number(),
122
263
  }).optional().describe('Crop region { x, y, width, height } in pixels'),
123
264
  // ── Timing ──
124
- delay: z.number().int().min(0).max(10000).optional().describe('Milliseconds to wait before capture (default: 0)'),
265
+ delay: z.number().int().min(0).max(30000).optional().describe('Milliseconds to wait before capture (default: 0)'),
125
266
  waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().describe('When to consider navigation finished (default: networkidle2)'),
126
267
  waitForSelector: z.string().optional().describe('Wait for this CSS selector to appear before capturing'),
268
+ navigationTimeout: z.number().int().min(0).max(30000).optional().describe('Navigation timeout in ms (default: 25000)'),
127
269
  // ── Emulation ──
128
270
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
129
271
  reducedMotion: z.boolean().optional().describe('Emulate prefers-reduced-motion to disable animations'),
@@ -136,28 +278,26 @@ server.tool(
136
278
  }).optional().describe('Emulate geolocation { latitude, longitude, accuracy? }'),
137
279
  userAgent: z.string().optional().describe('Override the browser User-Agent string'),
138
280
  // ── Auth & headers ──
139
- cookies: z.array(
140
- z.union([
141
- z.string(),
142
- z.object({
143
- name: z.string(),
144
- value: z.string(),
145
- domain: z.string().optional(),
146
- }),
147
- ])
148
- ).optional().describe('Cookies to set — array of "name=value" strings or { name, value, domain? } objects'),
281
+ cookies: z.array(cookieSchema).optional().describe('Cookies to set — array of "name=value" strings or { name, value, domain? } objects'),
149
282
  headers: z.record(z.string(), z.string()).optional().describe('Extra HTTP headers to send with the request'),
150
283
  authorization: z.string().optional().describe('Authorization header value (e.g. "Bearer <token>")'),
151
284
  bypassCSP: z.boolean().optional().describe('Bypass Content-Security-Policy on the page'),
152
285
  // ── Content manipulation ──
153
286
  hideSelectors: z.array(z.string()).optional().describe('Array of CSS selectors to hide before capture'),
154
287
  click: z.string().optional().describe('CSS selector to click before capturing the screenshot'),
288
+ injectCss: z.string().optional().describe('Custom CSS to inject before capturing (max 50KB)'),
289
+ injectJs: z.string().optional().describe('Custom JavaScript to execute before capturing (max 50KB)'),
290
+ // ── Blocking ──
155
291
  blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
156
292
  blockAds: z.boolean().optional().describe('Block advertisements on the page'),
157
293
  blockChats: z.boolean().optional().describe('Block live chat widgets on the page'),
158
294
  blockTrackers: z.boolean().optional().describe('Block tracking scripts on the page'),
159
- // ── Extras ──
295
+ blockRequests: z.array(z.string()).optional().describe('URL patterns to block (array of strings)'),
296
+ blockResources: z.array(z.string()).optional().describe('Resource types to block (e.g. ["image", "font"])'),
297
+ // ── Metadata ──
160
298
  extractMetadata: z.boolean().optional().describe('Extract page metadata (title, description, OG tags) alongside the screenshot'),
299
+ // ── Styling ──
300
+ style: styleSchema,
161
301
  },
162
302
  async (params) => {
163
303
  if (!params.url && !params.html && !params.markdown) {
@@ -172,35 +312,57 @@ server.tool(
172
312
  const data = await res.json();
173
313
  const format = params.format || 'png';
174
314
 
175
- return {
176
- content: [
177
- {
178
- type: 'image',
179
- data: data.data,
180
- mimeType: imageMimeType(format),
181
- },
182
- {
183
- type: 'text',
184
- text: `Screenshot captured successfully. Format: ${format}, Size: ${data.size_bytes} bytes, Duration: ${data.duration_ms}ms`,
185
- },
186
- ],
187
- };
315
+ const content = [
316
+ {
317
+ type: 'image',
318
+ data: data.data,
319
+ mimeType: imageMimeType(format),
320
+ },
321
+ {
322
+ type: 'text',
323
+ text: `Screenshot captured successfully. Format: ${format}, Size: ${data.size_bytes} bytes, Duration: ${data.duration_ms}ms`,
324
+ },
325
+ ];
326
+
327
+ // Include metadata if extracted
328
+ if (data.metadata) {
329
+ content.push({
330
+ type: 'text',
331
+ text: `Metadata:\n${JSON.stringify(data.metadata, null, 2)}`,
332
+ });
333
+ }
334
+
335
+ return { content };
188
336
  }
189
337
  );
190
338
 
191
- // ─── Tool: generate_pdf ─────────────────────────────────────────
339
+ // ═══════════════════════════════════════════════════════════════════
340
+ // Tool: generate_pdf — COMPLETE coverage
341
+ // ═══════════════════════════════════════════════════════════════════
192
342
  server.tool(
193
343
  'generate_pdf',
194
- 'Generate a PDF from a URL or HTML content. Saves the PDF to disk and returns the file path.',
344
+ 'Generate a PDF from a URL or HTML content. Supports custom margins, headers/footers, page ranges, and scaling. Saves the PDF to disk and returns the file path.',
195
345
  {
196
346
  url: z.string().url().optional().describe('URL to render as PDF (required if no html)'),
197
347
  html: z.string().optional().describe('Raw HTML to render as PDF (required if no url)'),
198
- format: z.string().optional().describe('Paper format: A4, Letter, Legal, Tabloid (default: A4)'),
348
+ format: z.string().optional().describe('Paper format: A4, Letter, Legal, Tabloid, A3, A5 (default: A4)'),
199
349
  landscape: z.boolean().optional().describe('Landscape orientation (default: false)'),
200
350
  printBackground: z.boolean().optional().describe('Include CSS backgrounds (default: true)'),
201
- margin: z.string().optional().describe('CSS margin for all sides, e.g. "1cm" or "0.5in"'),
351
+ margin: z.union([
352
+ z.string(),
353
+ z.object({
354
+ top: z.string().optional(),
355
+ right: z.string().optional(),
356
+ bottom: z.string().optional(),
357
+ left: z.string().optional(),
358
+ }),
359
+ ]).optional().describe('CSS margin — string for all sides (e.g. "1cm") or object { top, right, bottom, left }'),
202
360
  scale: z.number().min(0.1).max(2).optional().describe('Rendering scale 0.1-2 (default: 1)'),
361
+ width: z.string().optional().describe('Page width (overrides format) — CSS value like "8.5in"'),
203
362
  pageRanges: z.string().optional().describe('Page ranges to include, e.g. "1-5, 8"'),
363
+ headerTemplate: z.string().optional().describe('HTML template for page header (uses Chromium templating)'),
364
+ footerTemplate: z.string().optional().describe('HTML template for page footer'),
365
+ displayHeaderFooter: z.boolean().optional().describe('Show header and footer (default: false)'),
204
366
  delay: z.number().int().min(0).max(10000).optional().describe('Milliseconds to wait before rendering (default: 0)'),
205
367
  saveTo: z.string().optional().describe('Output file path (default: ./output.pdf)'),
206
368
  },
@@ -216,18 +378,37 @@ server.tool(
216
378
  });
217
379
 
218
380
  const data = await res.json();
219
- const outputPath = resolve(saveTo || './output.pdf');
220
381
 
221
- // Decode base64 and write to disk
222
- const buffer = Buffer.from(data.data, 'base64');
223
- writeFileSync(outputPath, buffer);
382
+ // Best-effort save to disk (may fail in hosted/sandboxed environments)
383
+ let savedPath = null;
384
+ try {
385
+ const outputPath = safePath(saveTo, './output.pdf');
386
+ const buffer = Buffer.from(data.data, 'base64');
387
+ writeFileSync(outputPath, buffer);
388
+ savedPath = outputPath;
389
+ } catch (_diskErr) {
390
+ // Disk write failed (e.g. hosted environment, read-only FS) — data is
391
+ // still returned as an embedded resource below, so the client gets it.
392
+ }
393
+
394
+ const fileNote = savedPath
395
+ ? ` File: ${savedPath}`
396
+ : ` File: (not saved to disk — use the embedded resource data below)`;
224
397
 
225
398
  return {
226
399
  content: [
400
+ {
401
+ type: 'resource',
402
+ resource: {
403
+ uri: 'pagebolt://pdf/output.pdf',
404
+ mimeType: 'application/pdf',
405
+ blob: data.data, // base64-encoded PDF — always delivered to client
406
+ },
407
+ },
227
408
  {
228
409
  type: 'text',
229
410
  text: `PDF generated successfully.\n` +
230
- ` File: ${outputPath}\n` +
411
+ `${fileNote}\n` +
231
412
  ` Size: ${data.size_bytes} bytes\n` +
232
413
  ` Duration: ${data.duration_ms}ms`,
233
414
  },
@@ -236,13 +417,15 @@ server.tool(
236
417
  }
237
418
  );
238
419
 
239
- // ─── Tool: create_og_image ──────────────────────────────────────
420
+ // ═══════════════════════════════════════════════════════════════════
421
+ // Tool: create_og_image — COMPLETE coverage
422
+ // ═══════════════════════════════════════════════════════════════════
240
423
  server.tool(
241
424
  'create_og_image',
242
425
  'Generate an Open Graph / social card image. Returns an image using built-in templates or custom HTML.',
243
426
  {
244
427
  template: z.enum(['default', 'minimal', 'gradient']).optional().describe('Built-in template name (default: "default")'),
245
- html: z.string().optional().describe('Custom HTML template (overrides template parameter)'),
428
+ html: z.string().optional().describe('Custom HTML template (overrides template parameter, Growth plan+)'),
246
429
  title: z.string().optional().describe('Main title text (default: "Your Title Here")'),
247
430
  subtitle: z.string().optional().describe('Subtitle text'),
248
431
  logo: z.string().optional().describe('Logo image URL'),
@@ -279,7 +462,9 @@ server.tool(
279
462
  }
280
463
  );
281
464
 
282
- // ─── Tool: run_sequence ─────────────────────────────────────────
465
+ // ═══════════════════════════════════════════════════════════════════
466
+ // Tool: run_sequence — COMPLETE coverage
467
+ // ═══════════════════════════════════════════════════════════════════
283
468
  server.tool(
284
469
  'run_sequence',
285
470
  'Execute a multi-step browser automation sequence. Navigate pages, interact with elements (click, fill, select), and capture multiple screenshots/PDFs in a single browser session. Each output counts as 1 API request.',
@@ -287,7 +472,7 @@ server.tool(
287
472
  steps: z.array(
288
473
  z.object({
289
474
  action: z.enum([
290
- 'navigate', 'click', 'fill', 'select', 'hover',
475
+ 'navigate', 'click', 'dblclick', 'fill', 'select', 'hover',
291
476
  'scroll', 'wait', 'wait_for', 'evaluate',
292
477
  'screenshot', 'pdf',
293
478
  ]).describe('The action to perform'),
@@ -302,11 +487,16 @@ server.tool(
302
487
  name: z.string().optional().describe('Name for the output (for screenshot/pdf actions)'),
303
488
  format: z.string().optional().describe('Image format: png, jpeg, webp (screenshot) or A4, Letter (pdf)'),
304
489
  fullPage: z.boolean().optional().describe('Capture full scrollable page (for screenshot action)'),
490
+ fullPageScroll: z.boolean().optional().describe('Auto-scroll for lazy images (for screenshot action)'),
305
491
  quality: z.number().int().min(1).max(100).optional().describe('JPEG/WebP quality (for screenshot action)'),
492
+ selector: z.string().optional().describe('Element selector (for screenshot action)'),
493
+ omitBackground: z.boolean().optional().describe('Transparent background (for screenshot action)'),
494
+ delay: z.number().int().min(0).max(10000).optional().describe('Pre-capture delay in ms (for screenshot action)'),
306
495
  landscape: z.boolean().optional().describe('Landscape orientation (for pdf action)'),
307
496
  printBackground: z.boolean().optional().describe('Include CSS backgrounds (for pdf action)'),
308
497
  margin: z.string().optional().describe('CSS margin for all sides (for pdf action)'),
309
498
  scale: z.number().min(0.1).max(2).optional().describe('Rendering scale (for pdf action)'),
499
+ style: styleSchema,
310
500
  })
311
501
  ).min(1).max(20).describe('Array of steps to execute in order. Must include at least one screenshot or pdf step. Max 20 steps, max 5 outputs.'),
312
502
  viewport: z.object({
@@ -315,6 +505,9 @@ server.tool(
315
505
  }).optional().describe('Browser viewport size'),
316
506
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
317
507
  blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
508
+ blockAds: z.boolean().optional().describe('Block advertisements on the page'),
509
+ blockChats: z.boolean().optional().describe('Block live chat widgets'),
510
+ blockTrackers: z.boolean().optional().describe('Block tracking scripts'),
318
511
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio (default: 1)'),
319
512
  },
320
513
  async (params) => {
@@ -365,15 +558,17 @@ server.tool(
365
558
  }
366
559
  );
367
560
 
368
- // ─── Tool: record_video ─────────────────────────────────────────
561
+ // ═══════════════════════════════════════════════════════════════════
562
+ // Tool: record_video — COMPLETE coverage
563
+ // ═══════════════════════════════════════════════════════════════════
369
564
  server.tool(
370
565
  'record_video',
371
- 'Record a professional demo video of a multi-step browser automation sequence. Produces MP4/WebM/GIF with automatic cursor highlighting, click ripple effects, smooth cursor movement, and auto-zoom on clicks (Cursorful-style). Each video costs 3 API requests. Saves to disk and returns the file path.',
566
+ 'Record a professional demo video of a multi-step browser automation sequence. Produces MP4/WebM/GIF with cursor highlighting, click effects, smooth movement, per-step zoom, step notes, browser frame (macOS/Windows), gradient/glass backgrounds, and more. Costs 3 API requests. Saves to disk.',
372
567
  {
373
568
  steps: z.array(
374
569
  z.object({
375
570
  action: z.enum([
376
- 'navigate', 'click', 'fill', 'select', 'hover',
571
+ 'navigate', 'click', 'dblclick', 'fill', 'select', 'hover',
377
572
  'scroll', 'wait', 'wait_for', 'evaluate',
378
573
  ]).describe('The action to perform (no screenshot/pdf — the whole sequence is recorded as video)'),
379
574
  url: z.string().url().optional().describe('URL to navigate to (for navigate action)'),
@@ -384,6 +579,12 @@ server.tool(
384
579
  x: z.number().optional().describe('Horizontal scroll position'),
385
580
  y: z.number().optional().describe('Vertical scroll position'),
386
581
  script: z.string().max(5000).optional().describe('JavaScript to execute in page context (for evaluate action)'),
582
+ note: z.string().max(200).optional().describe('Tooltip annotation text shown during this step (max 200 chars)'),
583
+ live: z.boolean().optional().describe('For wait steps: true captures animated content in real-time, false freezes a single frame (default: false)'),
584
+ zoom: z.object({
585
+ enabled: z.boolean().optional().describe('Enable zoom on this step (default: false)'),
586
+ level: z.number().min(1.2).max(4).optional().describe('Zoom magnification (inherits from global zoom.level if not set)'),
587
+ }).optional().describe('Per-step zoom override (for click/dblclick steps). Overrides global zoom settings.'),
387
588
  })
388
589
  ).min(1).max(50).describe('Array of steps to execute and record. Max steps depends on plan (10-50).'),
389
590
  viewport: z.object({
@@ -392,31 +593,63 @@ server.tool(
392
593
  }).optional().describe('Browser viewport size'),
393
594
  format: z.enum(['mp4', 'webm', 'gif']).optional().describe('Video format (default: mp4). webm/gif require Starter+ plan.'),
394
595
  framerate: z.number().int().optional().describe('Frames per second: 24, 30, or 60 (default: 30)'),
596
+ // ── Cursor ──
395
597
  cursor: z.object({
396
598
  visible: z.boolean().optional().describe('Show cursor overlay (default: true)'),
397
- style: z.enum(['highlight', 'circle', 'spotlight', 'dot']).optional().describe('Cursor style (default: highlight)'),
599
+ style: z.enum(['highlight', 'circle', 'spotlight', 'dot', 'classic']).optional().describe('Cursor style (default: highlight). classic = natural arrow cursor.'),
398
600
  color: z.string().optional().describe('Cursor color as hex, e.g. "#3B82F6" (default: blue)'),
399
601
  size: z.number().int().min(8).max(60).optional().describe('Cursor size in pixels (default: 20)'),
400
602
  smoothing: z.boolean().optional().describe('Smooth animated cursor movement (default: true)'),
603
+ opacity: z.number().min(0.1).max(1.0).optional().describe('Cursor opacity 0.1-1.0 (default: 1.0)'),
604
+ persist: z.boolean().optional().describe('Keep cursor visible between actions, not just during them (default: false)'),
401
605
  }).optional().describe('Cursor appearance settings'),
606
+ // ── Zoom (global defaults, per-step overrides available) ──
402
607
  zoom: z.object({
403
- enabled: z.boolean().optional().describe('Auto-zoom on clicks (default: true)'),
404
- level: z.number().min(1.5).max(4).optional().describe('Zoom magnification (default: 2.0)'),
405
- duration: z.number().int().min(200).max(2000).optional().describe('Zoom animation duration in ms (default: 600)'),
406
- }).optional().describe('Auto-zoom settings for click actions'),
608
+ enabled: z.boolean().optional().describe('Enable auto-zoom on clicks (default: false — use per-step zoom instead)'),
609
+ level: z.number().min(1.2).max(4).optional().describe('Default zoom magnification (default: 1.5)'),
610
+ duration: z.number().int().min(400).max(3000).optional().describe('Zoom animation duration in ms (default: 1200)'),
611
+ easing: z.enum(['ease-in-out', 'linear', 'ease']).optional().describe('Zoom animation easing (default: ease-in-out)'),
612
+ }).optional().describe('Global zoom settings. Per-step zoom on click/dblclick steps overrides these.'),
407
613
  autoZoom: z.boolean().optional().describe('Shorthand: set to true to enable auto-zoom with defaults (same as zoom.enabled=true)'),
614
+ // ── Click effects ──
408
615
  clickEffect: z.object({
409
616
  enabled: z.boolean().optional().describe('Show click ripple effects (default: true)'),
410
617
  style: z.enum(['ripple', 'pulse', 'ring']).optional().describe('Click effect style (default: ripple)'),
411
618
  color: z.string().optional().describe('Click effect color as hex'),
412
619
  }).optional().describe('Visual click effect settings'),
620
+ // ── Pace ──
413
621
  pace: z.union([
414
622
  z.number().min(0.25).max(6),
415
623
  z.enum(['fast', 'normal', 'slow', 'dramatic', 'cinematic']),
416
624
  ]).optional().describe('Controls how deliberate the video feels. Number (0.25–6.0, higher = slower) or preset: "fast" (0.5×), "normal" (1×), "slow" (2×), "dramatic" (3×), "cinematic" (4.5×). Default: "normal".'),
625
+ // ── Frame (browser chrome) ──
626
+ frame: z.object({
627
+ enabled: z.boolean().optional().describe('Enable browser frame around the video (default: false)'),
628
+ style: z.enum(['macos', 'windows', 'minimal']).optional().describe('Frame style: macos (traffic lights), windows (min/max/close), minimal (dots only). Default: macos.'),
629
+ theme: z.enum(['light', 'dark', 'auto']).optional().describe('Frame color theme (default: auto)'),
630
+ showUrl: z.boolean().optional().describe('Show URL in the frame bar (default: true)'),
631
+ }).optional().describe('Browser chrome frame around the video. Adds a macOS/Windows-style title bar.'),
632
+ // ── Background ──
633
+ background: z.object({
634
+ enabled: z.boolean().optional().describe('Enable styled background (default: false)'),
635
+ type: z.enum(['solid', 'gradient']).optional().describe('Background type (default: gradient)'),
636
+ gradient: z.enum([
637
+ 'ocean', 'sunset', 'forest', 'midnight', 'aurora',
638
+ 'lavender', 'peach', 'arctic', 'ember', 'slate', 'neon', 'custom',
639
+ ]).optional().describe('Gradient preset name. 12 built-in presets, or "custom" to use colors array. Default: ocean.'),
640
+ color: z.string().optional().describe('Solid background color as hex (e.g. "#1e3a5f"). Used when type is "solid" or gradient is "custom".'),
641
+ colors: z.array(z.string()).optional().describe('Array of 2 hex colors for custom gradient (e.g. ["#1e3a5f", "#7c3aed"]). Only used when gradient is "custom".'),
642
+ padding: z.number().int().min(0).max(120).optional().describe('Padding around the video in pixels (default: 40)'),
643
+ borderRadius: z.number().int().min(0).max(40).optional().describe('Corner radius in pixels (default: 12)'),
644
+ }).optional().describe('Styled background behind the video. Adds gradient/solid background with padding and rounded corners — creates a "floating window" effect.'),
645
+ // ── Blocking ──
417
646
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
418
- blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: true)'),
647
+ blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: true for videos)'),
648
+ blockAds: z.boolean().optional().describe('Block advertisements on the page'),
649
+ blockChats: z.boolean().optional().describe('Block live chat widgets'),
650
+ blockTrackers: z.boolean().optional().describe('Block tracking scripts'),
419
651
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio (default: 1)'),
652
+ variables: z.record(z.string()).optional().describe('Key-value map for variable substitution in step URLs/values. E.g. { "base_url": "https://example.com" } replaces {{base_url}} in steps.'),
420
653
  saveTo: z.string().optional().describe('Output file path (default: ./recording.mp4)'),
421
654
  },
422
655
  async (params) => {
@@ -435,20 +668,42 @@ server.tool(
435
668
  const data = await res.json();
436
669
  const format = params.format || 'mp4';
437
670
  const ext = format === 'gif' ? 'gif' : format;
438
- const outputPath = resolve(saveTo || `./recording.${ext}`);
439
671
 
440
- // Decode base64 and write to disk
441
- const buffer = Buffer.from(data.data, 'base64');
442
- writeFileSync(outputPath, buffer);
672
+ // Determine video MIME type
673
+ const videoMimeTypes = { mp4: 'video/mp4', webm: 'video/webm', gif: 'image/gif' };
674
+ const mimeType = videoMimeTypes[ext] || 'video/mp4';
675
+
676
+ // Best-effort save to disk (may fail in hosted/sandboxed environments)
677
+ let savedPath = null;
678
+ try {
679
+ const outputPath = safePath(saveTo, `./recording.${ext}`);
680
+ const buffer = Buffer.from(data.data, 'base64');
681
+ writeFileSync(outputPath, buffer);
682
+ savedPath = outputPath;
683
+ } catch (_diskErr) {
684
+ // Disk write failed (e.g. hosted environment, read-only FS) — data is
685
+ // still returned as an embedded resource below, so the client gets it.
686
+ }
443
687
 
444
688
  const durationSec = (data.duration_ms / 1000).toFixed(1);
689
+ const fileNote = savedPath
690
+ ? ` File: ${savedPath}\n`
691
+ : ` File: (not saved to disk — use the embedded resource data below)\n`;
445
692
 
446
693
  return {
447
694
  content: [
695
+ {
696
+ type: 'resource',
697
+ resource: {
698
+ uri: `pagebolt://video/recording.${ext}`,
699
+ mimeType,
700
+ blob: data.data, // base64-encoded video — always delivered to client
701
+ },
702
+ },
448
703
  {
449
704
  type: 'text',
450
705
  text: `Video recorded successfully.\n` +
451
- ` File: ${outputPath}\n` +
706
+ fileNote +
452
707
  ` Format: ${data.format}\n` +
453
708
  ` Size: ${(data.size_bytes / 1024).toFixed(1)} KB\n` +
454
709
  ` Duration: ${durationSec}s\n` +
@@ -465,10 +720,12 @@ server.tool(
465
720
  }
466
721
  );
467
722
 
468
- // ─── Tool: inspect_page ─────────────────────────────────────────
723
+ // ═══════════════════════════════════════════════════════════════════
724
+ // Tool: inspect_page — COMPLETE coverage
725
+ // ═══════════════════════════════════════════════════════════════════
469
726
  server.tool(
470
727
  'inspect_page',
471
- 'Inspect a web page and get a structured map of all interactive elements, headings, forms, links, and images — each with a unique CSS selector. Use this BEFORE run_sequence to discover what elements exist on the page and get reliable selectors. Returns text (not an image), so it is fast and cheap. Costs 1 API request.',
728
+ 'Inspect a web page and get a structured map of all interactive elements, headings, forms, links, and images — each with a unique CSS selector. Use this BEFORE run_sequence or record_video to discover what elements exist on the page and get reliable selectors. Returns text (not an image), so it is fast and cheap. Costs 1 API request.',
472
729
  {
473
730
  // ── Source ──
474
731
  url: z.string().url().optional().describe('URL to inspect (required if no html)'),
@@ -479,36 +736,39 @@ server.tool(
479
736
  viewportDevice: z.string().optional().describe('Device preset for viewport emulation (e.g. "iphone_14_pro"). Use list_devices to see all presets.'),
480
737
  viewportMobile: z.boolean().optional().describe('Enable mobile meta viewport emulation'),
481
738
  viewportHasTouch: z.boolean().optional().describe('Enable touch event emulation'),
739
+ viewportLandscape: z.boolean().optional().describe('Landscape orientation'),
482
740
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio (default: 1)'),
483
741
  // ── Timing ──
484
742
  waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().describe('When to consider navigation finished (default: networkidle2)'),
485
743
  waitForSelector: z.string().optional().describe('Wait for this CSS selector to appear before inspecting'),
744
+ navigationTimeout: z.number().int().min(0).max(30000).optional().describe('Navigation timeout in ms (default: 25000)'),
486
745
  // ── Emulation ──
487
746
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
488
747
  reducedMotion: z.boolean().optional().describe('Emulate prefers-reduced-motion'),
748
+ mediaType: z.enum(['screen', 'print']).optional().describe('Emulate CSS media type'),
749
+ timeZone: z.string().optional().describe('Override browser timezone'),
750
+ geolocation: z.object({
751
+ latitude: z.number(),
752
+ longitude: z.number(),
753
+ accuracy: z.number().optional(),
754
+ }).optional().describe('Emulate geolocation'),
489
755
  userAgent: z.string().optional().describe('Override the browser User-Agent string'),
490
756
  // ── Auth & headers ──
491
- cookies: z.array(
492
- z.union([
493
- z.string(),
494
- z.object({
495
- name: z.string(),
496
- value: z.string(),
497
- domain: z.string().optional(),
498
- }),
499
- ])
500
- ).optional().describe('Cookies to set — array of "name=value" strings or { name, value, domain? } objects'),
757
+ cookies: z.array(cookieSchema).optional().describe('Cookies to set — array of "name=value" strings or { name, value, domain? } objects'),
501
758
  headers: z.record(z.string(), z.string()).optional().describe('Extra HTTP headers to send with the request'),
502
759
  authorization: z.string().optional().describe('Authorization header value (e.g. "Bearer <token>")'),
503
760
  bypassCSP: z.boolean().optional().describe('Bypass Content-Security-Policy on the page'),
504
761
  // ── Content manipulation ──
505
762
  hideSelectors: z.array(z.string()).optional().describe('Array of CSS selectors to hide before inspecting'),
763
+ injectCss: z.string().optional().describe('Custom CSS to inject before inspecting'),
764
+ injectJs: z.string().optional().describe('Custom JavaScript to execute before inspecting'),
765
+ // ── Blocking ──
506
766
  blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
507
767
  blockAds: z.boolean().optional().describe('Block advertisements on the page'),
508
768
  blockChats: z.boolean().optional().describe('Block live chat widgets'),
509
769
  blockTrackers: z.boolean().optional().describe('Block tracking scripts'),
510
- injectCss: z.string().optional().describe('Custom CSS to inject before inspecting'),
511
- injectJs: z.string().optional().describe('Custom JavaScript to execute before inspecting'),
770
+ blockRequests: z.array(z.string()).optional().describe('URL patterns to block'),
771
+ blockResources: z.array(z.string()).optional().describe('Resource types to block'),
512
772
  },
513
773
  async (params) => {
514
774
  if (!params.url && !params.html) {
@@ -526,7 +786,6 @@ server.tool(
526
786
  // Format as structured text for efficient LLM consumption
527
787
  const lines = [];
528
788
 
529
- // Header
530
789
  lines.push(`Page: ${data.title || '(untitled)'} (${data.url || params.url || 'html content'})`);
531
790
  if (data.metadata) {
532
791
  if (data.metadata.description) lines.push(`Description: ${data.metadata.description}`);
@@ -535,7 +794,6 @@ server.tool(
535
794
  }
536
795
  lines.push('');
537
796
 
538
- // Headings
539
797
  if (data.headings && data.headings.length > 0) {
540
798
  lines.push(`Headings (${data.headings.length}):`);
541
799
  for (const h of data.headings) {
@@ -544,7 +802,6 @@ server.tool(
544
802
  lines.push('');
545
803
  }
546
804
 
547
- // Interactive elements
548
805
  if (data.elements && data.elements.length > 0) {
549
806
  lines.push(`Interactive Elements (${data.elements.length}):`);
550
807
  for (const el of data.elements) {
@@ -560,7 +817,6 @@ server.tool(
560
817
  lines.push('');
561
818
  }
562
819
 
563
- // Forms
564
820
  if (data.forms && data.forms.length > 0) {
565
821
  lines.push(`Forms (${data.forms.length}):`);
566
822
  for (const f of data.forms) {
@@ -574,7 +830,6 @@ server.tool(
574
830
  lines.push('');
575
831
  }
576
832
 
577
- // Links
578
833
  if (data.links && data.links.length > 0) {
579
834
  lines.push(`Links (${data.links.length}):`);
580
835
  for (const l of data.links) {
@@ -583,7 +838,6 @@ server.tool(
583
838
  lines.push('');
584
839
  }
585
840
 
586
- // Images
587
841
  if (data.images && data.images.length > 0) {
588
842
  lines.push(`Images (${data.images.length}):`);
589
843
  for (const img of data.images) {
@@ -596,12 +850,7 @@ server.tool(
596
850
  lines.push(`Duration: ${data.duration_ms}ms`);
597
851
 
598
852
  return {
599
- content: [
600
- {
601
- type: 'text',
602
- text: lines.join('\n'),
603
- },
604
- ],
853
+ content: [{ type: 'text', text: lines.join('\n') }],
605
854
  };
606
855
  } catch (err) {
607
856
  return { content: [{ type: 'text', text: `Inspect error: ${err.message}` }], isError: true };
@@ -609,7 +858,9 @@ server.tool(
609
858
  }
610
859
  );
611
860
 
612
- // ─── Tool: list_devices ─────────────────────────────────────────
861
+ // ═══════════════════════════════════════════════════════════════════
862
+ // Tool: list_devices
863
+ // ═══════════════════════════════════════════════════════════════════
613
864
  server.tool(
614
865
  'list_devices',
615
866
  'List all available device presets for viewport emulation (e.g. iphone_14_pro, macbook_pro_14). Use the returned device names with the viewportDevice parameter in take_screenshot.',
@@ -638,7 +889,9 @@ server.tool(
638
889
  }
639
890
  );
640
891
 
641
- // ─── Tool: check_usage ──────────────────────────────────────────
892
+ // ═══════════════════════════════════════════════════════════════════
893
+ // Tool: check_usage
894
+ // ═══════════════════════════════════════════════════════════════════
642
895
  server.tool(
643
896
  'check_usage',
644
897
  'Check your current PageBolt API usage and plan limits.',
@@ -668,7 +921,194 @@ server.tool(
668
921
 
669
922
  } // end registerTools
670
923
 
671
- // ─── Smithery sandbox export (for scanning tools without credentials) ─
924
+ // ─── Prompts ────────────────────────────────────────────────────
925
+ function registerPrompts(server) {
926
+
927
+ server.prompt(
928
+ 'capture-page',
929
+ 'Capture a clean screenshot of any URL with sensible defaults. Optionally inspects the page first.',
930
+ {
931
+ url: z.string().describe('The URL to capture'),
932
+ device: z.string().optional().describe('Device preset, e.g. "iphone_14_pro" or "macbook_pro_14"'),
933
+ dark_mode: z.enum(['true', 'false']).optional().describe('Enable dark mode (default: false)'),
934
+ full_page: z.enum(['true', 'false']).optional().describe('Capture the full scrollable page (default: false)'),
935
+ style_theme: z.enum([
936
+ 'notion', 'paper', 'vercel', 'glass', 'ocean', 'sunset',
937
+ 'linear', 'arc', 'glassDark', 'glassWarm', 'spotlight',
938
+ 'neonBlue', 'neonPurple', 'neonGreen', 'lavender', 'ember', 'dots', 'grid',
939
+ 'none',
940
+ ]).optional().describe('Screenshot style theme (default: none). Use "glass" for frosted glass, "ocean" for gradient, "linear" for Linear-style dark.'),
941
+ },
942
+ (args) => {
943
+ const device = args.device ? `\n- Use device preset: ${args.device}` : '';
944
+ const dark = args.dark_mode === 'true' ? '\n- Enable dark mode' : '';
945
+ const full = args.full_page === 'true' ? '\n- Capture the full scrollable page' : '';
946
+ const style = args.style_theme && args.style_theme !== 'none'
947
+ ? `\n- Apply style theme: "${args.style_theme}" (adds frame, background, shadow)`
948
+ : '';
949
+
950
+ return {
951
+ messages: [
952
+ {
953
+ role: 'user',
954
+ content: {
955
+ type: 'text',
956
+ text: `Take a clean screenshot of ${args.url} with these settings:
957
+ - Block banners, ads, chats, and trackers for a clean capture${device}${dark}${full}${style}
958
+ - Use PNG format
959
+
960
+ Call take_screenshot with:
961
+ url: "${args.url}"
962
+ blockBanners: true
963
+ blockAds: true
964
+ blockChats: true
965
+ blockTrackers: true${args.device ? `\n viewportDevice: "${args.device}"` : ''}${args.dark_mode === 'true' ? '\n darkMode: true' : ''}${args.full_page === 'true' ? '\n fullPage: true\n fullPageScroll: true' : ''}${args.style_theme && args.style_theme !== 'none' ? `\n style: { theme: "${args.style_theme}" }` : ''}`,
966
+ },
967
+ },
968
+ ],
969
+ };
970
+ }
971
+ );
972
+
973
+ server.prompt(
974
+ 'record-demo',
975
+ 'Record a professional demo video of a web page or flow. Generates a step sequence automatically.',
976
+ {
977
+ url: z.string().describe('The starting URL to record'),
978
+ description: z.string().describe('What the demo should show, e.g. "Sign in and explore the dashboard"'),
979
+ pace: z.enum(['fast', 'normal', 'slow', 'dramatic', 'cinematic']).optional().describe('Video pace preset (default: normal)'),
980
+ format: z.enum(['mp4', 'webm', 'gif']).optional().describe('Output format (default: mp4)'),
981
+ frame: z.enum(['macos', 'windows', 'minimal', 'none']).optional().describe('Browser frame style (default: none)'),
982
+ background: z.enum(['ocean', 'sunset', 'midnight', 'glass', 'none']).optional().describe('Background style (default: none)'),
983
+ },
984
+ (args) => {
985
+ const pace = args.pace || 'normal';
986
+ const format = args.format || 'mp4';
987
+ const frame = args.frame || 'none';
988
+ const bg = args.background || 'none';
989
+
990
+ const frameConfig = frame !== 'none'
991
+ ? `\n - frame: { enabled: true, style: "${frame}", theme: "dark" }`
992
+ : '';
993
+ const bgConfig = bg !== 'none'
994
+ ? `\n - background: { enabled: true, type: "gradient", gradient: "${bg}", padding: 40, borderRadius: 12 }`
995
+ : '';
996
+
997
+ return {
998
+ messages: [
999
+ {
1000
+ role: 'user',
1001
+ content: {
1002
+ type: 'text',
1003
+ text: `Record a professional demo video. Here's what I need:
1004
+
1005
+ **Starting URL:** ${args.url}
1006
+ **What to demo:** ${args.description}
1007
+ **Pace:** ${pace}
1008
+ **Format:** ${format}
1009
+
1010
+ Please follow this workflow:
1011
+
1012
+ 1. First, call inspect_page on ${args.url} (with blockBanners: true) to discover the page structure and get reliable CSS selectors.
1013
+
1014
+ 2. Based on the inspection results and the description above, plan a sequence of steps (navigate, click, fill, scroll, wait, etc.) that demonstrates the described flow.
1015
+
1016
+ 3. Call record_video with:
1017
+ - The planned steps array
1018
+ - format: "${format}"
1019
+ - pace: "${pace}"
1020
+ - blockBanners: true
1021
+ - cursor: { style: "classic", visible: true, persist: true }
1022
+ - clickEffect: { style: "ripple" }${frameConfig}${bgConfig}
1023
+
1024
+ Important tips:
1025
+ - Use selectors from the inspect_page results — never guess selectors
1026
+ - Add wait steps (ms: 800-1200) between interactions for visual clarity
1027
+ - Use wait_for after navigation to ensure the page loads
1028
+ - **ALWAYS add a "note" field on every meaningful step** — notes render as styled tooltip annotations that explain what's happening, creating a guided tour experience. Examples:
1029
+ - navigate: note: "Opening the dashboard"
1030
+ - click: note: "This button creates a new project"
1031
+ - fill: note: "Enter your email to get started"
1032
+ - hover: note: "Hover to reveal the dropdown menu"
1033
+ - The ONLY steps without notes should be wait/wait_for (pauses)
1034
+ - Keep to 15 steps or fewer for best results
1035
+ - Each video costs 3 API requests`,
1036
+ },
1037
+ },
1038
+ ],
1039
+ };
1040
+ }
1041
+ );
1042
+
1043
+ server.prompt(
1044
+ 'audit-page',
1045
+ 'Inspect a page and return a structured analysis of its elements, forms, links, and interactive components.',
1046
+ {
1047
+ url: z.string().describe('The URL to audit'),
1048
+ },
1049
+ (args) => {
1050
+ return {
1051
+ messages: [
1052
+ {
1053
+ role: 'user',
1054
+ content: {
1055
+ type: 'text',
1056
+ text: `Perform a structured audit of ${args.url}.
1057
+
1058
+ 1. Call inspect_page with:
1059
+ - url: "${args.url}"
1060
+ - blockBanners: true
1061
+ - blockAds: true
1062
+
1063
+ 2. Analyze the results and provide a clear summary:
1064
+ - **Page overview:** Title, description, language, HTTP status
1065
+ - **Navigation:** List all nav links with their destinations
1066
+ - **Forms:** List all forms with their fields and actions
1067
+ - **Interactive elements:** Buttons, dropdowns, toggles with their selectors
1068
+ - **Headings:** Document outline (h1-h6 hierarchy)
1069
+ - **Images:** Count and list images missing alt text
1070
+ - **Potential issues:** Missing form labels, broken links, accessibility concerns
1071
+
1072
+ 3. If this page will be used for automation (sequence/video), list the most useful CSS selectors the user should know about.`,
1073
+ },
1074
+ },
1075
+ ],
1076
+ };
1077
+ }
1078
+ );
1079
+
1080
+ } // end registerPrompts
1081
+
1082
+ // ─── Resources ──────────────────────────────────────────────────
1083
+ function registerResources(server) {
1084
+
1085
+ server.resource(
1086
+ 'api-docs',
1087
+ 'pagebolt://api-docs',
1088
+ { description: 'Complete PageBolt API reference with all endpoints, parameters, examples, and plan limits. Read this for detailed documentation beyond tool descriptions.', mimeType: 'text/plain' },
1089
+ async () => {
1090
+ try {
1091
+ const res = await fetch(`${BASE_URL}/llms-full.txt`);
1092
+ if (res.ok) {
1093
+ const text = await res.text();
1094
+ return { contents: [{ uri: 'pagebolt://api-docs', text, mimeType: 'text/plain' }] };
1095
+ }
1096
+ } catch (_) {
1097
+ // fall through to embedded fallback
1098
+ }
1099
+ return {
1100
+ contents: [{
1101
+ uri: 'pagebolt://api-docs',
1102
+ text: 'Full API docs available at https://pagebolt.dev/docs or https://pagebolt.dev/llms-full.txt',
1103
+ mimeType: 'text/plain',
1104
+ }],
1105
+ };
1106
+ }
1107
+ );
1108
+
1109
+ } // end registerResources
1110
+
1111
+ // ─── Smithery sandbox export ─────────────────────────────────────
672
1112
  export function createSandboxServer() {
673
1113
  return createConfiguredServer();
674
1114
  }