create-pdf
Generate PDF documents with text, images, shapes, and full layout control. Supports Unicode fonts, custom page sizes, colors, and precise positioning for simple to complex designs.
Instructions
Create a PDF document with text, images, shapes, and full layout control. Supports progressive enhancement from simple documents to complex designs.
Key Features: • Unicode support: Chinese, Japanese, Korean, Arabic, emoji - auto-detects system fonts • Page setup (custom size, margins, background color) • Text with colors, fonts, positioning, and styling (oblique, spacing, etc.) • Shapes (rectangles, circles, lines) for visual design • Emoji rendering with inline image support
Common Patterns:
Simple Document (no pageSetup needed): [{"type": "heading", "text": "Title"}, {"type": "text", "text": "Body"}]
Styled Document (add colors/background): pageSetup: {"backgroundColor": "#F5F5F5"} content: [{"type": "heading", "text": "Title", "color": "#4A90E2"}]
Custom Layout (letterhead, certificates): [{"type": "rect", "x": 0, "y": 0, "width": 612, "height": 80, "fillColor": "navy"}, {"type": "heading", "text": "Company", "color": "white", "y": 25, "align": "center"}]
Algorithmic Design (tapering fonts, progressive layouts): Calculate values in a loop, then pass to tool: for (let i = 0; i < lines.length; i++) { const progress = i / lines.length; const fontSize = 8 + (progress * 16); // 8pt → 24pt content.push({"type": "text", "text": lines[i], "fontSize": fontSize}); }
Coordinate System: • Origin (0, 0) is top-left corner • Letter size page: 612 x 792 points (8.5" x 11") • 72 points = 1 inch
Tips: • Use "align": "center" for centered text (works with or without "width") • Use "x" to manually position (calculate as: (pageWidth - textWidth) / 2 for centering) • Colors: hex ("#FFD700") or named ("gold", "navy", "black") • Oblique: true for default slant, or number for degrees (15 = italic look) • All pageSetup and visual styling fields are optional - defaults match standard documents
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| author | No | Document author metadata | |
| content | Yes | Array of content items to add to the PDF | |
| filename | No | Optional filename for the PDF (defaults to "document.pdf"). SECURITY: Filenames are sanitized and written to a sandboxed directory: • Default: ~/.mcp-pdf/ • Override: Set PDF_OUTPUT_DIR environment variable • Path traversal attempts (.., /, etc) are blocked • Only alphanumeric, spaces, hyphens, underscores, and dots allowed • If file exists, timestamp is appended automatically | |
| font | No | Font for the PDF (optional - defaults to "auto"). **Default**: "auto" auto-detects Unicode fonts on macOS/Linux/Windows. Works for Chinese, Japanese, Korean, Arabic, emoji, and all languages. Advanced options: • Built-in: Helvetica, Times-Roman, Courier - ASCII/Latin only, no Chinese support • Custom: Absolute path to TTF/OTF font file for special needs **You can omit this parameter** - auto-detection works for 99% of use cases. | |
| pageSetup | No | Optional page setup configuration | |
| title | No | Document title metadata |
Input Schema (JSON Schema)
Implementation Reference
- src/tools/create-pdf.ts:230-375 (handler)The core handler function for the 'create-pdf' tool. It creates a PDF document using PDFKit, supports complex layouts with text (including emoji and fonts), images, vector shapes (rect, circle, line), page breaks, custom page setup, and saves the output file.async (args: CreatePdfArgs) => { const { filename = 'document.pdf', title, author, font, pageSetup, content } = args; try { const docOptions: PDFKit.PDFDocumentOptions = { info: { ...(title && { Title: title }), ...(author && { Author: author }), ...(filename && { Subject: filename }), }, }; if (pageSetup?.size) docOptions.size = pageSetup.size; if (pageSetup?.margins) docOptions.margins = pageSetup.margins; const doc = new PDFDocument(docOptions); const chunks: Buffer[] = []; doc.on('data', (c: Buffer) => chunks.push(c)); const pdfPromise = new Promise<Buffer>((resolve, reject) => { doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); }); if (pageSetup?.backgroundColor) { const pageSize = pageSetup?.size || [612, 792]; doc.rect(0, 0, pageSize[0], pageSize[1]).fill(pageSetup.backgroundColor); } const contentText = JSON.stringify(content); const containsEmoji = hasEmoji(contentText); const emojiAvailable = containsEmoji ? registerEmojiFont() : false; const fonts = await setupFonts(doc, font); const { regular: regularFont, bold: boldFont } = fonts; const warnings: string[] = []; for (const item of content) { if ((item.type === 'text' || item.type === 'heading') && item.text) { const fnt = item.bold ? boldFont : regularFont; const validation = validateTextForFont(item.text, fnt); if (validation.hasUnsupportedCharacters) warnings.push(...validation.warnings); } } const drawBackgroundOnPage = () => { if (pageSetup?.backgroundColor) { const pageSize = pageSetup?.size || [612, 792]; const x = doc.x; const y = doc.y; doc.rect(0, 0, pageSize[0], pageSize[1]).fill(pageSetup.backgroundColor); doc.x = x; doc.y = y; } }; doc.on('pageAdded', drawBackgroundOnPage); for (const item of content) { switch (item.type) { case 'text': { if (item.x !== undefined && item.align !== undefined) throw new Error('Cannot use both x and align in text element'); const fontSize = item.fontSize ?? 12; const fnt = item.bold ? boldFont : regularFont; if (item.color) doc.fillColor(item.color); const options = extractTextOptions(item); renderTextWithEmoji(doc, item.text ?? '', fontSize, fnt, emojiAvailable, options); if (item.color) doc.fillColor('black'); if (item.moveDown !== undefined) doc.moveDown(item.moveDown); break; } case 'heading': { if (item.x !== undefined && item.align !== undefined) throw new Error('Cannot use both x and align in heading element'); const fontSize = item.fontSize ?? 24; const fnt = item.bold !== false ? boldFont : regularFont; if (item.color) doc.fillColor(item.color); const options = extractTextOptions(item); renderTextWithEmoji(doc, item.text ?? '', fontSize, fnt, emojiAvailable, options); if (item.color) doc.fillColor('black'); if (item.moveDown !== undefined) doc.moveDown(item.moveDown); break; } case 'image': { const opts: Record<string, unknown> = {}; if (item.width !== undefined) opts.width = item.width; if (item.height !== undefined) opts.height = item.height; if (item.x !== undefined && item.y !== undefined) doc.image(item.imagePath, item.x, item.y, opts); else doc.image(item.imagePath, opts); break; } case 'rect': { doc.rect(item.x, item.y, item.width, item.height); if (item.fillColor && item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.fillAndStroke(item.fillColor, item.strokeColor); } else if (item.fillColor) doc.fill(item.fillColor); else if (item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.stroke(item.strokeColor); } doc.fillColor('black'); break; } case 'circle': { doc.circle(item.x, item.y, item.radius); if (item.fillColor && item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.fillAndStroke(item.fillColor, item.strokeColor); } else if (item.fillColor) doc.fill(item.fillColor); else if (item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.stroke(item.strokeColor); } doc.fillColor('black'); break; } case 'line': { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc .moveTo(item.x1, item.y1) .lineTo(item.x2, item.y2) .stroke(item.strokeColor || 'black'); break; } case 'pageBreak': { doc.addPage(); break; } } } doc.end(); const pdfBuffer = await pdfPromise; const uuid = crypto.randomUUID(); const storedFilename = `${uuid}.pdf`; const { fullPath } = await writePdfToFile(pdfBuffer, storedFilename, config.storageDir); const includePath = config.includePath; const warningText = warnings.length > 0 ? `\n\n⚠️ Character Warnings:\n${warnings.map((w) => `• ${w}`).join('\n')}` : ''; return { content: [ { type: 'text' as const, text: ['PDF created successfully', `Resource: mcp-pdf://${uuid}`, includePath ? `Output: ${fullPath}` : undefined, `Size: ${pdfBuffer.length} bytes`, filename !== 'document.pdf' ? `Filename: ${filename}` : undefined, warningText || undefined].filter(Boolean).join('\n'), }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text' as const, text: `Error creating PDF: ${message}` }], isError: true }; } }
- src/tools/create-pdf.ts:122-229 (schema)Comprehensive Zod input schema defining parameters for PDF creation: metadata, page setup, and typed content array supporting text/heading with styling, images, geometric shapes, and page breaks.inputSchema: { filename: z.string().optional().describe('Optional logical filename (metadata only). Storage uses UUID. Defaults to "document.pdf".'), title: z.string().optional().describe('Document title metadata'), author: z.string().optional().describe('Document author metadata'), font: z.string().optional().describe('Font strategy (default: auto). Built-ins: Helvetica, Times-Roman, Courier. Use a path or URL for Unicode.'), pageSetup: z .object({ size: z.tuple([z.number(), z.number()]).optional(), margins: z .object({ top: z.number(), bottom: z.number(), left: z.number(), right: z.number(), }) .optional(), backgroundColor: z.string().optional(), }) .optional(), content: z.array( z.union([ z.object({ type: z.literal('text'), text: z.string().optional(), fontSize: z.number().optional(), bold: z.boolean().optional(), color: z.string().optional(), x: z.number().optional(), y: z.number().optional(), align: z.enum(['left', 'center', 'right', 'justify']).optional(), indent: z.number().optional(), lineGap: z.number().optional(), paragraphGap: z.number().optional(), width: z.number().optional(), moveDown: z.number().optional(), underline: z.boolean().optional(), strike: z.boolean().optional(), oblique: z.union([z.boolean(), z.number()]).optional(), link: z.string().optional(), characterSpacing: z.number().optional(), wordSpacing: z.number().optional(), continued: z.boolean().optional(), lineBreak: z.boolean().optional(), }), z.object({ type: z.literal('heading'), text: z.string().optional(), fontSize: z.number().optional(), bold: z.boolean().optional(), color: z.string().optional(), x: z.number().optional(), y: z.number().optional(), align: z.enum(['left', 'center', 'right', 'justify']).optional(), indent: z.number().optional(), lineGap: z.number().optional(), paragraphGap: z.number().optional(), width: z.number().optional(), moveDown: z.number().optional(), underline: z.boolean().optional(), strike: z.boolean().optional(), oblique: z.union([z.boolean(), z.number()]).optional(), link: z.string().optional(), characterSpacing: z.number().optional(), wordSpacing: z.number().optional(), continued: z.boolean().optional(), lineBreak: z.boolean().optional(), }), z.object({ type: z.literal('image'), imagePath: z.string(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), }), z.object({ type: z.literal('rect'), x: z.number(), y: z.number(), width: z.number(), height: z.number(), fillColor: z.string().optional(), strokeColor: z.string().optional(), lineWidth: z.number().optional(), }), z.object({ type: z.literal('circle'), x: z.number(), y: z.number(), radius: z.number(), fillColor: z.string().optional(), strokeColor: z.string().optional(), lineWidth: z.number().optional(), }), z.object({ type: z.literal('line'), x1: z.number(), y1: z.number(), x2: z.number(), y2: z.number(), strokeColor: z.string().optional(), lineWidth: z.number().optional(), }), z.object({ type: z.literal('pageBreak') }), ]) ), } as Record<string, z.ZodTypeAny>, },
- src/tools/create-pdf.ts:116-377 (registration)The registerCreatePdfTool function that registers the 'create-pdf' MCP tool with the server, specifying name, metadata, input schema, and handler.export function registerCreatePdfTool(server: McpServer, config: PdfServerConfig) { server.registerTool( 'create-pdf', { title: 'Create PDF', description: 'Create a PDF document with text, images, shapes, and layout control. Supports Unicode + emoji fonts, backgrounds, and vector shapes.', inputSchema: { filename: z.string().optional().describe('Optional logical filename (metadata only). Storage uses UUID. Defaults to "document.pdf".'), title: z.string().optional().describe('Document title metadata'), author: z.string().optional().describe('Document author metadata'), font: z.string().optional().describe('Font strategy (default: auto). Built-ins: Helvetica, Times-Roman, Courier. Use a path or URL for Unicode.'), pageSetup: z .object({ size: z.tuple([z.number(), z.number()]).optional(), margins: z .object({ top: z.number(), bottom: z.number(), left: z.number(), right: z.number(), }) .optional(), backgroundColor: z.string().optional(), }) .optional(), content: z.array( z.union([ z.object({ type: z.literal('text'), text: z.string().optional(), fontSize: z.number().optional(), bold: z.boolean().optional(), color: z.string().optional(), x: z.number().optional(), y: z.number().optional(), align: z.enum(['left', 'center', 'right', 'justify']).optional(), indent: z.number().optional(), lineGap: z.number().optional(), paragraphGap: z.number().optional(), width: z.number().optional(), moveDown: z.number().optional(), underline: z.boolean().optional(), strike: z.boolean().optional(), oblique: z.union([z.boolean(), z.number()]).optional(), link: z.string().optional(), characterSpacing: z.number().optional(), wordSpacing: z.number().optional(), continued: z.boolean().optional(), lineBreak: z.boolean().optional(), }), z.object({ type: z.literal('heading'), text: z.string().optional(), fontSize: z.number().optional(), bold: z.boolean().optional(), color: z.string().optional(), x: z.number().optional(), y: z.number().optional(), align: z.enum(['left', 'center', 'right', 'justify']).optional(), indent: z.number().optional(), lineGap: z.number().optional(), paragraphGap: z.number().optional(), width: z.number().optional(), moveDown: z.number().optional(), underline: z.boolean().optional(), strike: z.boolean().optional(), oblique: z.union([z.boolean(), z.number()]).optional(), link: z.string().optional(), characterSpacing: z.number().optional(), wordSpacing: z.number().optional(), continued: z.boolean().optional(), lineBreak: z.boolean().optional(), }), z.object({ type: z.literal('image'), imagePath: z.string(), x: z.number().optional(), y: z.number().optional(), width: z.number().optional(), height: z.number().optional(), }), z.object({ type: z.literal('rect'), x: z.number(), y: z.number(), width: z.number(), height: z.number(), fillColor: z.string().optional(), strokeColor: z.string().optional(), lineWidth: z.number().optional(), }), z.object({ type: z.literal('circle'), x: z.number(), y: z.number(), radius: z.number(), fillColor: z.string().optional(), strokeColor: z.string().optional(), lineWidth: z.number().optional(), }), z.object({ type: z.literal('line'), x1: z.number(), y1: z.number(), x2: z.number(), y2: z.number(), strokeColor: z.string().optional(), lineWidth: z.number().optional(), }), z.object({ type: z.literal('pageBreak') }), ]) ), } as Record<string, z.ZodTypeAny>, }, async (args: CreatePdfArgs) => { const { filename = 'document.pdf', title, author, font, pageSetup, content } = args; try { const docOptions: PDFKit.PDFDocumentOptions = { info: { ...(title && { Title: title }), ...(author && { Author: author }), ...(filename && { Subject: filename }), }, }; if (pageSetup?.size) docOptions.size = pageSetup.size; if (pageSetup?.margins) docOptions.margins = pageSetup.margins; const doc = new PDFDocument(docOptions); const chunks: Buffer[] = []; doc.on('data', (c: Buffer) => chunks.push(c)); const pdfPromise = new Promise<Buffer>((resolve, reject) => { doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); }); if (pageSetup?.backgroundColor) { const pageSize = pageSetup?.size || [612, 792]; doc.rect(0, 0, pageSize[0], pageSize[1]).fill(pageSetup.backgroundColor); } const contentText = JSON.stringify(content); const containsEmoji = hasEmoji(contentText); const emojiAvailable = containsEmoji ? registerEmojiFont() : false; const fonts = await setupFonts(doc, font); const { regular: regularFont, bold: boldFont } = fonts; const warnings: string[] = []; for (const item of content) { if ((item.type === 'text' || item.type === 'heading') && item.text) { const fnt = item.bold ? boldFont : regularFont; const validation = validateTextForFont(item.text, fnt); if (validation.hasUnsupportedCharacters) warnings.push(...validation.warnings); } } const drawBackgroundOnPage = () => { if (pageSetup?.backgroundColor) { const pageSize = pageSetup?.size || [612, 792]; const x = doc.x; const y = doc.y; doc.rect(0, 0, pageSize[0], pageSize[1]).fill(pageSetup.backgroundColor); doc.x = x; doc.y = y; } }; doc.on('pageAdded', drawBackgroundOnPage); for (const item of content) { switch (item.type) { case 'text': { if (item.x !== undefined && item.align !== undefined) throw new Error('Cannot use both x and align in text element'); const fontSize = item.fontSize ?? 12; const fnt = item.bold ? boldFont : regularFont; if (item.color) doc.fillColor(item.color); const options = extractTextOptions(item); renderTextWithEmoji(doc, item.text ?? '', fontSize, fnt, emojiAvailable, options); if (item.color) doc.fillColor('black'); if (item.moveDown !== undefined) doc.moveDown(item.moveDown); break; } case 'heading': { if (item.x !== undefined && item.align !== undefined) throw new Error('Cannot use both x and align in heading element'); const fontSize = item.fontSize ?? 24; const fnt = item.bold !== false ? boldFont : regularFont; if (item.color) doc.fillColor(item.color); const options = extractTextOptions(item); renderTextWithEmoji(doc, item.text ?? '', fontSize, fnt, emojiAvailable, options); if (item.color) doc.fillColor('black'); if (item.moveDown !== undefined) doc.moveDown(item.moveDown); break; } case 'image': { const opts: Record<string, unknown> = {}; if (item.width !== undefined) opts.width = item.width; if (item.height !== undefined) opts.height = item.height; if (item.x !== undefined && item.y !== undefined) doc.image(item.imagePath, item.x, item.y, opts); else doc.image(item.imagePath, opts); break; } case 'rect': { doc.rect(item.x, item.y, item.width, item.height); if (item.fillColor && item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.fillAndStroke(item.fillColor, item.strokeColor); } else if (item.fillColor) doc.fill(item.fillColor); else if (item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.stroke(item.strokeColor); } doc.fillColor('black'); break; } case 'circle': { doc.circle(item.x, item.y, item.radius); if (item.fillColor && item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.fillAndStroke(item.fillColor, item.strokeColor); } else if (item.fillColor) doc.fill(item.fillColor); else if (item.strokeColor) { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc.stroke(item.strokeColor); } doc.fillColor('black'); break; } case 'line': { if (item.lineWidth) doc.lineWidth(item.lineWidth); doc .moveTo(item.x1, item.y1) .lineTo(item.x2, item.y2) .stroke(item.strokeColor || 'black'); break; } case 'pageBreak': { doc.addPage(); break; } } } doc.end(); const pdfBuffer = await pdfPromise; const uuid = crypto.randomUUID(); const storedFilename = `${uuid}.pdf`; const { fullPath } = await writePdfToFile(pdfBuffer, storedFilename, config.storageDir); const includePath = config.includePath; const warningText = warnings.length > 0 ? `\n\n⚠️ Character Warnings:\n${warnings.map((w) => `• ${w}`).join('\n')}` : ''; return { content: [ { type: 'text' as const, text: ['PDF created successfully', `Resource: mcp-pdf://${uuid}`, includePath ? `Output: ${fullPath}` : undefined, `Size: ${pdfBuffer.length} bytes`, filename !== 'document.pdf' ? `Filename: ${filename}` : undefined, warningText || undefined].filter(Boolean).join('\n'), }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text' as const, text: `Error creating PDF: ${message}` }], isError: true }; } } ); }
- src/server.ts:26-26 (registration)Call to registerCreatePdfTool during MCP server initialization, passing the server instance and config.registerCreatePdfTool(server, serverConfig);