pretext-pdf-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/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # pretext-pdf-mcp
2
+
3
+ MCP server for [pretext-pdf](https://github.com/Himaan1998Y/pretext-pdf) — generate professional PDFs from structured JSON in Claude, Cursor, or any AI agent.
4
+
5
+ No headless browser. No puppeteer. Pure Node.js with embedded fonts and precision text layout.
6
+
7
+ ## Install
8
+
9
+ ### Option 1: npx (no global install needed)
10
+
11
+ ```bash
12
+ npx pretext-pdf-mcp
13
+ ```
14
+
15
+ ### Option 2: Global install
16
+
17
+ ```bash
18
+ npm install -g pretext-pdf-mcp
19
+ pretext-pdf-mcp
20
+ ```
21
+
22
+ ## Claude Desktop Configuration
23
+
24
+ Add to your `claude_desktop_config.json`:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "pretext-pdf": {
30
+ "command": "npx",
31
+ "args": ["-y", "pretext-pdf-mcp"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Config file location:
38
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
39
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
40
+
41
+ ## Tools
42
+
43
+ | Tool | Input | Output |
44
+ |------|-------|--------|
45
+ | `generate_pdf` | PdfDocument JSON descriptor | Base64 PDF + filename + size |
46
+ | `generate_invoice` | Invoice data (parties, items, GST, currency) | Base64 PDF |
47
+ | `generate_report` | Report sections with optional tables and callouts | Base64 PDF |
48
+ | `list_element_types` | none | Markdown reference of all element types |
49
+
50
+ ### generate_pdf
51
+
52
+ Full-power access to the pretext-pdf API. Pass any PdfDocument descriptor.
53
+
54
+ ```json
55
+ {
56
+ "document": {
57
+ "pageSize": "A4",
58
+ "footer": { "text": "Page {{pageNumber}} of {{totalPages}}", "fontSize": 9 },
59
+ "content": [
60
+ { "type": "heading", "level": 1, "text": "My Document" },
61
+ { "type": "paragraph", "text": "Hello world." }
62
+ ]
63
+ },
64
+ "filename": "my-document"
65
+ }
66
+ ```
67
+
68
+ Returns:
69
+ ```json
70
+ {
71
+ "success": true,
72
+ "base64": "<base64-encoded PDF bytes>",
73
+ "filename": "my-document.pdf",
74
+ "size_bytes": 42816
75
+ }
76
+ ```
77
+
78
+ ### generate_invoice
79
+
80
+ Business-friendly invoice generator. No PDF knowledge needed.
81
+
82
+ ```json
83
+ {
84
+ "from": {
85
+ "company": "Antigravity Systems",
86
+ "address": "Gurugram, Haryana",
87
+ "gstin": "06AABCA1234Z1ZK",
88
+ "email": "hello@antigravity.dev"
89
+ },
90
+ "to": {
91
+ "company": "TCS Ltd",
92
+ "address": "Mumbai, Maharashtra",
93
+ "gstin": "27AAACT2727Q1ZW"
94
+ },
95
+ "invoice_number": "INV-2026-001",
96
+ "date": "2026-04-08",
97
+ "due_date": "2026-05-08",
98
+ "currency": "INR",
99
+ "items": [
100
+ {
101
+ "description": "LLM Fine-tuning Pipeline",
102
+ "hsn_code": "998314",
103
+ "quantity": 1,
104
+ "rate": 250000,
105
+ "gst_rate": 18
106
+ },
107
+ {
108
+ "description": "AI Strategy Workshop",
109
+ "quantity": 2,
110
+ "rate": 75000,
111
+ "gst_rate": 18
112
+ }
113
+ ],
114
+ "notes": "Payment due within 30 days. NEFT/IMPS preferred."
115
+ }
116
+ ```
117
+
118
+ Returns:
119
+ ```json
120
+ {
121
+ "success": true,
122
+ "base64": "<base64-encoded PDF bytes>",
123
+ "filename": "invoice-INV-2026-001.pdf",
124
+ "size_bytes": 68420
125
+ }
126
+ ```
127
+
128
+ Features:
129
+ - Supports INR, USD, EUR, GBP
130
+ - Auto-calculates IGST per line item when `gst_rate` is set
131
+ - HSN/SAC code column appears automatically when any item has it
132
+ - Company header with from/to details in a 2-column table
133
+ - Professional footer with invoice number and page numbers
134
+
135
+ ### generate_report
136
+
137
+ Multi-section report with optional TOC, tables, and callout boxes.
138
+
139
+ ```json
140
+ {
141
+ "title": "Haryana Real Estate Q1 2026",
142
+ "subtitle": "Residential & Commercial Analysis",
143
+ "author": "Antigravity Research",
144
+ "include_toc": true,
145
+ "sections": [
146
+ {
147
+ "heading": "Executive Summary",
148
+ "body": "Strong growth across all micro-markets.\n\nGurugram led with 18% YoY volume growth.",
149
+ "table": {
150
+ "headers": ["Market", "Avg Rs./sqft", "YoY"],
151
+ "rows": [
152
+ ["New Gurugram", "9,800", "+15.6%"],
153
+ ["Sohna Road", "8,400", "+12.1%"]
154
+ ]
155
+ },
156
+ "callout": {
157
+ "style": "warning",
158
+ "text": "Repo rate risk: any hike above 6.75% could suppress volumes 10-15%."
159
+ }
160
+ }
161
+ ]
162
+ }
163
+ ```
164
+
165
+ ### list_element_types
166
+
167
+ No input. Returns a markdown reference of all 16 element types (paragraph, heading, table, list, image, svg, code, blockquote, callout, toc, form-field, comment, hr, spacer, page-break, rich-paragraph) with key properties and examples.
168
+
169
+ ## Decoding the base64 PDF
170
+
171
+ In Node.js:
172
+ ```javascript
173
+ const bytes = Buffer.from(result.base64, 'base64')
174
+ fs.writeFileSync('output.pdf', bytes)
175
+ ```
176
+
177
+ In Python:
178
+ ```python
179
+ import base64
180
+ pdf_bytes = base64.b64decode(result['base64'])
181
+ with open('output.pdf', 'wb') as f:
182
+ f.write(pdf_bytes)
183
+ ```
184
+
185
+ ## License
186
+
187
+ MIT — Himanshu Jain
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { generatePdfTool } from './tools/generate-pdf.js';
6
+ import { generateInvoiceTool } from './tools/generate-invoice.js';
7
+ import { generateReportTool } from './tools/generate-report.js';
8
+ import { listElementsTool } from './tools/list-elements.js';
9
+ const server = new Server({ name: 'pretext-pdf', version: '1.0.0' }, { capabilities: { tools: {} } });
10
+ const tools = [generatePdfTool, generateInvoiceTool, generateReportTool, listElementsTool];
11
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
12
+ tools: tools.map(t => t.schema),
13
+ }));
14
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
15
+ const tool = tools.find(t => t.schema.name === request.params.name);
16
+ if (!tool) {
17
+ return {
18
+ content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
19
+ isError: true,
20
+ };
21
+ }
22
+ return tool.handler(request.params.arguments ?? {});
23
+ });
24
+ const transport = new StdioServerTransport();
25
+ await server.connect(transport);
@@ -0,0 +1,323 @@
1
+ import { render } from 'pretext-pdf';
2
+ import { toBase64 } from '../utils/base64.js';
3
+ const CURRENCY_SYMBOLS = {
4
+ INR: '₹',
5
+ USD: '$',
6
+ EUR: '€',
7
+ GBP: '£',
8
+ };
9
+ function formatMoney(amount, symbol) {
10
+ return `${symbol}${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
11
+ }
12
+ function partyBlock(p) {
13
+ const lines = [p.company];
14
+ if (p.address)
15
+ lines.push(p.address);
16
+ if (p.gstin)
17
+ lines.push(`GSTIN: ${p.gstin}`);
18
+ if (p.email)
19
+ lines.push(p.email);
20
+ if (p.phone)
21
+ lines.push(p.phone);
22
+ return lines.join('\n');
23
+ }
24
+ function todayISO() {
25
+ return new Date().toISOString().slice(0, 10);
26
+ }
27
+ function buildInvoiceDocument(input) {
28
+ const currency = input.currency ?? 'INR';
29
+ const sym = CURRENCY_SYMBOLS[currency];
30
+ const date = input.date ?? todayISO();
31
+ const invoiceNo = input.invoice_number ?? `INV-${Date.now()}`;
32
+ const hasHsn = input.items.some(i => i.hsn_code);
33
+ const hasGst = input.items.some(i => i.gst_rate !== undefined && i.gst_rate > 0);
34
+ // Build columns for line items table
35
+ const itemColumns = [{ width: '3*', align: 'left' }];
36
+ if (hasHsn)
37
+ itemColumns.push({ width: 70, align: 'center' });
38
+ itemColumns.push({ width: 60, align: 'right' });
39
+ itemColumns.push({ width: 80, align: 'right' });
40
+ itemColumns.push({ width: 90, align: 'right' });
41
+ // Header row for items table
42
+ const headerCells = [
43
+ { text: 'Description', fontWeight: 700, color: '#ffffff' },
44
+ ];
45
+ if (hasHsn)
46
+ headerCells.push({ text: 'HSN', fontWeight: 700, color: '#ffffff' });
47
+ headerCells.push({ text: 'Qty', fontWeight: 700, color: '#ffffff' });
48
+ headerCells.push({ text: 'Rate', fontWeight: 700, color: '#ffffff' });
49
+ headerCells.push({ text: 'Amount', fontWeight: 700, color: '#ffffff' });
50
+ // Data rows
51
+ let subtotal = 0;
52
+ const itemRows = [{ isHeader: true, cells: headerCells }];
53
+ for (const item of input.items) {
54
+ const amount = item.quantity * item.rate;
55
+ subtotal += amount;
56
+ const cells = [{ text: item.description }];
57
+ if (hasHsn)
58
+ cells.push({ text: item.hsn_code ?? '' });
59
+ cells.push({ text: String(item.quantity) });
60
+ cells.push({ text: formatMoney(item.rate, sym) });
61
+ cells.push({ text: formatMoney(amount, sym) });
62
+ itemRows.push({ cells });
63
+ }
64
+ // GST calculation: use IGST if inter-state (default), or CGST+SGST if intra
65
+ // We'll use IGST for simplicity (single tax line)
66
+ const totalGst = hasGst
67
+ ? input.items.reduce((sum, item) => {
68
+ const amount = item.quantity * item.rate;
69
+ const rate = item.gst_rate ?? 0;
70
+ return sum + (amount * rate) / 100;
71
+ }, 0)
72
+ : 0;
73
+ const grandTotal = subtotal + totalGst;
74
+ // Build totals section
75
+ const totalsContent = [
76
+ { type: 'hr', color: '#dddddd', thickness: 0.5, spaceBelow: 6 },
77
+ {
78
+ type: 'paragraph',
79
+ text: `Subtotal: ${formatMoney(subtotal, sym)}`,
80
+ align: 'right',
81
+ spaceAfter: hasGst ? 4 : 8,
82
+ },
83
+ ];
84
+ if (hasGst) {
85
+ // Build per-rate GST breakdown
86
+ const rateGroups = {};
87
+ for (const item of input.items) {
88
+ if (!item.gst_rate)
89
+ continue;
90
+ const amount = item.quantity * item.rate;
91
+ rateGroups[item.gst_rate] = (rateGroups[item.gst_rate] ?? 0) + (amount * item.gst_rate) / 100;
92
+ }
93
+ for (const [rate, gstAmt] of Object.entries(rateGroups)) {
94
+ totalsContent.push({
95
+ type: 'paragraph',
96
+ text: `IGST @ ${rate}%: ${formatMoney(gstAmt, sym)}`,
97
+ align: 'right',
98
+ color: '#555555',
99
+ spaceAfter: 4,
100
+ });
101
+ }
102
+ totalsContent.push({ type: 'hr', color: '#1a1a2e', thickness: 1, spaceBelow: 6 });
103
+ totalsContent.push({
104
+ type: 'paragraph',
105
+ text: `GRAND TOTAL: ${formatMoney(grandTotal, sym)}`,
106
+ fontSize: 13,
107
+ fontWeight: 700,
108
+ color: '#1a1a2e',
109
+ align: 'right',
110
+ spaceAfter: 16,
111
+ });
112
+ }
113
+ else {
114
+ totalsContent.push({ type: 'hr', color: '#1a1a2e', thickness: 1, spaceBelow: 6 });
115
+ totalsContent.push({
116
+ type: 'paragraph',
117
+ text: `TOTAL: ${formatMoney(grandTotal, sym)}`,
118
+ fontSize: 13,
119
+ fontWeight: 700,
120
+ color: '#1a1a2e',
121
+ align: 'right',
122
+ spaceAfter: 16,
123
+ });
124
+ }
125
+ const content = [
126
+ // Header: company name
127
+ { type: 'heading', level: 1, text: input.from.company, fontSize: 22, color: '#1a1a2e', spaceAfter: 4 },
128
+ {
129
+ type: 'paragraph',
130
+ text: [input.from.address, input.from.gstin ? `GSTIN: ${input.from.gstin}` : null]
131
+ .filter(Boolean)
132
+ .join(' · '),
133
+ fontSize: 9,
134
+ color: '#666666',
135
+ spaceAfter: 2,
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ text: [input.from.email, input.from.phone].filter(Boolean).join(' · '),
140
+ fontSize: 9,
141
+ color: '#0070f3',
142
+ spaceAfter: 14,
143
+ },
144
+ { type: 'hr', color: '#1a1a2e', thickness: 2, spaceBelow: 12 },
145
+ // Invoice meta
146
+ { type: 'heading', level: 3, text: 'INVOICE', fontSize: 16, color: '#1a1a2e', spaceAfter: 8 },
147
+ {
148
+ type: 'table',
149
+ columns: [{ width: '1*' }, { width: '1*' }],
150
+ rows: [
151
+ {
152
+ cells: [
153
+ { text: `Invoice No.\n${invoiceNo}` },
154
+ { text: `Bill To\n${partyBlock(input.to)}` },
155
+ ],
156
+ },
157
+ {
158
+ cells: [
159
+ { text: `Date\n${date}` },
160
+ { text: input.due_date ? `Due Date\n${input.due_date}` : '' },
161
+ ],
162
+ },
163
+ ],
164
+ borderColor: '#e8e8e8',
165
+ borderWidth: 0.5,
166
+ cellPaddingH: 10,
167
+ cellPaddingV: 8,
168
+ spaceAfter: 16,
169
+ },
170
+ // Line items
171
+ { type: 'heading', level: 3, text: 'Services / Items', color: '#1a1a2e', spaceAfter: 6 },
172
+ {
173
+ type: 'table',
174
+ columns: itemColumns,
175
+ rows: itemRows,
176
+ headerBgColor: '#1a1a2e',
177
+ borderColor: '#e0e0e0',
178
+ borderWidth: 0.5,
179
+ cellPaddingH: 8,
180
+ cellPaddingV: 6,
181
+ spaceAfter: 4,
182
+ },
183
+ // Totals
184
+ ...totalsContent,
185
+ ];
186
+ // Notes
187
+ if (input.notes) {
188
+ content.push({ type: 'hr', color: '#e8e8e8', thickness: 0.5, spaceBelow: 10 });
189
+ content.push({ type: 'heading', level: 4, text: 'Notes', spaceAfter: 4 });
190
+ content.push({ type: 'paragraph', text: input.notes, fontSize: 10, color: '#555555', spaceAfter: 12 });
191
+ }
192
+ // Footer note
193
+ content.push({ type: 'hr', color: '#e8e8e8', thickness: 0.5, spaceBelow: 8 });
194
+ content.push({
195
+ type: 'paragraph',
196
+ text: 'Generated by pretext-pdf',
197
+ fontSize: 8,
198
+ color: '#aaaaaa',
199
+ align: 'center',
200
+ });
201
+ return {
202
+ pageSize: 'A4',
203
+ margins: { top: 50, bottom: 50, left: 56, right: 56 },
204
+ defaultFontSize: 10,
205
+ footer: {
206
+ text: `Invoice ${invoiceNo} · Page {{pageNumber}} of {{totalPages}}`,
207
+ fontSize: 8,
208
+ color: '#aaaaaa',
209
+ align: 'center',
210
+ },
211
+ content,
212
+ };
213
+ }
214
+ export const generateInvoiceTool = {
215
+ schema: {
216
+ name: 'generate_invoice',
217
+ description: 'Generate a professional invoice PDF. Accepts structured invoice data (from/to parties, line items, GST). Returns base64-encoded PDF. Supports INR/USD/EUR/GBP currencies. GST (IGST) is auto-calculated when gst_rate is set on items.',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ from: {
222
+ type: 'object',
223
+ description: 'Issuing party (your company)',
224
+ properties: {
225
+ company: { type: 'string' },
226
+ address: { type: 'string' },
227
+ gstin: { type: 'string' },
228
+ email: { type: 'string' },
229
+ phone: { type: 'string' },
230
+ },
231
+ required: ['company'],
232
+ },
233
+ to: {
234
+ type: 'object',
235
+ description: 'Billing party (client)',
236
+ properties: {
237
+ company: { type: 'string' },
238
+ address: { type: 'string' },
239
+ gstin: { type: 'string' },
240
+ },
241
+ required: ['company'],
242
+ },
243
+ invoice_number: { type: 'string', description: 'Invoice identifier e.g. INV-2026-001' },
244
+ date: { type: 'string', description: 'Invoice date ISO format YYYY-MM-DD. Defaults to today.' },
245
+ due_date: { type: 'string', description: 'Payment due date ISO format.' },
246
+ currency: {
247
+ type: 'string',
248
+ enum: ['INR', 'USD', 'EUR', 'GBP'],
249
+ description: 'Currency. Default: INR',
250
+ },
251
+ items: {
252
+ type: 'array',
253
+ description: 'Line items',
254
+ items: {
255
+ type: 'object',
256
+ properties: {
257
+ description: { type: 'string' },
258
+ hsn_code: { type: 'string', description: 'HSN/SAC code for India GST' },
259
+ quantity: { type: 'number' },
260
+ rate: { type: 'number', description: 'Unit price' },
261
+ gst_rate: {
262
+ type: 'number',
263
+ enum: [0, 5, 12, 18, 28],
264
+ description: 'GST rate %. If set, IGST is calculated.',
265
+ },
266
+ },
267
+ required: ['description', 'quantity', 'rate'],
268
+ },
269
+ },
270
+ notes: { type: 'string', description: 'Additional notes or payment terms.' },
271
+ filename: { type: 'string', description: 'Suggested filename without .pdf extension.' },
272
+ },
273
+ required: ['from', 'to', 'items'],
274
+ },
275
+ },
276
+ handler: async (args) => {
277
+ try {
278
+ // Validate required fields
279
+ if (!args.from || !args.to) {
280
+ return {
281
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'VALIDATION_ERROR', message: 'from and to are required' }) }],
282
+ isError: true,
283
+ };
284
+ }
285
+ const items = args.items;
286
+ if (!Array.isArray(items) || items.length === 0) {
287
+ return {
288
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'VALIDATION_ERROR', message: 'items must be a non-empty array' }) }],
289
+ isError: true,
290
+ };
291
+ }
292
+ const input = args;
293
+ const doc = buildInvoiceDocument(input);
294
+ const bytes = await render(doc);
295
+ const base64 = toBase64(bytes);
296
+ const invoiceNo = input.invoice_number ?? 'invoice';
297
+ const filename = (args.filename ?? `invoice-${invoiceNo}`) + '.pdf';
298
+ return {
299
+ content: [
300
+ {
301
+ type: 'text',
302
+ text: JSON.stringify({ success: true, base64, filename, size_bytes: bytes.length }),
303
+ },
304
+ ],
305
+ };
306
+ }
307
+ catch (err) {
308
+ return {
309
+ content: [
310
+ {
311
+ type: 'text',
312
+ text: JSON.stringify({
313
+ success: false,
314
+ error: err.code ?? 'UNKNOWN_ERROR',
315
+ message: err.message,
316
+ }),
317
+ },
318
+ ],
319
+ isError: true,
320
+ };
321
+ }
322
+ },
323
+ };
@@ -0,0 +1,59 @@
1
+ import { render } from 'pretext-pdf';
2
+ import { toBase64 } from '../utils/base64.js';
3
+ export const generatePdfTool = {
4
+ schema: {
5
+ name: 'generate_pdf',
6
+ description: 'Generate a PDF from a pretext-pdf document descriptor (PdfDocument JSON). Returns base64-encoded PDF bytes. Use list_element_types to see available elements.',
7
+ inputSchema: {
8
+ type: 'object',
9
+ properties: {
10
+ document: {
11
+ type: 'object',
12
+ description: 'A PdfDocument config object with content array and optional pageSize, margins, fonts, header, footer, watermark, encryption, etc.',
13
+ },
14
+ filename: {
15
+ type: 'string',
16
+ description: 'Suggested filename (without .pdf extension)',
17
+ default: 'document',
18
+ },
19
+ },
20
+ required: ['document'],
21
+ },
22
+ },
23
+ handler: async (args) => {
24
+ try {
25
+ if (!args.document || typeof args.document !== 'object') {
26
+ return {
27
+ content: [{ type: 'text', text: 'Error: document is required and must be an object' }],
28
+ isError: true,
29
+ };
30
+ }
31
+ const bytes = await render(args.document);
32
+ const base64 = toBase64(bytes);
33
+ const filename = (args.filename ?? 'document') + '.pdf';
34
+ return {
35
+ content: [
36
+ {
37
+ type: 'text',
38
+ text: JSON.stringify({ success: true, base64, filename, size_bytes: bytes.length }),
39
+ },
40
+ ],
41
+ };
42
+ }
43
+ catch (err) {
44
+ return {
45
+ content: [
46
+ {
47
+ type: 'text',
48
+ text: JSON.stringify({
49
+ success: false,
50
+ error: err.code ?? 'UNKNOWN_ERROR',
51
+ message: err.message,
52
+ }),
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ },
59
+ };
@@ -0,0 +1,253 @@
1
+ import { render } from 'pretext-pdf';
2
+ import { toBase64 } from '../utils/base64.js';
3
+ const CALLOUT_COLORS = {
4
+ info: '#0070f3',
5
+ warning: '#f59e0b',
6
+ tip: '#10b981',
7
+ note: '#6366f1',
8
+ };
9
+ function todayISO() {
10
+ return new Date().toISOString().slice(0, 10);
11
+ }
12
+ function buildReportDocument(input) {
13
+ const includeToc = input.include_toc !== false;
14
+ const date = input.date ?? todayISO();
15
+ const content = [
16
+ // Cover block
17
+ { type: 'spacer', height: 40 },
18
+ {
19
+ type: 'heading',
20
+ level: 1,
21
+ text: input.title,
22
+ fontSize: 28,
23
+ color: '#1a1a2e',
24
+ align: 'center',
25
+ spaceAfter: 10,
26
+ bookmark: false,
27
+ },
28
+ ];
29
+ if (input.subtitle) {
30
+ content.push({
31
+ type: 'paragraph',
32
+ text: input.subtitle,
33
+ fontSize: 14,
34
+ color: '#555555',
35
+ align: 'center',
36
+ spaceAfter: 10,
37
+ });
38
+ }
39
+ const metaParts = [];
40
+ if (input.author)
41
+ metaParts.push(input.author);
42
+ metaParts.push(date);
43
+ content.push({
44
+ type: 'paragraph',
45
+ text: metaParts.join(' · '),
46
+ fontSize: 10,
47
+ color: '#888888',
48
+ align: 'center',
49
+ spaceAfter: 6,
50
+ });
51
+ content.push({ type: 'hr', color: '#1a1a2e', thickness: 2, spaceBelow: 40 });
52
+ // TOC
53
+ if (includeToc) {
54
+ content.push({
55
+ type: 'toc',
56
+ title: 'Contents',
57
+ showTitle: true,
58
+ leader: '.',
59
+ minLevel: 1,
60
+ maxLevel: 2,
61
+ fontSize: 11,
62
+ spaceAfter: 24,
63
+ });
64
+ content.push({ type: 'page-break' });
65
+ }
66
+ // Sections
67
+ for (const section of input.sections) {
68
+ content.push({
69
+ type: 'heading',
70
+ level: 1,
71
+ text: section.heading,
72
+ anchor: section.heading.toLowerCase().replace(/\s+/g, '-'),
73
+ spaceAfter: 8,
74
+ });
75
+ // Body: split on double newlines for multiple paragraphs, single newlines become spaces
76
+ const paragraphs = section.body.split(/\n\n+/);
77
+ for (const para of paragraphs) {
78
+ if (para.trim()) {
79
+ content.push({
80
+ type: 'paragraph',
81
+ text: para.trim().replace(/\n/g, ' '),
82
+ spaceAfter: 8,
83
+ });
84
+ }
85
+ }
86
+ if (section.table) {
87
+ const { headers, rows } = section.table;
88
+ const columns = headers.map(() => ({ width: `${Math.floor(100 / headers.length)}%` }));
89
+ // Use equal fractional widths
90
+ const fracColumns = headers.map(() => ({ width: '1*', align: 'left' }));
91
+ const headerRow = {
92
+ isHeader: true,
93
+ cells: headers.map(h => ({ text: h, fontWeight: 700, color: '#ffffff' })),
94
+ };
95
+ const dataRows = rows.map(row => ({
96
+ cells: row.map(cell => ({ text: cell })),
97
+ }));
98
+ content.push({
99
+ type: 'table',
100
+ columns: fracColumns,
101
+ rows: [headerRow, ...dataRows],
102
+ headerBgColor: '#1a1a2e',
103
+ borderColor: '#dddddd',
104
+ borderWidth: 0.5,
105
+ cellPaddingH: 8,
106
+ cellPaddingV: 6,
107
+ spaceAfter: 12,
108
+ });
109
+ // Suppress unused variable warning
110
+ void columns;
111
+ }
112
+ if (section.callout) {
113
+ const borderColor = CALLOUT_COLORS[section.callout.style] ?? '#888888';
114
+ content.push({
115
+ type: 'callout',
116
+ style: section.callout.style,
117
+ content: section.callout.text,
118
+ borderColor,
119
+ spaceAfter: 12,
120
+ });
121
+ }
122
+ }
123
+ return {
124
+ pageSize: 'A4',
125
+ margins: { top: 60, bottom: 60, left: 64, right: 64 },
126
+ defaultFontSize: 11,
127
+ bookmarks: { minLevel: 1, maxLevel: 3 },
128
+ header: {
129
+ text: input.title,
130
+ fontSize: 8,
131
+ color: '#999999',
132
+ align: 'right',
133
+ },
134
+ footer: {
135
+ text: 'Page {{pageNumber}} of {{totalPages}}',
136
+ fontSize: 8,
137
+ color: '#999999',
138
+ align: 'center',
139
+ },
140
+ metadata: {
141
+ title: input.title,
142
+ author: input.author,
143
+ subject: input.subtitle,
144
+ },
145
+ content,
146
+ };
147
+ }
148
+ export const generateReportTool = {
149
+ schema: {
150
+ name: 'generate_report',
151
+ description: 'Generate a professional multi-section report PDF with optional TOC, tables, and callout boxes. Returns base64-encoded PDF.',
152
+ inputSchema: {
153
+ type: 'object',
154
+ properties: {
155
+ title: { type: 'string', description: 'Report title' },
156
+ subtitle: { type: 'string', description: 'Report subtitle or description' },
157
+ author: { type: 'string', description: 'Author name' },
158
+ date: { type: 'string', description: 'Date string. Defaults to today.' },
159
+ include_toc: {
160
+ type: 'boolean',
161
+ description: 'Include a Table of Contents page. Default: true',
162
+ default: true,
163
+ },
164
+ sections: {
165
+ type: 'array',
166
+ description: 'Report sections',
167
+ items: {
168
+ type: 'object',
169
+ properties: {
170
+ heading: { type: 'string', description: 'Section heading' },
171
+ body: {
172
+ type: 'string',
173
+ description: 'Section body text. Use double newlines (\\n\\n) to separate paragraphs.',
174
+ },
175
+ table: {
176
+ type: 'object',
177
+ description: 'Optional data table',
178
+ properties: {
179
+ headers: { type: 'array', items: { type: 'string' } },
180
+ rows: {
181
+ type: 'array',
182
+ items: { type: 'array', items: { type: 'string' } },
183
+ },
184
+ },
185
+ required: ['headers', 'rows'],
186
+ },
187
+ callout: {
188
+ type: 'object',
189
+ description: 'Optional callout / alert box',
190
+ properties: {
191
+ style: {
192
+ type: 'string',
193
+ enum: ['info', 'warning', 'tip', 'note'],
194
+ },
195
+ text: { type: 'string' },
196
+ },
197
+ required: ['style', 'text'],
198
+ },
199
+ },
200
+ required: ['heading', 'body'],
201
+ },
202
+ },
203
+ filename: { type: 'string', description: 'Suggested filename without .pdf extension.' },
204
+ },
205
+ required: ['title', 'sections'],
206
+ },
207
+ },
208
+ handler: async (args) => {
209
+ try {
210
+ if (!args.title || typeof args.title !== 'string') {
211
+ return {
212
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'VALIDATION_ERROR', message: 'title is required' }) }],
213
+ isError: true,
214
+ };
215
+ }
216
+ const sections = args.sections;
217
+ if (!Array.isArray(sections) || sections.length === 0) {
218
+ return {
219
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'VALIDATION_ERROR', message: 'sections must be a non-empty array' }) }],
220
+ isError: true,
221
+ };
222
+ }
223
+ const input = args;
224
+ const doc = buildReportDocument(input);
225
+ const bytes = await render(doc);
226
+ const base64 = toBase64(bytes);
227
+ const filename = (args.filename ?? `report-${Date.now()}`) + '.pdf';
228
+ return {
229
+ content: [
230
+ {
231
+ type: 'text',
232
+ text: JSON.stringify({ success: true, base64, filename, size_bytes: bytes.length }),
233
+ },
234
+ ],
235
+ };
236
+ }
237
+ catch (err) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: 'text',
242
+ text: JSON.stringify({
243
+ success: false,
244
+ error: err.code ?? 'UNKNOWN_ERROR',
245
+ message: err.message,
246
+ }),
247
+ },
248
+ ],
249
+ isError: true,
250
+ };
251
+ }
252
+ },
253
+ };
@@ -0,0 +1,98 @@
1
+ const ELEMENTS_REFERENCE = `# pretext-pdf Element Types Reference
2
+
3
+ ## paragraph
4
+ Renders a text block. Key props: \`text\` (required), \`fontSize\`, \`fontWeight\` (400|700), \`color\` (#hex), \`align\` (left|center|right|justify), \`spaceAfter\`, \`spaceBefore\`, \`bgColor\`, \`underline\`, \`strikethrough\`, \`url\`, \`letterSpacing\`, \`smallCaps\`.
5
+ Example: \`{ type: "paragraph", text: "Hello world", fontSize: 12, color: "#333333" }\`
6
+
7
+ ## heading
8
+ Section heading with automatic bookmarks. Key props: \`level\` (1–4, required), \`text\` (required), \`fontSize\` (defaults: h1=28, h2=22, h3=18, h4=15), \`color\`, \`align\`, \`anchor\` (for internal links), \`bookmark\` (set false to exclude from PDF outline), \`spaceAfter\`.
9
+ Example: \`{ type: "heading", level: 1, text: "Introduction", color: "#1a1a2e" }\`
10
+
11
+ ## rich-paragraph
12
+ Paragraph with per-span formatting. Key props: \`lines\` (array of RichLine, each with \`spans\` array). Each span: \`text\`, \`fontWeight\`, \`color\`, \`fontSize\`, \`italic\`, \`underline\`, \`strikethrough\`, \`url\`, \`href\` (internal anchor link).
13
+ Example: \`{ type: "rich-paragraph", lines: [{ spans: [{ text: "Bold", fontWeight: 700 }, { text: " normal" }] }] }\`
14
+
15
+ ## table
16
+ Data table with optional header rows, borders, and column alignment. Key props: \`columns\` (array of \`{width, align}\`; width can be pt number, \`"2*"\` fraction, or \`"auto"\`), \`rows\` (array of \`{cells, isHeader, bgColor}\`), \`headerBgColor\`, \`borderColor\`, \`borderWidth\`, \`cellPaddingH\`, \`cellPaddingV\`, \`spaceAfter\`.
17
+ Cell props: \`text\`, \`fontWeight\`, \`color\`, \`bgColor\`, \`align\`, \`colSpan\`.
18
+ Example: \`{ type: "table", columns: [{ width: "2*" }, { width: 100, align: "right" }], rows: [{ isHeader: true, cells: [{ text: "Name", fontWeight: 700 }, { text: "Amount", fontWeight: 700 }] }] }\`
19
+
20
+ ## list
21
+ Bulleted or numbered list. Key props: \`style\` (unordered|ordered, required), \`items\` (array of \`{text, children}\`), \`fontSize\`, \`color\`, \`spaceAfter\`, \`indent\` (pt for nested lists).
22
+ Example: \`{ type: "list", style: "unordered", items: [{ text: "Item one" }, { text: "Item two", children: [{ text: "Sub-item" }] }] }\`
23
+
24
+ ## image
25
+ Embed PNG/JPG image. Key props: \`src\` (absolute file path or Uint8Array, required), \`width\` (pt, defaults to content width), \`height\` (optional, auto-scales), \`align\` (left|center|right), \`caption\`, \`spaceAfter\`.
26
+ Example: \`{ type: "image", src: "/abs/path/to/logo.png", width: 120, align: "center" }\`
27
+
28
+ ## svg
29
+ Inline SVG vector graphic. Key props: \`content\` (SVG markup string, required), \`width\` (pt), \`height\` (pt), \`align\`, \`spaceAfter\`.
30
+ Example: \`{ type: "svg", content: "<svg viewBox='0 0 100 100'><circle cx='50' cy='50' r='40'/></svg>", width: 100, height: 100 }\`
31
+
32
+ ## code
33
+ Monospace code block with optional syntax label. Key props: \`code\` (required), \`language\` (label only, no syntax highlighting), \`fontSize\`, \`bgColor\`, \`color\`, \`spaceAfter\`.
34
+ Example: \`{ type: "code", language: "typescript", code: "const x = 42;", bgColor: "#f4f4f4" }\`
35
+
36
+ ## blockquote
37
+ Indented quote with a left border. Key props: \`text\` (required), \`borderColor\` (#hex), \`color\`, \`fontSize\`, \`spaceAfter\`.
38
+ Example: \`{ type: "blockquote", text: "The best code is no code at all.", borderColor: "#6366f1" }\`
39
+
40
+ ## callout
41
+ Alert/info box with preset color schemes and optional title. Key props: \`content\` (body text, required), \`style\` (info|warning|tip|note — sets default colors), \`title\` (bold heading above body), \`borderColor\`, \`backgroundColor\`, \`color\`, \`fontSize\`, \`padding\`, \`spaceAfter\`.
42
+ Example: \`{ type: "callout", style: "warning", content: "This action cannot be undone." }\`
43
+
44
+ ## toc
45
+ Auto-generated Table of Contents. Place after cover, before content. Key props: \`title\` (heading text), \`showTitle\` (boolean), \`leader\` (dot fill char, e.g. "."), \`minLevel\` (1–4), \`maxLevel\` (1–4), \`fontSize\`, \`spaceAfter\`.
46
+ Example: \`{ type: "toc", title: "Contents", showTitle: true, leader: ".", minLevel: 1, maxLevel: 2 }\`
47
+
48
+ ## form-field
49
+ Interactive AcroForm field (text, checkbox, radio, dropdown, button). Key props: \`fieldType\` (required), \`name\` (unique, required), \`label\`, \`placeholder\`, \`defaultValue\`, \`multiline\`, \`options\` (for radio/dropdown), \`width\`, \`height\`, \`borderColor\`, \`spaceAfter\`.
50
+ Example: \`{ type: "form-field", fieldType: "text", name: "full_name", label: "Full Name", placeholder: "Enter your name" }\`
51
+
52
+ ## comment
53
+ Invisible sticky-note annotation (shows as icon in PDF viewer). Key props: \`contents\` (popup text, required), \`author\`, \`color\` (#hex), \`open\` (show popup by default).
54
+ Example: \`{ type: "comment", contents: "Review this section.", author: "Jane" }\`
55
+
56
+ ## hr
57
+ Horizontal rule / divider line. Key props: \`color\` (#hex), \`thickness\` (pt), \`spaceBelow\`.
58
+ Example: \`{ type: "hr", color: "#cccccc", thickness: 1, spaceBelow: 8 }\`
59
+
60
+ ## spacer
61
+ Vertical whitespace. Key props: \`height\` (pt, required).
62
+ Example: \`{ type: "spacer", height: 24 }\`
63
+
64
+ ## page-break
65
+ Force start of a new page. No additional props.
66
+ Example: \`{ type: "page-break" }\`
67
+
68
+ ---
69
+
70
+ ## Document-level options (PdfDocument)
71
+ - \`pageSize\`: 'A4'|'Letter'|'Legal'|'A3'|'A5' or \`[width, height]\` in pt. Default: 'A4'
72
+ - \`margins\`: \`{ top, bottom, left, right }\` in pt. Default: all 72pt
73
+ - \`defaultFont\`: font family name. Default: 'Inter'
74
+ - \`defaultFontSize\`: pt. Default: 12
75
+ - \`header\`: \`{ text, fontSize, align, color, fontFamily }\`. Supports \`{{pageNumber}}\` and \`{{totalPages}}\`
76
+ - \`footer\`: same as header
77
+ - \`watermark\`: \`{ text, opacity, rotation, fontSize, color }\`
78
+ - \`encryption\`: \`{ userPassword, ownerPassword, permissions }\`
79
+ - \`bookmarks\`: \`{ minLevel, maxLevel }\` or \`false\`
80
+ - \`metadata\`: \`{ title, author, subject, keywords, language }\`
81
+ - \`hyphenation\`: \`{ language: 'en-us' }\`
82
+ `;
83
+ export const listElementsTool = {
84
+ schema: {
85
+ name: 'list_element_types',
86
+ description: 'Returns a markdown reference of all pretext-pdf element types and their key properties. Use this before calling generate_pdf to understand what elements and options are available.',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {},
90
+ required: [],
91
+ },
92
+ },
93
+ handler: async (_args) => {
94
+ return {
95
+ content: [{ type: 'text', text: ELEMENTS_REFERENCE }],
96
+ };
97
+ },
98
+ };
@@ -0,0 +1,6 @@
1
+ export function toBase64(bytes) {
2
+ return Buffer.from(bytes).toString('base64');
3
+ }
4
+ export function fromBase64(b64) {
5
+ return new Uint8Array(Buffer.from(b64, 'base64'));
6
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "pretext-pdf-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for pretext-pdf — generate professional PDFs from JSON in Claude, Cursor, or any AI agent",
5
+ "keywords": ["mcp", "pdf", "pdf-generation", "invoice", "report", "claude", "ai-agent", "pretext"],
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "bin": { "pretext-pdf-mcp": "./dist/index.js" },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts",
12
+ "test": "tsx --test test/*.test.ts"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0",
16
+ "pretext-pdf": "^0.4.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "tsx": "^4.0.0",
21
+ "typescript": "^5.0.0"
22
+ },
23
+ "files": ["dist/**/*", "smithery.yaml", "README.md", "LICENSE"],
24
+ "engines": { "node": ">=18.0.0" },
25
+ "license": "MIT",
26
+ "author": "Himanshu Jain",
27
+ "repository": { "type": "git", "url": "https://github.com/Himaan1998Y/pretext-pdf-mcp" },
28
+ "homepage": "https://github.com/Himaan1998Y/pretext-pdf-mcp#readme"
29
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,7 @@
1
+ name: pretext-pdf
2
+ description: "Generate professional PDFs from structured JSON. Supports invoices (with GST), reports, tables, forms, encryption, and more. No headless browser — pure Node.js."
3
+ version: "1.0.0"
4
+ startCommand:
5
+ type: stdio
6
+ command: npx
7
+ args: ["-y", "pretext-pdf-mcp"]