pagebolt-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PageBolt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # PageBolt MCP Server
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pagebolt-mcp.svg)](https://www.npmjs.com/package/pagebolt-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![MCP](https://img.shields.io/badge/MCP-compatible-brightgreen)](https://modelcontextprotocol.io)
6
+
7
+ Take screenshots, generate PDFs, and create OG images directly from your AI coding assistant.
8
+
9
+ **Works with Claude Desktop, Cursor, Windsurf, Cline, and any MCP-compatible client.**
10
+
11
+ <p align="center">
12
+ <img src="https://pagebolt.dev/og-image-default.png" alt="PageBolt" width="600" />
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## What It Does
18
+
19
+ PageBolt MCP Server connects your AI assistant to [PageBolt's web capture API](https://pagebolt.dev), giving it the ability to:
20
+
21
+ - **Take screenshots** of any URL, HTML, or Markdown (30+ parameters)
22
+ - **Generate PDFs** from URLs or HTML (invoices, reports, docs)
23
+ - **Create OG images** for social cards using templates or custom HTML
24
+ - **Run browser sequences** — multi-step automation (navigate, click, fill, screenshot)
25
+ - **List device presets** — 25+ devices (iPhone, iPad, MacBook, Galaxy, etc.)
26
+ - **Check usage** — monitor your API quota in real time
27
+
28
+ All results are returned inline — screenshots appear directly in your chat.
29
+
30
+ ---
31
+
32
+ ## Quick Start
33
+
34
+ ### 1. Get a free API key
35
+
36
+ Sign up at [pagebolt.dev](https://pagebolt.dev) — the free tier includes 100 requests/month, no credit card required.
37
+
38
+ ### 2. Install & configure
39
+
40
+ #### Claude Desktop
41
+
42
+ Add to `~/.claude/claude_desktop_config.json`:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "pagebolt": {
48
+ "command": "npx",
49
+ "args": ["-y", "pagebolt-mcp"],
50
+ "env": {
51
+ "PAGEBOLT_API_KEY": "pf_live_your_key_here"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ #### Cursor
59
+
60
+ Add to `.cursor/mcp.json` in your project (or global config):
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "pagebolt": {
66
+ "command": "npx",
67
+ "args": ["-y", "pagebolt-mcp"],
68
+ "env": {
69
+ "PAGEBOLT_API_KEY": "pf_live_your_key_here"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ #### Windsurf
77
+
78
+ Add to your Windsurf MCP settings:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "pagebolt": {
84
+ "command": "npx",
85
+ "args": ["-y", "pagebolt-mcp"],
86
+ "env": {
87
+ "PAGEBOLT_API_KEY": "pf_live_your_key_here"
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ #### Cline / Other MCP Clients
95
+
96
+ Same config pattern — set `command` to `npx`, `args` to `["-y", "pagebolt-mcp"]`, and provide your API key in `env`.
97
+
98
+ ### 3. Try it
99
+
100
+ Ask your AI assistant:
101
+
102
+ > "Take a screenshot of https://github.com in dark mode at 1920x1080"
103
+
104
+ The screenshot will appear inline in your chat.
105
+
106
+ ---
107
+
108
+ ## Tools
109
+
110
+ ### `take_screenshot`
111
+
112
+ Capture a pixel-perfect screenshot of any URL, HTML, or Markdown.
113
+
114
+ **Key parameters:**
115
+ - `url` / `html` / `markdown` — content source
116
+ - `width`, `height` — viewport size (default: 1280x720)
117
+ - `viewportDevice` — device preset (e.g. `"iphone_14_pro"`, `"macbook_pro_14"`)
118
+ - `fullPage` — capture the entire scrollable page
119
+ - `darkMode` — emulate dark color scheme
120
+ - `format` — `png`, `jpeg`, or `webp`
121
+ - `blockBanners` — hide cookie consent banners
122
+ - `blockAds` — block advertisements
123
+ - `blockChats` — remove live chat widgets
124
+ - `blockTrackers` — block tracking scripts
125
+ - `extractMetadata` — get page title, description, OG tags alongside the screenshot
126
+ - `selector` — capture a specific DOM element
127
+ - `delay` — wait before capture (for animations)
128
+ - `cookies`, `headers`, `authorization` — authenticated captures
129
+ - `geolocation`, `timeZone` — location emulation
130
+ - ...and 15+ more
131
+
132
+ **Example prompts:**
133
+ - "Screenshot https://example.com on an iPhone 14 Pro"
134
+ - "Take a full-page screenshot of https://news.ycombinator.com with ad blocking"
135
+ - "Capture this HTML in dark mode: `<h1>Hello World</h1>`"
136
+
137
+ ### `generate_pdf`
138
+
139
+ Generate a PDF from any URL or HTML content.
140
+
141
+ **Parameters:** `url`/`html`, `format` (A4/Letter/Legal), `landscape`, `margin`, `scale`, `pageRanges`, `delay`, `saveTo`
142
+
143
+ **Example prompts:**
144
+ - "Generate a PDF of https://example.com and save it to ./report.pdf"
145
+ - "Create a PDF from this invoice HTML in Letter format, landscape"
146
+
147
+ ### `create_og_image`
148
+
149
+ Create Open Graph / social preview images.
150
+
151
+ **Parameters:** `template` (default/minimal/gradient), `html` (custom), `title`, `subtitle`, `logo`, `bgColor`, `textColor`, `accentColor`, `width`, `height`, `format`
152
+
153
+ **Example prompts:**
154
+ - "Create an OG image with title 'How to Build a SaaS' using the gradient template"
155
+ - "Generate a social card with a dark blue background and white text"
156
+
157
+ ### `run_sequence`
158
+
159
+ Execute multi-step browser automation.
160
+
161
+ **Actions:** `navigate`, `click`, `fill`, `select`, `hover`, `scroll`, `wait`, `wait_for`, `evaluate`, `screenshot`, `pdf`
162
+
163
+ **Example prompts:**
164
+ - "Go to https://example.com, click the pricing link, then screenshot both pages"
165
+ - "Navigate to the login page, fill in test credentials, submit, and screenshot the dashboard"
166
+
167
+ ### `list_devices`
168
+
169
+ List all 25+ available device presets with viewport dimensions.
170
+
171
+ **Example prompt:**
172
+ - "What device presets are available for screenshots?"
173
+
174
+ ### `check_usage`
175
+
176
+ Check your current API usage and plan limits.
177
+
178
+ **Example prompt:**
179
+ - "How many API requests do I have left this month?"
180
+
181
+ ---
182
+
183
+ ## Configuration
184
+
185
+ | Environment Variable | Required | Default | Description |
186
+ |---------------------|----------|---------|-------------|
187
+ | `PAGEBOLT_API_KEY` | **Yes** | — | Your PageBolt API key ([get one free](https://pagebolt.dev)) |
188
+ | `PAGEBOLT_BASE_URL` | No | `https://pagebolt.dev` | API base URL |
189
+
190
+ ---
191
+
192
+ ## Pricing
193
+
194
+ | Plan | Price | Requests/mo | Rate Limit |
195
+ |------|-------|-------------|------------|
196
+ | **Free** | $0 | 100 | 10 req/min |
197
+ | Starter | $29/mo | 5,000 | 60 req/min |
198
+ | Growth | $79/mo | 25,000 | 120 req/min |
199
+ | Scale | $199/mo | 100,000 | 300 req/min |
200
+
201
+ Free plan requires no credit card. Starter and Growth include a 14-day free trial.
202
+
203
+ ---
204
+
205
+ ## Why PageBolt?
206
+
207
+ - **5 APIs, one key** — screenshot, PDF, OG image, browser automation, and MCP server. Stop paying for separate tools.
208
+ - **Clean captures** — automatic ad blocking, cookie banner removal, chat widget suppression, tracker blocking.
209
+ - **25+ device presets** — iPhone SE to Galaxy S24 Ultra, iPad Pro, MacBook, Desktop 4K.
210
+ - **Ship in 5 minutes** — plain HTTP, no SDKs required, works in any language.
211
+ - **Inline results** — screenshots and OG images appear directly in your AI chat.
212
+
213
+ ---
214
+
215
+ ## Links
216
+
217
+ - **Website:** [pagebolt.dev](https://pagebolt.dev)
218
+ - **API Docs:** [pagebolt.dev/docs.html](https://pagebolt.dev/docs.html)
219
+ - **npm:** [npmjs.com/package/pagebolt-mcp](https://www.npmjs.com/package/pagebolt-mcp)
220
+ - **Issues:** [github.com/Custodia-Admin/pagebolt-mcp/issues](https://github.com/Custodia-Admin/pagebolt-mcp/issues)
221
+
222
+ ---
223
+
224
+ ## License
225
+
226
+ MIT
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "pagebolt-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for PageBolt — take screenshots, generate PDFs, and create OG images from AI coding assistants like Claude, Cursor, and Windsurf.",
5
+ "main": "src/index.mjs",
6
+ "bin": {
7
+ "pagebolt-mcp": "src/index.mjs"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node src/index.mjs"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "mcp-server",
16
+ "model-context-protocol",
17
+ "screenshot",
18
+ "screenshot-api",
19
+ "pdf",
20
+ "pdf-generation",
21
+ "og-image",
22
+ "open-graph",
23
+ "social-card",
24
+ "browser-automation",
25
+ "puppeteer",
26
+ "web-capture",
27
+ "pagebolt",
28
+ "ai-tools",
29
+ "claude",
30
+ "cursor",
31
+ "windsurf",
32
+ "cline",
33
+ "devtools"
34
+ ],
35
+ "author": "PageBolt <hello@pagebolt.dev> (https://pagebolt.dev)",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/Custodia-Admin/pagebolt-mcp.git"
40
+ },
41
+ "homepage": "https://pagebolt.dev",
42
+ "bugs": {
43
+ "url": "https://github.com/Custodia-Admin/pagebolt-mcp/issues"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "files": [
49
+ "src/",
50
+ "README.md",
51
+ "LICENSE",
52
+ "server.json"
53
+ ],
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.26.0",
56
+ "zod": "^4.3.6"
57
+ }
58
+ }
package/server.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "pagebolt",
3
+ "version": "1.0.0",
4
+ "description": "Take screenshots, generate PDFs, and create OG images from AI coding assistants.",
5
+ "homepage": "https://pagebolt.dev",
6
+ "repository": "https://github.com/Custodia-Admin/pagebolt-mcp",
7
+ "tools": [
8
+ {
9
+ "name": "take_screenshot",
10
+ "description": "Capture a screenshot of a URL, HTML, or Markdown. 30+ parameters including device emulation, ad blocking, dark mode, geolocation, and more."
11
+ },
12
+ {
13
+ "name": "generate_pdf",
14
+ "description": "Generate a PDF from a URL or HTML content. Supports A4/Letter/Legal, margins, landscape, and page ranges."
15
+ },
16
+ {
17
+ "name": "create_og_image",
18
+ "description": "Create Open Graph / social card images from templates or custom HTML."
19
+ },
20
+ {
21
+ "name": "run_sequence",
22
+ "description": "Multi-step browser automation: navigate, click, fill, screenshot — all in one session."
23
+ },
24
+ {
25
+ "name": "list_devices",
26
+ "description": "List 25+ device presets for viewport emulation (iPhone, iPad, MacBook, etc.)."
27
+ },
28
+ {
29
+ "name": "check_usage",
30
+ "description": "Check current API usage and plan limits."
31
+ }
32
+ ],
33
+ "config": {
34
+ "required": {
35
+ "PAGEBOLT_API_KEY": "Your PageBolt API key (get one free at https://pagebolt.dev)"
36
+ },
37
+ "optional": {
38
+ "PAGEBOLT_BASE_URL": "API base URL (default: https://pagebolt.dev)"
39
+ }
40
+ }
41
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PageBolt MCP Server
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.).
9
+ *
10
+ * Get your free API key at https://pagebolt.dev
11
+ *
12
+ * Configuration (environment variables):
13
+ * PAGEBOLT_API_KEY — Required. Your PageBolt API key.
14
+ * 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
+ */
21
+
22
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24
+ import { z } from 'zod';
25
+ import { writeFileSync } from 'node:fs';
26
+ import { resolve } from 'node:path';
27
+
28
+ // ─── Configuration ───────────────────────────────────────────────
29
+ const API_KEY = process.env.PAGEBOLT_API_KEY;
30
+ const BASE_URL = (process.env.PAGEBOLT_BASE_URL || 'https://pagebolt.dev').replace(/\/$/, '');
31
+
32
+ if (!API_KEY) {
33
+ console.error(
34
+ 'ERROR: PAGEBOLT_API_KEY environment variable is required.\n\n' +
35
+ 'Get your free API key at https://pagebolt.dev\n\n' +
36
+ 'Then set it in your MCP client config:\n\n' +
37
+ ' Claude Desktop (~/.claude/claude_desktop_config.json):\n' +
38
+ ' "env": { "PAGEBOLT_API_KEY": "pf_live_..." }\n\n' +
39
+ ' Cursor (.cursor/mcp.json):\n' +
40
+ ' "env": { "PAGEBOLT_API_KEY": "pf_live_..." }\n'
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ // ─── HTTP helper ─────────────────────────────────────────────────
46
+ async function callApi(endpoint, options = {}) {
47
+ const url = `${BASE_URL}${endpoint}`;
48
+ const method = options.method || 'GET';
49
+ const headers = {
50
+ 'x-api-key': API_KEY,
51
+ 'user-agent': 'pagebolt-mcp/1.0.0',
52
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
53
+ };
54
+
55
+ const res = await fetch(url, {
56
+ method,
57
+ headers,
58
+ body: options.body ? JSON.stringify(options.body) : undefined,
59
+ });
60
+
61
+ if (!res.ok) {
62
+ let errorMsg;
63
+ try {
64
+ const errJson = await res.json();
65
+ errorMsg = errJson.error || JSON.stringify(errJson);
66
+ } catch {
67
+ errorMsg = `HTTP ${res.status} ${res.statusText}`;
68
+ }
69
+ throw new Error(`PageBolt API error: ${errorMsg}`);
70
+ }
71
+
72
+ return res;
73
+ }
74
+
75
+ // ─── MIME type helper ────────────────────────────────────────────
76
+ function imageMimeType(format) {
77
+ const map = { png: 'image/png', jpeg: 'image/jpeg', jpg: 'image/jpeg', webp: 'image/webp' };
78
+ return map[format] || 'image/png';
79
+ }
80
+
81
+ // ─── Create MCP Server ──────────────────────────────────────────
82
+ const server = new McpServer({
83
+ name: 'pagebolt',
84
+ version: '1.0.0',
85
+ });
86
+
87
+ // ─── Tool: take_screenshot ──────────────────────────────────────
88
+ server.tool(
89
+ 'take_screenshot',
90
+ '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).',
91
+ {
92
+ // ── Source ──
93
+ url: z.string().url().optional().describe('URL to capture (required if no html/markdown)'),
94
+ html: z.string().optional().describe('Raw HTML to render (required if no url/markdown)'),
95
+ markdown: z.string().optional().describe('Render Markdown content as a screenshot'),
96
+ // ── Viewport ──
97
+ width: z.number().int().min(1).max(3840).optional().describe('Viewport width in pixels (default: 1280)'),
98
+ height: z.number().int().min(1).max(2160).optional().describe('Viewport height in pixels (default: 720)'),
99
+ 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.'),
100
+ viewportMobile: z.boolean().optional().describe('Enable mobile meta viewport emulation'),
101
+ viewportHasTouch: z.boolean().optional().describe('Enable touch event emulation'),
102
+ deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio, use 2 for retina (default: 1)'),
103
+ // ── Output format ──
104
+ format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format (default: png)'),
105
+ quality: z.number().int().min(1).max(100).optional().describe('JPEG/WebP quality 1-100 (default: 80)'),
106
+ omitBackground: z.boolean().optional().describe('Transparent background (PNG/WebP only)'),
107
+ // ── Capture region ──
108
+ fullPage: z.boolean().optional().describe('Capture the full scrollable page (default: false)'),
109
+ fullPageScroll: z.boolean().optional().describe('Auto-scroll page before capture to trigger lazy-loaded images'),
110
+ fullPageMaxHeight: z.number().int().optional().describe('Maximum pixel height cap for full-page captures'),
111
+ selector: z.string().optional().describe('CSS selector to capture a specific element'),
112
+ clip: z.object({
113
+ x: z.number(),
114
+ y: z.number(),
115
+ width: z.number(),
116
+ height: z.number(),
117
+ }).optional().describe('Crop region { x, y, width, height } in pixels'),
118
+ // ── Timing ──
119
+ delay: z.number().int().min(0).max(10000).optional().describe('Milliseconds to wait before capture (default: 0)'),
120
+ waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().describe('When to consider navigation finished (default: networkidle2)'),
121
+ waitForSelector: z.string().optional().describe('Wait for this CSS selector to appear before capturing'),
122
+ // ── Emulation ──
123
+ darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
124
+ reducedMotion: z.boolean().optional().describe('Emulate prefers-reduced-motion to disable animations'),
125
+ mediaType: z.enum(['screen', 'print']).optional().describe('Emulate CSS media type'),
126
+ timeZone: z.string().optional().describe('Override browser timezone (e.g. "America/New_York")'),
127
+ geolocation: z.object({
128
+ latitude: z.number(),
129
+ longitude: z.number(),
130
+ accuracy: z.number().optional(),
131
+ }).optional().describe('Emulate geolocation { latitude, longitude, accuracy? }'),
132
+ userAgent: z.string().optional().describe('Override the browser User-Agent string'),
133
+ // ── Auth & headers ──
134
+ cookies: z.array(
135
+ z.union([
136
+ z.string(),
137
+ z.object({
138
+ name: z.string(),
139
+ value: z.string(),
140
+ domain: z.string().optional(),
141
+ }),
142
+ ])
143
+ ).optional().describe('Cookies to set — array of "name=value" strings or { name, value, domain? } objects'),
144
+ headers: z.record(z.string()).optional().describe('Extra HTTP headers to send with the request'),
145
+ authorization: z.string().optional().describe('Authorization header value (e.g. "Bearer <token>")'),
146
+ bypassCSP: z.boolean().optional().describe('Bypass Content-Security-Policy on the page'),
147
+ // ── Content manipulation ──
148
+ hideSelectors: z.array(z.string()).optional().describe('Array of CSS selectors to hide before capture'),
149
+ click: z.string().optional().describe('CSS selector to click before capturing the screenshot'),
150
+ blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
151
+ blockAds: z.boolean().optional().describe('Block advertisements on the page'),
152
+ blockChats: z.boolean().optional().describe('Block live chat widgets on the page'),
153
+ blockTrackers: z.boolean().optional().describe('Block tracking scripts on the page'),
154
+ // ── Extras ──
155
+ extractMetadata: z.boolean().optional().describe('Extract page metadata (title, description, OG tags) alongside the screenshot'),
156
+ },
157
+ async (params) => {
158
+ if (!params.url && !params.html && !params.markdown) {
159
+ return { content: [{ type: 'text', text: 'Error: One of "url", "html", or "markdown" is required.' }], isError: true };
160
+ }
161
+
162
+ const res = await callApi('/api/v1/screenshot', {
163
+ method: 'POST',
164
+ body: { ...params, response_type: 'json' },
165
+ });
166
+
167
+ const data = await res.json();
168
+ const format = params.format || 'png';
169
+
170
+ return {
171
+ content: [
172
+ {
173
+ type: 'image',
174
+ data: data.data,
175
+ mimeType: imageMimeType(format),
176
+ },
177
+ {
178
+ type: 'text',
179
+ text: `Screenshot captured successfully. Format: ${format}, Size: ${data.size_bytes} bytes, Duration: ${data.duration_ms}ms`,
180
+ },
181
+ ],
182
+ };
183
+ }
184
+ );
185
+
186
+ // ─── Tool: generate_pdf ─────────────────────────────────────────
187
+ server.tool(
188
+ 'generate_pdf',
189
+ 'Generate a PDF from a URL or HTML content. Saves the PDF to disk and returns the file path.',
190
+ {
191
+ url: z.string().url().optional().describe('URL to render as PDF (required if no html)'),
192
+ html: z.string().optional().describe('Raw HTML to render as PDF (required if no url)'),
193
+ format: z.string().optional().describe('Paper format: A4, Letter, Legal, Tabloid (default: A4)'),
194
+ landscape: z.boolean().optional().describe('Landscape orientation (default: false)'),
195
+ printBackground: z.boolean().optional().describe('Include CSS backgrounds (default: true)'),
196
+ margin: z.string().optional().describe('CSS margin for all sides, e.g. "1cm" or "0.5in"'),
197
+ scale: z.number().min(0.1).max(2).optional().describe('Rendering scale 0.1-2 (default: 1)'),
198
+ pageRanges: z.string().optional().describe('Page ranges to include, e.g. "1-5, 8"'),
199
+ delay: z.number().int().min(0).max(10000).optional().describe('Milliseconds to wait before rendering (default: 0)'),
200
+ saveTo: z.string().optional().describe('Output file path (default: ./output.pdf)'),
201
+ },
202
+ async (params) => {
203
+ if (!params.url && !params.html) {
204
+ return { content: [{ type: 'text', text: 'Error: Either "url" or "html" is required.' }], isError: true };
205
+ }
206
+
207
+ const { saveTo, ...apiParams } = params;
208
+ const res = await callApi('/api/v1/pdf', {
209
+ method: 'POST',
210
+ body: { ...apiParams, response_type: 'json' },
211
+ });
212
+
213
+ const data = await res.json();
214
+ const outputPath = resolve(saveTo || './output.pdf');
215
+
216
+ // Decode base64 and write to disk
217
+ const buffer = Buffer.from(data.data, 'base64');
218
+ writeFileSync(outputPath, buffer);
219
+
220
+ return {
221
+ content: [
222
+ {
223
+ type: 'text',
224
+ text: `PDF generated successfully.\n` +
225
+ ` File: ${outputPath}\n` +
226
+ ` Size: ${data.size_bytes} bytes\n` +
227
+ ` Duration: ${data.duration_ms}ms`,
228
+ },
229
+ ],
230
+ };
231
+ }
232
+ );
233
+
234
+ // ─── Tool: create_og_image ──────────────────────────────────────
235
+ server.tool(
236
+ 'create_og_image',
237
+ 'Generate an Open Graph / social card image. Returns an image using built-in templates or custom HTML.',
238
+ {
239
+ template: z.enum(['default', 'minimal', 'gradient']).optional().describe('Built-in template name (default: "default")'),
240
+ html: z.string().optional().describe('Custom HTML template (overrides template parameter)'),
241
+ title: z.string().optional().describe('Main title text (default: "Your Title Here")'),
242
+ subtitle: z.string().optional().describe('Subtitle text'),
243
+ logo: z.string().optional().describe('Logo image URL'),
244
+ bgColor: z.string().optional().describe('Background color as hex, e.g. "#0f172a"'),
245
+ textColor: z.string().optional().describe('Text color as hex, e.g. "#f8fafc"'),
246
+ accentColor: z.string().optional().describe('Accent color as hex, e.g. "#6366f1"'),
247
+ bgImage: z.string().optional().describe('Background image URL'),
248
+ width: z.number().int().min(1).max(2400).optional().describe('Image width in pixels (default: 1200)'),
249
+ height: z.number().int().min(1).max(1260).optional().describe('Image height in pixels (default: 630)'),
250
+ format: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format (default: png)'),
251
+ },
252
+ async (params) => {
253
+ const res = await callApi('/api/v1/og-image', {
254
+ method: 'POST',
255
+ body: { ...params, response_type: 'json' },
256
+ });
257
+
258
+ const data = await res.json();
259
+ const format = params.format || 'png';
260
+
261
+ return {
262
+ content: [
263
+ {
264
+ type: 'image',
265
+ data: data.data,
266
+ mimeType: imageMimeType(format),
267
+ },
268
+ {
269
+ type: 'text',
270
+ text: `OG image created successfully. Format: ${format}, Size: ${data.size_bytes} bytes, Duration: ${data.duration_ms}ms`,
271
+ },
272
+ ],
273
+ };
274
+ }
275
+ );
276
+
277
+ // ─── Tool: run_sequence ─────────────────────────────────────────
278
+ server.tool(
279
+ 'run_sequence',
280
+ '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.',
281
+ {
282
+ steps: z.array(
283
+ z.object({
284
+ action: z.enum([
285
+ 'navigate', 'click', 'fill', 'select', 'hover',
286
+ 'scroll', 'wait', 'wait_for', 'evaluate',
287
+ 'screenshot', 'pdf',
288
+ ]).describe('The action to perform'),
289
+ url: z.string().url().optional().describe('URL to navigate to (for navigate action)'),
290
+ selector: z.string().optional().describe('CSS selector for the target element'),
291
+ value: z.string().optional().describe('Value to type or select'),
292
+ ms: z.number().int().min(0).max(10000).optional().describe('Milliseconds to wait (for wait action)'),
293
+ timeout: z.number().int().min(0).max(15000).optional().describe('Timeout in ms for wait_for (default: 10000)'),
294
+ x: z.number().optional().describe('Horizontal scroll position'),
295
+ y: z.number().optional().describe('Vertical scroll position'),
296
+ script: z.string().max(5000).optional().describe('JavaScript to execute in page context (for evaluate action)'),
297
+ name: z.string().optional().describe('Name for the output (for screenshot/pdf actions)'),
298
+ format: z.string().optional().describe('Image format: png, jpeg, webp (screenshot) or A4, Letter (pdf)'),
299
+ fullPage: z.boolean().optional().describe('Capture full scrollable page (for screenshot action)'),
300
+ quality: z.number().int().min(1).max(100).optional().describe('JPEG/WebP quality (for screenshot action)'),
301
+ landscape: z.boolean().optional().describe('Landscape orientation (for pdf action)'),
302
+ printBackground: z.boolean().optional().describe('Include CSS backgrounds (for pdf action)'),
303
+ margin: z.string().optional().describe('CSS margin for all sides (for pdf action)'),
304
+ scale: z.number().min(0.1).max(2).optional().describe('Rendering scale (for pdf action)'),
305
+ })
306
+ ).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.'),
307
+ viewport: z.object({
308
+ width: z.number().int().min(320).max(3840).optional().describe('Viewport width (default: 1280)'),
309
+ height: z.number().int().min(200).max(2160).optional().describe('Viewport height (default: 720)'),
310
+ }).optional().describe('Browser viewport size'),
311
+ darkMode: z.boolean().optional().describe('Emulate dark color scheme (default: false)'),
312
+ blockBanners: z.boolean().optional().describe('Hide cookie consent banners (default: false)'),
313
+ deviceScaleFactor: z.number().min(1).max(3).optional().describe('Device pixel ratio (default: 1)'),
314
+ },
315
+ async (params) => {
316
+ if (!params.steps || params.steps.length === 0) {
317
+ return { content: [{ type: 'text', text: 'Error: "steps" must be a non-empty array.' }], isError: true };
318
+ }
319
+
320
+ try {
321
+ const res = await callApi('/api/v1/sequence', {
322
+ method: 'POST',
323
+ body: params,
324
+ });
325
+
326
+ const data = await res.json();
327
+ const content = [];
328
+
329
+ for (const output of data.outputs) {
330
+ if (output.type === 'screenshot') {
331
+ content.push({
332
+ type: 'image',
333
+ data: output.data,
334
+ mimeType: output.content_type,
335
+ });
336
+ content.push({
337
+ type: 'text',
338
+ text: `[${output.name}] Screenshot — ${output.format}, ${output.size_bytes} bytes, step ${output.step_index}`,
339
+ });
340
+ } else if (output.type === 'pdf') {
341
+ content.push({
342
+ type: 'text',
343
+ text: `[${output.name}] PDF generated — ${output.format}, ${output.size_bytes} bytes, step ${output.step_index} (base64 data available in raw response)`,
344
+ });
345
+ }
346
+ }
347
+
348
+ const failedSteps = data.step_results.filter(s => s.status === 'error');
349
+ let summary = `Sequence complete: ${data.steps_completed}/${data.total_steps} steps, ${data.outputs.length} outputs, ${data.total_duration_ms}ms total.`;
350
+ if (failedSteps.length > 0) {
351
+ summary += `\nFailed steps: ${failedSteps.map(s => `Step ${s.step_index} (${s.action}): ${s.error}`).join('; ')}`;
352
+ }
353
+ summary += `\nUsage: ${data.usage.outputs_charged} request(s) charged, ${data.usage.remaining} remaining.`;
354
+
355
+ content.push({ type: 'text', text: summary });
356
+ return { content };
357
+ } catch (err) {
358
+ return { content: [{ type: 'text', text: `Sequence error: ${err.message}` }], isError: true };
359
+ }
360
+ }
361
+ );
362
+
363
+ // ─── Tool: list_devices ─────────────────────────────────────────
364
+ server.tool(
365
+ 'list_devices',
366
+ '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.',
367
+ {},
368
+ async () => {
369
+ const res = await callApi('/api/v1/devices');
370
+ const data = await res.json();
371
+
372
+ const lines = data.devices.map((d) => {
373
+ const touch = d.hasTouch ? ', touch' : '';
374
+ const mobile = d.isMobile ? ', mobile' : '';
375
+ return ` ${d.name} — ${d.viewport.width}x${d.viewport.height} @${d.viewport.deviceScaleFactor}x${mobile}${touch}`;
376
+ });
377
+
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text:
383
+ `Available device presets (${data.devices.length}):\n` +
384
+ lines.join('\n') +
385
+ `\n\nUse the device name as the "viewportDevice" parameter in take_screenshot.`,
386
+ },
387
+ ],
388
+ };
389
+ }
390
+ );
391
+
392
+ // ─── Tool: check_usage ──────────────────────────────────────────
393
+ server.tool(
394
+ 'check_usage',
395
+ 'Check your current PageBolt API usage and plan limits.',
396
+ {},
397
+ async () => {
398
+ const res = await callApi('/api/v1/usage');
399
+ const data = await res.json();
400
+
401
+ const { plan, usage } = data;
402
+ const pct = usage.limit > 0 ? Math.round((usage.current / usage.limit) * 100) : 0;
403
+
404
+ return {
405
+ content: [
406
+ {
407
+ type: 'text',
408
+ text:
409
+ `PageBolt Usage\n` +
410
+ ` Plan: ${plan}\n` +
411
+ ` Used: ${usage.current.toLocaleString()} / ${usage.limit.toLocaleString()} requests\n` +
412
+ ` Remaining: ${usage.remaining.toLocaleString()}\n` +
413
+ ` Usage: ${pct}%`,
414
+ },
415
+ ],
416
+ };
417
+ }
418
+ );
419
+
420
+ // ─── Start ──────────────────────────────────────────────────────
421
+ const transport = new StdioServerTransport();
422
+ await server.connect(transport);