Skip to main content
Glama

MCP PDF

create-pdf.ts12.4 kB
import crypto from 'node:crypto'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import PDFDocument from 'pdfkit'; import { z } from 'zod/v3'; import type { PdfServerConfig } from '../lib/config.ts'; import { registerEmojiFont } from '../lib/emoji-renderer.ts'; import { hasEmoji, setupFonts, validateTextForFont } from '../lib/fonts.ts'; import { writePdfToFile } from '../lib/output-handler.ts'; import { renderTextWithEmoji } from '../lib/pdf-helpers.ts'; 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 any, }, async (args: any) => { const { filename = 'document.pdf', title, author, font, pageSetup, content } = args; try { const docOptions: any = { 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: any = {}; ['x', 'y', 'align', 'indent', 'lineGap', 'paragraphGap', 'width', 'underline', 'strike', 'oblique', 'link', 'characterSpacing', 'wordSpacing', 'continued', 'lineBreak'].forEach((k) => { if ((item as any)[k] !== undefined) options[k] = (item as any)[k]; }); 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: any = {}; ['x', 'y', 'align', 'indent', 'lineGap', 'paragraphGap', 'width', 'underline', 'strike', 'oblique', 'link', 'characterSpacing', 'wordSpacing', 'continued', 'lineBreak'].forEach((k) => { if ((item as any)[k] !== undefined) options[k] = (item as any)[k]; }); 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 }; } } ); }

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