Skip to main content
Glama

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:

  1. Simple Document (no pageSetup needed): [{"type": "heading", "text": "Title"}, {"type": "text", "text": "Body"}]

  2. Styled Document (add colors/background): pageSetup: {"backgroundColor": "#F5F5F5"} content: [{"type": "heading", "text": "Title", "color": "#4A90E2"}]

  3. 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"}]

  4. 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

TableJSON Schema
NameRequiredDescriptionDefault
authorNoDocument author metadata
contentYesArray of content items to add to the PDF
filenameNoOptional 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
fontNoFont 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.
pageSetupNoOptional page setup configuration
titleNoDocument title metadata

Implementation Reference

  • 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 }; } }
  • 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>, },
  • 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);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mcp-z/mcp-pdf'

If you have feedback or need assistance with the MCP directory API, please join our Discord server