pagebolt-mcp 1.3.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 +0 -4
  2. package/package.json +1 -1
  3. package/src/index.mjs +316 -109
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagebolt-mcp",
3
- "version": "1.3.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,8 +87,50 @@ 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
+
78
133
  // ─── Server Instructions ────────────────────────────────────────
79
- // Sent to the AI agent on connection so it knows how to use the tools.
80
134
  const SERVER_INSTRUCTIONS = `
81
135
  PageBolt gives you 8 tools for web capture and browser automation. All tools use your API key automatically.
82
136
 
@@ -102,6 +156,23 @@ When building sequences or videos, ALWAYS use inspect_page first to discover rel
102
156
 
103
157
  This avoids guessing selectors like "#submit" when the actual element is "#submitBtn".
104
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
+
105
176
  ## Common Parameters (available on most tools)
106
177
 
107
178
  - blockBanners: true — hides cookie consent banners (GDPR popups, OneTrust, CookieBot, etc.)
@@ -119,7 +190,7 @@ Use blockBanners on almost every request to get clean captures. Combine blockAds
119
190
  - extractMetadata: true on take_screenshot returns title, description, OG tags, HTTP status
120
191
  - response_type: "json" returns base64 data instead of binary (useful for programmatic use)
121
192
  - record_video pace presets: "fast" (0.5x), "normal" (1x), "slow" (2x), "dramatic" (3x), "cinematic" (4.5x)
122
- - record_video cursor styles: "highlight", "circle", "spotlight", "dot"
193
+ - record_video cursor styles: "highlight", "circle", "spotlight", "dot", "classic"
123
194
  - run_sequence requires at least 1 screenshot or pdf step as output
124
195
  - record_video does NOT allow screenshot/pdf steps — the whole sequence IS the video
125
196
  - Max 2 evaluate (JavaScript) steps per sequence/video
@@ -140,7 +211,7 @@ Use blockBanners on almost every request to get clean captures. Combine blockAds
140
211
  function createConfiguredServer() {
141
212
  const srv = new McpServer({
142
213
  name: 'pagebolt',
143
- version: '1.3.0',
214
+ version: '1.5.0',
144
215
  }, {
145
216
  instructions: SERVER_INSTRUCTIONS,
146
217
  });
@@ -154,10 +225,12 @@ const server = createConfiguredServer();
154
225
 
155
226
  function registerTools(server) {
156
227
 
157
- // ─── Tool: take_screenshot ──────────────────────────────────────
228
+ // ═══════════════════════════════════════════════════════════════════
229
+ // Tool: take_screenshot — COMPLETE coverage
230
+ // ═══════════════════════════════════════════════════════════════════
158
231
  server.tool(
159
232
  'take_screenshot',
160
- '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).',
161
234
  {
162
235
  // ── Source ──
163
236
  url: z.string().url().optional().describe('URL to capture (required if no html/markdown)'),
@@ -169,6 +242,7 @@ server.tool(
169
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.'),
170
243
  viewportMobile: z.boolean().optional().describe('Enable mobile meta viewport emulation'),
171
244
  viewportHasTouch: z.boolean().optional().describe('Enable touch event emulation'),
245
+ viewportLandscape: z.boolean().optional().describe('Landscape orientation'),
172
246
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio, use 2 for retina (default: 1)'),
173
247
  // ── Output format ──
174
248
  format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format (default: png)'),
@@ -177,6 +251,8 @@ server.tool(
177
251
  // ── Capture region ──
178
252
  fullPage: z.boolean().optional().describe('Capture the full scrollable page (default: false)'),
179
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)'),
180
256
  fullPageMaxHeight: z.number().int().optional().describe('Maximum pixel height cap for full-page captures'),
181
257
  selector: z.string().optional().describe('CSS selector to capture a specific element'),
182
258
  clip: z.object({
@@ -186,9 +262,10 @@ server.tool(
186
262
  height: z.number(),
187
263
  }).optional().describe('Crop region { x, y, width, height } in pixels'),
188
264
  // ── Timing ──
189
- 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)'),
190
266
  waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().describe('When to consider navigation finished (default: networkidle2)'),
191
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)'),
192
269
  // ── Emulation ──
193
270
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
194
271
  reducedMotion: z.boolean().optional().describe('Emulate prefers-reduced-motion to disable animations'),
@@ -201,28 +278,26 @@ server.tool(
201
278
  }).optional().describe('Emulate geolocation { latitude, longitude, accuracy? }'),
202
279
  userAgent: z.string().optional().describe('Override the browser User-Agent string'),
203
280
  // ── Auth & headers ──
204
- cookies: z.array(
205
- z.union([
206
- z.string(),
207
- z.object({
208
- name: z.string(),
209
- value: z.string(),
210
- domain: z.string().optional(),
211
- }),
212
- ])
213
- ).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'),
214
282
  headers: z.record(z.string(), z.string()).optional().describe('Extra HTTP headers to send with the request'),
215
283
  authorization: z.string().optional().describe('Authorization header value (e.g. "Bearer <token>")'),
216
284
  bypassCSP: z.boolean().optional().describe('Bypass Content-Security-Policy on the page'),
217
285
  // ── Content manipulation ──
218
286
  hideSelectors: z.array(z.string()).optional().describe('Array of CSS selectors to hide before capture'),
219
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 ──
220
291
  blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
221
292
  blockAds: z.boolean().optional().describe('Block advertisements on the page'),
222
293
  blockChats: z.boolean().optional().describe('Block live chat widgets on the page'),
223
294
  blockTrackers: z.boolean().optional().describe('Block tracking scripts on the page'),
224
- // ── 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 ──
225
298
  extractMetadata: z.boolean().optional().describe('Extract page metadata (title, description, OG tags) alongside the screenshot'),
299
+ // ── Styling ──
300
+ style: styleSchema,
226
301
  },
227
302
  async (params) => {
228
303
  if (!params.url && !params.html && !params.markdown) {
@@ -237,35 +312,57 @@ server.tool(
237
312
  const data = await res.json();
238
313
  const format = params.format || 'png';
239
314
 
240
- return {
241
- content: [
242
- {
243
- type: 'image',
244
- data: data.data,
245
- mimeType: imageMimeType(format),
246
- },
247
- {
248
- type: 'text',
249
- text: `Screenshot captured successfully. Format: ${format}, Size: ${data.size_bytes} bytes, Duration: ${data.duration_ms}ms`,
250
- },
251
- ],
252
- };
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 };
253
336
  }
254
337
  );
255
338
 
256
- // ─── Tool: generate_pdf ─────────────────────────────────────────
339
+ // ═══════════════════════════════════════════════════════════════════
340
+ // Tool: generate_pdf — COMPLETE coverage
341
+ // ═══════════════════════════════════════════════════════════════════
257
342
  server.tool(
258
343
  'generate_pdf',
259
- '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.',
260
345
  {
261
346
  url: z.string().url().optional().describe('URL to render as PDF (required if no html)'),
262
347
  html: z.string().optional().describe('Raw HTML to render as PDF (required if no url)'),
263
- 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)'),
264
349
  landscape: z.boolean().optional().describe('Landscape orientation (default: false)'),
265
350
  printBackground: z.boolean().optional().describe('Include CSS backgrounds (default: true)'),
266
- 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 }'),
267
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"'),
268
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)'),
269
366
  delay: z.number().int().min(0).max(10000).optional().describe('Milliseconds to wait before rendering (default: 0)'),
270
367
  saveTo: z.string().optional().describe('Output file path (default: ./output.pdf)'),
271
368
  },
@@ -281,18 +378,37 @@ server.tool(
281
378
  });
282
379
 
283
380
  const data = await res.json();
284
- const outputPath = resolve(saveTo || './output.pdf');
285
381
 
286
- // Decode base64 and write to disk
287
- const buffer = Buffer.from(data.data, 'base64');
288
- 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)`;
289
397
 
290
398
  return {
291
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
+ },
292
408
  {
293
409
  type: 'text',
294
410
  text: `PDF generated successfully.\n` +
295
- ` File: ${outputPath}\n` +
411
+ `${fileNote}\n` +
296
412
  ` Size: ${data.size_bytes} bytes\n` +
297
413
  ` Duration: ${data.duration_ms}ms`,
298
414
  },
@@ -301,13 +417,15 @@ server.tool(
301
417
  }
302
418
  );
303
419
 
304
- // ─── Tool: create_og_image ──────────────────────────────────────
420
+ // ═══════════════════════════════════════════════════════════════════
421
+ // Tool: create_og_image — COMPLETE coverage
422
+ // ═══════════════════════════════════════════════════════════════════
305
423
  server.tool(
306
424
  'create_og_image',
307
425
  'Generate an Open Graph / social card image. Returns an image using built-in templates or custom HTML.',
308
426
  {
309
427
  template: z.enum(['default', 'minimal', 'gradient']).optional().describe('Built-in template name (default: "default")'),
310
- 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+)'),
311
429
  title: z.string().optional().describe('Main title text (default: "Your Title Here")'),
312
430
  subtitle: z.string().optional().describe('Subtitle text'),
313
431
  logo: z.string().optional().describe('Logo image URL'),
@@ -344,7 +462,9 @@ server.tool(
344
462
  }
345
463
  );
346
464
 
347
- // ─── Tool: run_sequence ─────────────────────────────────────────
465
+ // ═══════════════════════════════════════════════════════════════════
466
+ // Tool: run_sequence — COMPLETE coverage
467
+ // ═══════════════════════════════════════════════════════════════════
348
468
  server.tool(
349
469
  'run_sequence',
350
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.',
@@ -352,7 +472,7 @@ server.tool(
352
472
  steps: z.array(
353
473
  z.object({
354
474
  action: z.enum([
355
- 'navigate', 'click', 'fill', 'select', 'hover',
475
+ 'navigate', 'click', 'dblclick', 'fill', 'select', 'hover',
356
476
  'scroll', 'wait', 'wait_for', 'evaluate',
357
477
  'screenshot', 'pdf',
358
478
  ]).describe('The action to perform'),
@@ -367,11 +487,16 @@ server.tool(
367
487
  name: z.string().optional().describe('Name for the output (for screenshot/pdf actions)'),
368
488
  format: z.string().optional().describe('Image format: png, jpeg, webp (screenshot) or A4, Letter (pdf)'),
369
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)'),
370
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)'),
371
495
  landscape: z.boolean().optional().describe('Landscape orientation (for pdf action)'),
372
496
  printBackground: z.boolean().optional().describe('Include CSS backgrounds (for pdf action)'),
373
497
  margin: z.string().optional().describe('CSS margin for all sides (for pdf action)'),
374
498
  scale: z.number().min(0.1).max(2).optional().describe('Rendering scale (for pdf action)'),
499
+ style: styleSchema,
375
500
  })
376
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.'),
377
502
  viewport: z.object({
@@ -380,6 +505,9 @@ server.tool(
380
505
  }).optional().describe('Browser viewport size'),
381
506
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
382
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'),
383
511
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio (default: 1)'),
384
512
  },
385
513
  async (params) => {
@@ -430,15 +558,17 @@ server.tool(
430
558
  }
431
559
  );
432
560
 
433
- // ─── Tool: record_video ─────────────────────────────────────────
561
+ // ═══════════════════════════════════════════════════════════════════
562
+ // Tool: record_video — COMPLETE coverage
563
+ // ═══════════════════════════════════════════════════════════════════
434
564
  server.tool(
435
565
  'record_video',
436
- '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.',
437
567
  {
438
568
  steps: z.array(
439
569
  z.object({
440
570
  action: z.enum([
441
- 'navigate', 'click', 'fill', 'select', 'hover',
571
+ 'navigate', 'click', 'dblclick', 'fill', 'select', 'hover',
442
572
  'scroll', 'wait', 'wait_for', 'evaluate',
443
573
  ]).describe('The action to perform (no screenshot/pdf — the whole sequence is recorded as video)'),
444
574
  url: z.string().url().optional().describe('URL to navigate to (for navigate action)'),
@@ -449,6 +579,12 @@ server.tool(
449
579
  x: z.number().optional().describe('Horizontal scroll position'),
450
580
  y: z.number().optional().describe('Vertical scroll position'),
451
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.'),
452
588
  })
453
589
  ).min(1).max(50).describe('Array of steps to execute and record. Max steps depends on plan (10-50).'),
454
590
  viewport: z.object({
@@ -457,31 +593,63 @@ server.tool(
457
593
  }).optional().describe('Browser viewport size'),
458
594
  format: z.enum(['mp4', 'webm', 'gif']).optional().describe('Video format (default: mp4). webm/gif require Starter+ plan.'),
459
595
  framerate: z.number().int().optional().describe('Frames per second: 24, 30, or 60 (default: 30)'),
596
+ // ── Cursor ──
460
597
  cursor: z.object({
461
598
  visible: z.boolean().optional().describe('Show cursor overlay (default: true)'),
462
- 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.'),
463
600
  color: z.string().optional().describe('Cursor color as hex, e.g. "#3B82F6" (default: blue)'),
464
601
  size: z.number().int().min(8).max(60).optional().describe('Cursor size in pixels (default: 20)'),
465
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)'),
466
605
  }).optional().describe('Cursor appearance settings'),
606
+ // ── Zoom (global defaults, per-step overrides available) ──
467
607
  zoom: z.object({
468
- enabled: z.boolean().optional().describe('Auto-zoom on clicks (default: true)'),
469
- level: z.number().min(1.5).max(4).optional().describe('Zoom magnification (default: 2.0)'),
470
- duration: z.number().int().min(200).max(2000).optional().describe('Zoom animation duration in ms (default: 600)'),
471
- }).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.'),
472
613
  autoZoom: z.boolean().optional().describe('Shorthand: set to true to enable auto-zoom with defaults (same as zoom.enabled=true)'),
614
+ // ── Click effects ──
473
615
  clickEffect: z.object({
474
616
  enabled: z.boolean().optional().describe('Show click ripple effects (default: true)'),
475
617
  style: z.enum(['ripple', 'pulse', 'ring']).optional().describe('Click effect style (default: ripple)'),
476
618
  color: z.string().optional().describe('Click effect color as hex'),
477
619
  }).optional().describe('Visual click effect settings'),
620
+ // ── Pace ──
478
621
  pace: z.union([
479
622
  z.number().min(0.25).max(6),
480
623
  z.enum(['fast', 'normal', 'slow', 'dramatic', 'cinematic']),
481
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 ──
482
646
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
483
- 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'),
484
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.'),
485
653
  saveTo: z.string().optional().describe('Output file path (default: ./recording.mp4)'),
486
654
  },
487
655
  async (params) => {
@@ -500,20 +668,42 @@ server.tool(
500
668
  const data = await res.json();
501
669
  const format = params.format || 'mp4';
502
670
  const ext = format === 'gif' ? 'gif' : format;
503
- const outputPath = resolve(saveTo || `./recording.${ext}`);
504
671
 
505
- // Decode base64 and write to disk
506
- const buffer = Buffer.from(data.data, 'base64');
507
- 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
+ }
508
687
 
509
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`;
510
692
 
511
693
  return {
512
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
+ },
513
703
  {
514
704
  type: 'text',
515
705
  text: `Video recorded successfully.\n` +
516
- ` File: ${outputPath}\n` +
706
+ fileNote +
517
707
  ` Format: ${data.format}\n` +
518
708
  ` Size: ${(data.size_bytes / 1024).toFixed(1)} KB\n` +
519
709
  ` Duration: ${durationSec}s\n` +
@@ -530,10 +720,12 @@ server.tool(
530
720
  }
531
721
  );
532
722
 
533
- // ─── Tool: inspect_page ─────────────────────────────────────────
723
+ // ═══════════════════════════════════════════════════════════════════
724
+ // Tool: inspect_page — COMPLETE coverage
725
+ // ═══════════════════════════════════════════════════════════════════
534
726
  server.tool(
535
727
  'inspect_page',
536
- '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.',
537
729
  {
538
730
  // ── Source ──
539
731
  url: z.string().url().optional().describe('URL to inspect (required if no html)'),
@@ -544,36 +736,39 @@ server.tool(
544
736
  viewportDevice: z.string().optional().describe('Device preset for viewport emulation (e.g. "iphone_14_pro"). Use list_devices to see all presets.'),
545
737
  viewportMobile: z.boolean().optional().describe('Enable mobile meta viewport emulation'),
546
738
  viewportHasTouch: z.boolean().optional().describe('Enable touch event emulation'),
739
+ viewportLandscape: z.boolean().optional().describe('Landscape orientation'),
547
740
  deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio (default: 1)'),
548
741
  // ── Timing ──
549
742
  waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().describe('When to consider navigation finished (default: networkidle2)'),
550
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)'),
551
745
  // ── Emulation ──
552
746
  darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
553
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'),
554
755
  userAgent: z.string().optional().describe('Override the browser User-Agent string'),
555
756
  // ── Auth & headers ──
556
- cookies: z.array(
557
- z.union([
558
- z.string(),
559
- z.object({
560
- name: z.string(),
561
- value: z.string(),
562
- domain: z.string().optional(),
563
- }),
564
- ])
565
- ).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'),
566
758
  headers: z.record(z.string(), z.string()).optional().describe('Extra HTTP headers to send with the request'),
567
759
  authorization: z.string().optional().describe('Authorization header value (e.g. "Bearer <token>")'),
568
760
  bypassCSP: z.boolean().optional().describe('Bypass Content-Security-Policy on the page'),
569
761
  // ── Content manipulation ──
570
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 ──
571
766
  blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
572
767
  blockAds: z.boolean().optional().describe('Block advertisements on the page'),
573
768
  blockChats: z.boolean().optional().describe('Block live chat widgets'),
574
769
  blockTrackers: z.boolean().optional().describe('Block tracking scripts'),
575
- injectCss: z.string().optional().describe('Custom CSS to inject before inspecting'),
576
- 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'),
577
772
  },
578
773
  async (params) => {
579
774
  if (!params.url && !params.html) {
@@ -591,7 +786,6 @@ server.tool(
591
786
  // Format as structured text for efficient LLM consumption
592
787
  const lines = [];
593
788
 
594
- // Header
595
789
  lines.push(`Page: ${data.title || '(untitled)'} (${data.url || params.url || 'html content'})`);
596
790
  if (data.metadata) {
597
791
  if (data.metadata.description) lines.push(`Description: ${data.metadata.description}`);
@@ -600,7 +794,6 @@ server.tool(
600
794
  }
601
795
  lines.push('');
602
796
 
603
- // Headings
604
797
  if (data.headings && data.headings.length > 0) {
605
798
  lines.push(`Headings (${data.headings.length}):`);
606
799
  for (const h of data.headings) {
@@ -609,7 +802,6 @@ server.tool(
609
802
  lines.push('');
610
803
  }
611
804
 
612
- // Interactive elements
613
805
  if (data.elements && data.elements.length > 0) {
614
806
  lines.push(`Interactive Elements (${data.elements.length}):`);
615
807
  for (const el of data.elements) {
@@ -625,7 +817,6 @@ server.tool(
625
817
  lines.push('');
626
818
  }
627
819
 
628
- // Forms
629
820
  if (data.forms && data.forms.length > 0) {
630
821
  lines.push(`Forms (${data.forms.length}):`);
631
822
  for (const f of data.forms) {
@@ -639,7 +830,6 @@ server.tool(
639
830
  lines.push('');
640
831
  }
641
832
 
642
- // Links
643
833
  if (data.links && data.links.length > 0) {
644
834
  lines.push(`Links (${data.links.length}):`);
645
835
  for (const l of data.links) {
@@ -648,7 +838,6 @@ server.tool(
648
838
  lines.push('');
649
839
  }
650
840
 
651
- // Images
652
841
  if (data.images && data.images.length > 0) {
653
842
  lines.push(`Images (${data.images.length}):`);
654
843
  for (const img of data.images) {
@@ -661,12 +850,7 @@ server.tool(
661
850
  lines.push(`Duration: ${data.duration_ms}ms`);
662
851
 
663
852
  return {
664
- content: [
665
- {
666
- type: 'text',
667
- text: lines.join('\n'),
668
- },
669
- ],
853
+ content: [{ type: 'text', text: lines.join('\n') }],
670
854
  };
671
855
  } catch (err) {
672
856
  return { content: [{ type: 'text', text: `Inspect error: ${err.message}` }], isError: true };
@@ -674,7 +858,9 @@ server.tool(
674
858
  }
675
859
  );
676
860
 
677
- // ─── Tool: list_devices ─────────────────────────────────────────
861
+ // ═══════════════════════════════════════════════════════════════════
862
+ // Tool: list_devices
863
+ // ═══════════════════════════════════════════════════════════════════
678
864
  server.tool(
679
865
  'list_devices',
680
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.',
@@ -703,7 +889,9 @@ server.tool(
703
889
  }
704
890
  );
705
891
 
706
- // ─── Tool: check_usage ──────────────────────────────────────────
892
+ // ═══════════════════════════════════════════════════════════════════
893
+ // Tool: check_usage
894
+ // ═══════════════════════════════════════════════════════════════════
707
895
  server.tool(
708
896
  'check_usage',
709
897
  'Check your current PageBolt API usage and plan limits.',
@@ -736,7 +924,6 @@ server.tool(
736
924
  // ─── Prompts ────────────────────────────────────────────────────
737
925
  function registerPrompts(server) {
738
926
 
739
- // ── Prompt: capture-page ──────────────────────────────────────
740
927
  server.prompt(
741
928
  'capture-page',
742
929
  'Capture a clean screenshot of any URL with sensible defaults. Optionally inspects the page first.',
@@ -745,11 +932,20 @@ function registerPrompts(server) {
745
932
  device: z.string().optional().describe('Device preset, e.g. "iphone_14_pro" or "macbook_pro_14"'),
746
933
  dark_mode: z.enum(['true', 'false']).optional().describe('Enable dark mode (default: false)'),
747
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.'),
748
941
  },
749
942
  (args) => {
750
943
  const device = args.device ? `\n- Use device preset: ${args.device}` : '';
751
944
  const dark = args.dark_mode === 'true' ? '\n- Enable dark mode' : '';
752
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
+ : '';
753
949
 
754
950
  return {
755
951
  messages: [
@@ -758,16 +954,15 @@ function registerPrompts(server) {
758
954
  content: {
759
955
  type: 'text',
760
956
  text: `Take a clean screenshot of ${args.url} with these settings:
761
- - Block banners, ads, chats, and trackers for a clean capture${device}${dark}${full}
957
+ - Block banners, ads, chats, and trackers for a clean capture${device}${dark}${full}${style}
762
958
  - Use PNG format
763
- - If the page looks complex or you need to verify elements, run inspect_page first
764
959
 
765
960
  Call take_screenshot with:
766
961
  url: "${args.url}"
767
962
  blockBanners: true
768
963
  blockAds: true
769
964
  blockChats: true
770
- 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' : ''}`,
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}" }` : ''}`,
771
966
  },
772
967
  },
773
968
  ],
@@ -775,7 +970,6 @@ Call take_screenshot with:
775
970
  }
776
971
  );
777
972
 
778
- // ── Prompt: record-demo ───────────────────────────────────────
779
973
  server.prompt(
780
974
  'record-demo',
781
975
  'Record a professional demo video of a web page or flow. Generates a step sequence automatically.',
@@ -784,10 +978,21 @@ Call take_screenshot with:
784
978
  description: z.string().describe('What the demo should show, e.g. "Sign in and explore the dashboard"'),
785
979
  pace: z.enum(['fast', 'normal', 'slow', 'dramatic', 'cinematic']).optional().describe('Video pace preset (default: normal)'),
786
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)'),
787
983
  },
788
984
  (args) => {
789
985
  const pace = args.pace || 'normal';
790
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
+ : '';
791
996
 
792
997
  return {
793
998
  messages: [
@@ -813,13 +1018,19 @@ Please follow this workflow:
813
1018
  - format: "${format}"
814
1019
  - pace: "${pace}"
815
1020
  - blockBanners: true
816
- - cursor: { style: "spotlight", color: "#6366f1" }
817
- - clickEffect: { style: "ripple", color: "#6366f1" }
1021
+ - cursor: { style: "classic", visible: true, persist: true }
1022
+ - clickEffect: { style: "ripple" }${frameConfig}${bgConfig}
818
1023
 
819
1024
  Important tips:
820
1025
  - Use selectors from the inspect_page results — never guess selectors
821
- - Add scroll actions between sections to show content naturally
1026
+ - Add wait steps (ms: 800-1200) between interactions for visual clarity
822
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)
823
1034
  - Keep to 15 steps or fewer for best results
824
1035
  - Each video costs 3 API requests`,
825
1036
  },
@@ -829,7 +1040,6 @@ Important tips:
829
1040
  }
830
1041
  );
831
1042
 
832
- // ── Prompt: audit-page ────────────────────────────────────────
833
1043
  server.prompt(
834
1044
  'audit-page',
835
1045
  'Inspect a page and return a structured analysis of its elements, forms, links, and interactive components.',
@@ -872,14 +1082,11 @@ Important tips:
872
1082
  // ─── Resources ──────────────────────────────────────────────────
873
1083
  function registerResources(server) {
874
1084
 
875
- // ── Resource: pagebolt://api-docs ─────────────────────────────
876
1085
  server.resource(
877
1086
  'api-docs',
878
1087
  'pagebolt://api-docs',
879
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' },
880
1089
  async () => {
881
- // Serve a comprehensive API reference. In production this is baked in;
882
- // we could also fetch /llms-full.txt but embedding avoids a network call.
883
1090
  try {
884
1091
  const res = await fetch(`${BASE_URL}/llms-full.txt`);
885
1092
  if (res.ok) {
@@ -901,7 +1108,7 @@ function registerResources(server) {
901
1108
 
902
1109
  } // end registerResources
903
1110
 
904
- // ─── Smithery sandbox export (for scanning tools without credentials) ─
1111
+ // ─── Smithery sandbox export ─────────────────────────────────────
905
1112
  export function createSandboxServer() {
906
1113
  return createConfiguredServer();
907
1114
  }