Skip to main content
Glama

generate-resume-pdf

Convert JSON Resume format data into a professional PDF document with customizable styling options for layout, fonts, and formatting.

Instructions

Generate a professional resume PDF from JSON Resume format. Follows the standard JSON Resume schema (https://jsonresume.org/schema). Supports basics, work, education, projects, skills, awards, certificates, languages, and more. Includes customizable styling options.

Input Schema

NameRequiredDescriptionDefault
filenameNoOptional filename for the PDF (defaults to "resume.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. Defaults to "auto" (system font detection). Options: • Built-in: Helvetica, Times-Roman, Courier (+ Bold/Italic variants) • URL: https://cdn.../font.woff2 (for Unicode/emoji support) • Path: /System/Library/Fonts/Arial.ttf • "auto": Auto-detect Unicode-capable system font Built-in fonts only support ASCII. For Unicode, use a font URL or path. Find Unicode fonts at https://fontsource.org
resumeYesResume data in JSON Resume format
stylingNoOptional styling customization for the resume layout

Input Schema (JSON Schema)

{ "properties": { "filename": { "description": "Optional filename for the PDF (defaults to \"resume.pdf\").\n\nSECURITY: Filenames are sanitized and written to a sandboxed directory:\n• Default: ~/.mcp-pdf/\n• Override: Set PDF_OUTPUT_DIR environment variable\n• Path traversal attempts (.., /, etc) are blocked\n• Only alphanumeric, spaces, hyphens, underscores, and dots allowed\n• If file exists, timestamp is appended automatically", "type": "string" }, "font": { "description": "Font for the PDF. Defaults to \"auto\" (system font detection).\n\nOptions:\n• Built-in: Helvetica, Times-Roman, Courier (+ Bold/Italic variants)\n• URL: https://cdn.../font.woff2 (for Unicode/emoji support)\n• Path: /System/Library/Fonts/Arial.ttf\n• \"auto\": Auto-detect Unicode-capable system font\n\nBuilt-in fonts only support ASCII. For Unicode, use a font URL or path.\nFind Unicode fonts at https://fontsource.org", "type": "string" }, "resume": { "additionalProperties": false, "description": "Resume data in JSON Resume format", "properties": { "awards": { "items": { "additionalProperties": false, "properties": { "awarder": { "type": "string" }, "date": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "summary": { "type": "string" }, "title": { "type": "string" } }, "type": "object" }, "type": "array" }, "basics": { "additionalProperties": false, "properties": { "email": { "format": "email", "type": "string" }, "image": { "format": "uri", "type": "string" }, "label": { "type": "string" }, "location": { "additionalProperties": false, "properties": { "address": { "type": "string" }, "city": { "type": "string" }, "countryCode": { "type": "string" }, "postalCode": { "type": "string" }, "region": { "type": "string" } }, "type": "object" }, "name": { "type": "string" }, "phone": { "type": "string" }, "profiles": { "items": { "additionalProperties": false, "properties": { "network": { "type": "string" }, "url": { "format": "uri", "type": "string" }, "username": { "type": "string" } }, "type": "object" }, "type": "array" }, "summary": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "certificates": { "items": { "additionalProperties": false, "properties": { "date": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "issuer": { "type": "string" }, "name": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "type": "array" }, "education": { "items": { "additionalProperties": false, "properties": { "area": { "type": "string" }, "courses": { "items": { "type": "string" }, "type": "array" }, "endDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "institution": { "type": "string" }, "score": { "type": "string" }, "startDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "studyType": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "type": "array" }, "interests": { "items": { "additionalProperties": false, "properties": { "keywords": { "items": { "type": "string" }, "type": "array" }, "name": { "type": "string" } }, "type": "object" }, "type": "array" }, "languages": { "items": { "additionalProperties": false, "properties": { "fluency": { "type": "string" }, "language": { "type": "string" } }, "type": "object" }, "type": "array" }, "projects": { "items": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "endDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "entity": { "type": "string" }, "highlights": { "items": { "type": "string" }, "type": "array" }, "keywords": { "items": { "type": "string" }, "type": "array" }, "name": { "type": "string" }, "roles": { "items": { "type": "string" }, "type": "array" }, "startDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "type": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "type": "array" }, "publications": { "items": { "additionalProperties": false, "properties": { "name": { "type": "string" }, "publisher": { "type": "string" }, "releaseDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "summary": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "type": "array" }, "references": { "items": { "additionalProperties": false, "properties": { "name": { "type": "string" }, "reference": { "type": "string" } }, "type": "object" }, "type": "array" }, "skills": { "items": { "additionalProperties": false, "properties": { "keywords": { "items": { "type": "string" }, "type": "array" }, "level": { "type": "string" }, "name": { "type": "string" } }, "type": "object" }, "type": "array" }, "volunteer": { "items": { "additionalProperties": false, "properties": { "endDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "highlights": { "items": { "type": "string" }, "type": "array" }, "organization": { "type": "string" }, "position": { "type": "string" }, "startDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "summary": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "type": "array" }, "work": { "items": { "additionalProperties": false, "properties": { "description": { "type": "string" }, "endDate": { "$ref": "#/properties/resume/properties/work/items/properties/startDate" }, "highlights": { "items": { "type": "string" }, "type": "array" }, "location": { "type": "string" }, "name": { "type": "string" }, "position": { "type": "string" }, "startDate": { "pattern": "^\\d{4}(-\\d{2}(-\\d{2})?)?$", "type": "string" }, "summary": { "type": "string" }, "url": { "format": "uri", "type": "string" } }, "type": "object" }, "type": "array" } }, "type": "object" }, "styling": { "additionalProperties": false, "description": "Optional styling customization for the resume layout", "properties": { "alignment": { "additionalProperties": false, "description": "Text alignment overrides", "properties": { "header": { "description": "Header alignment (default: center)", "enum": [ "left", "center", "right" ], "type": "string" } }, "type": "object" }, "fontSize": { "additionalProperties": false, "description": "Font size overrides for different text elements", "properties": { "body": { "description": "Body text font size (default: 10)", "type": "number" }, "contact": { "description": "Contact info font size (default: 10)", "type": "number" }, "heading": { "description": "Section heading font size (default: 18)", "type": "number" }, "label": { "description": "Job title/label font size (default: 12)", "type": "number" }, "name": { "description": "Name/title font size (default: 24)", "type": "number" }, "subheading": { "description": "Subsection heading font size (default: 14)", "type": "number" } }, "type": "object" }, "margins": { "additionalProperties": false, "description": "Page margin overrides", "properties": { "bottom": { "description": "Bottom margin in points (default: 50)", "type": "number" }, "left": { "description": "Left margin in points (default: 50)", "type": "number" }, "right": { "description": "Right margin in points (default: 50)", "type": "number" }, "top": { "description": "Top margin in points (default: 50)", "type": "number" } }, "type": "object" }, "spacing": { "additionalProperties": false, "description": "Spacing overrides (in moveDown units)", "properties": { "afterContact": { "description": "Space after contact info (default: 0.5)", "type": "number" }, "afterHeading": { "description": "Space after section headings (default: 0.5)", "type": "number" }, "afterLabel": { "description": "Space after label (default: 0.3)", "type": "number" }, "afterName": { "description": "Space after name (default: 0.3)", "type": "number" }, "afterSubheading": { "description": "Space after subsection headings (default: 0.3)", "type": "number" }, "afterText": { "description": "Space after body text (default: 0.3)", "type": "number" }, "betweenSections": { "description": "Space between major sections (default: 0.5)", "type": "number" } }, "type": "object" } }, "type": "object" } }, "required": [ "resume" ], "type": "object" }

Implementation Reference

  • MCP tool handler that validates input, generates PDF using helper, stores file with UUID, and returns success message with resource URI or error.
    async (args: GenerateResumePdfArgs) => { const { filename = 'resume.pdf', resume, font, styling } = args; try { const pdfBuffer = await generateResumePDFBuffer(resume, font, styling); const uuid = crypto.randomUUID(); const storedFilename = `${uuid}.pdf`; const { fullPath } = await writePdfToFile(pdfBuffer, storedFilename, config.storageDir); const includePath = config.includePath; return { content: [ { type: 'text' as const, text: ['Resume PDF generated successfully', `Resource: mcp-pdf://${uuid}`, includePath ? `Output: ${fullPath}` : undefined, `Size: ${pdfBuffer.length} bytes`, filename !== 'resume.pdf' ? `Filename: ${filename}` : undefined].filter(Boolean).join('\n'), }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text' as const, text: `Error generating resume PDF: ${message}` }], isError: true }; } }
  • Core helper function that generates the actual PDF buffer from JSON Resume data using PDFKit. Handles styling, fonts (Unicode/emoji), all resume sections (basics, work, education, projects, skills, etc.), and text rendering.
    export async function generateResumePDFBuffer(resume: JsonResume, font?: string, styling?: ResumeStyling): Promise<Buffer> { // Merge styling with defaults const margins = { top: styling?.margins?.top ?? 50, bottom: styling?.margins?.bottom ?? 50, left: styling?.margins?.left ?? 50, right: styling?.margins?.right ?? 50, }; const fontSize = { name: styling?.fontSize?.name ?? 24, label: styling?.fontSize?.label ?? 12, heading: styling?.fontSize?.heading ?? 18, subheading: styling?.fontSize?.subheading ?? 14, body: styling?.fontSize?.body ?? 10, contact: styling?.fontSize?.contact ?? 10, }; const spacing = { afterName: styling?.spacing?.afterName ?? 0.3, afterLabel: styling?.spacing?.afterLabel ?? 0.3, afterContact: styling?.spacing?.afterContact ?? 0.5, afterHeading: styling?.spacing?.afterHeading ?? 0.5, afterSubheading: styling?.spacing?.afterSubheading ?? 0.3, afterText: styling?.spacing?.afterText ?? 0.3, betweenSections: styling?.spacing?.betweenSections ?? 0.5, }; const alignment = { header: styling?.alignment?.header ?? 'center', }; const doc = new PDFDocument({ margins, info: { Title: resume.basics?.name ? `Resume - ${resume.basics.name}` : 'Resume', ...(resume.basics?.name && { Author: resume.basics.name }), }, }); // Capture PDF in memory const chunks: Buffer[] = []; doc.on('data', (chunk: Buffer) => chunks.push(chunk)); const pdfPromise = new Promise<Buffer>((resolve, reject) => { doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); }); // Check if content has Unicode characters or emoji const resumeText = JSON.stringify(resume); const containsUnicode = needsUnicodeFont(resumeText); const containsEmoji = hasEmoji(resumeText); const isDefaultFont = !font || font === 'auto'; // Register emoji font for rendering const emojiAvailable = containsEmoji ? registerEmojiFont() : false; // Warn about emoji if font not available if (containsEmoji && !emojiAvailable) { console.warn('⚠️ EMOJI DETECTED but emoji font not available.\n' + ' Run: npm install (to download Noto Color Emoji)\n' + ' Emojis will be skipped in the PDF.'); } else if (containsEmoji && emojiAvailable) { console.log('✅ Emoji support enabled - rendering emojis as inline images'); } // Warn if Unicode detected with default font if (containsUnicode && isDefaultFont && !containsEmoji) { console.warn("⚠️ Unicode characters detected. If they don't render properly, " + 'provide a Unicode font URL. Find fonts at https://fontsource.org'); } // Setup fonts const fonts = await setupFonts(doc, font); const { regular: regularFont, bold: boldFont, oblique: obliqueFont } = fonts; // Helper functions const addHeading = (text: string) => { renderTextWithEmoji(doc, text, fontSize.heading, boldFont, emojiAvailable); doc.moveDown(spacing.afterHeading); }; const addSubheading = (text: string) => { renderTextWithEmoji(doc, text, fontSize.subheading, boldFont, emojiAvailable); doc.moveDown(spacing.afterSubheading); }; const addText = (text: string, indent = 0) => { renderTextWithEmoji(doc, text, fontSize.body, regularFont, emojiAvailable, { indent }); doc.moveDown(spacing.afterText); }; const addBullets = (items: string[]) => { for (const item of items) { renderTextWithEmoji(doc, `• ${item}`, fontSize.body, regularFont, emojiAvailable, { indent: 20 }); } doc.moveDown(spacing.afterText); }; const formatDate = (date?: string) => { if (!date) return ''; // Handle YYYY, YYYY-MM, YYYY-MM-DD formats const parts = date.split('-'); if (parts.length === 1) return parts[0]; // YYYY if (parts.length === 2) { // YYYY-MM const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const monthIndex = Number.parseInt(parts[1] ?? '1', 10) - 1; return `${months[monthIndex]} ${parts[0]}`; } // YYYY-MM-DD const date2 = new Date(date); return date2.toLocaleDateString('en-US', { month: 'short', year: 'numeric', }); }; // BASICS SECTION if (resume.basics) { const { name, label, email, phone, url, location, summary, profiles } = resume.basics; if (name) { renderTextWithEmoji(doc, name, fontSize.name, boldFont, emojiAvailable, { align: alignment.header }); doc.moveDown(spacing.afterName); } if (label) { renderTextWithEmoji(doc, label, fontSize.label, regularFont, emojiAvailable, { align: alignment.header }); doc.moveDown(spacing.afterLabel); } // Contact info const contactInfo = []; if (email) contactInfo.push(email); if (phone) contactInfo.push(phone); if (url) contactInfo.push(url); if (location?.city && location?.region) { contactInfo.push(`${location.city}, ${location.region}`); } else if (location?.city) { contactInfo.push(location.city); } if (contactInfo.length > 0) { renderTextWithEmoji(doc, contactInfo.join(' | '), fontSize.contact, regularFont, emojiAvailable, { align: alignment.header, }); doc.moveDown(spacing.afterContact); } // Profiles if (profiles && profiles.length > 0) { const profileLinks = profiles .map((p) => { if (p.network && p.username) return `${p.network}: ${p.username}`; if (p.url) return p.url; return null; }) .filter(Boolean); if (profileLinks.length > 0) { renderTextWithEmoji(doc, profileLinks.join(' | '), fontSize.contact, regularFont, emojiAvailable, { align: alignment.header, }); doc.moveDown(spacing.afterContact); } } doc.moveDown(spacing.betweenSections); // Summary if (summary) { addHeading('Summary'); addText(summary); doc.moveDown(spacing.betweenSections); } } // WORK EXPERIENCE if (resume.work && resume.work.length > 0) { addHeading('Experience'); for (const job of resume.work) { if (job.position || job.name) { const title = [job.position, job.name].filter(Boolean).join(' at '); addSubheading(title); } const details = []; if (job.location) details.push(job.location); if (job.startDate || job.endDate) { const start = formatDate(job.startDate) || 'Present'; const end = formatDate(job.endDate) || 'Present'; details.push(`${start} - ${end}`); } if (details.length > 0) { renderTextWithEmoji(doc, details.join(' | '), fontSize.body, obliqueFont, emojiAvailable); doc.moveDown(spacing.afterText); } if (job.summary) { addText(job.summary); } if (job.highlights && job.highlights.length > 0) { addBullets(job.highlights); } doc.moveDown(spacing.betweenSections); } } // EDUCATION if (resume.education && resume.education.length > 0) { addHeading('Education'); for (const edu of resume.education) { const degree = [edu.studyType, edu.area].filter(Boolean).join(' in '); if (degree) { addSubheading(degree); } const details = []; if (edu.institution) details.push(edu.institution); if (edu.startDate || edu.endDate) { const start = formatDate(edu.startDate) || ''; const end = formatDate(edu.endDate) || 'Present'; details.push(`${start} - ${end}`); } if (edu.score) details.push(`GPA: ${edu.score}`); if (details.length > 0) { renderTextWithEmoji(doc, details.join(' | '), fontSize.body, regularFont, emojiAvailable); doc.moveDown(spacing.afterText); } if (edu.courses && edu.courses.length > 0) { renderTextWithEmoji(doc, `Courses: ${edu.courses.join(', ')}`, fontSize.body, regularFont, emojiAvailable); doc.moveDown(spacing.afterText); } doc.moveDown(spacing.betweenSections); } } // PROJECTS if (resume.projects && resume.projects.length > 0) { addHeading('Projects'); for (const project of resume.projects) { if (project.name) { addSubheading(project.name); } if (project.description) { addText(project.description); } if (project.highlights && project.highlights.length > 0) { addBullets(project.highlights); } const details = []; if (project.url) details.push(project.url); if (project.keywords && project.keywords.length > 0) { details.push(`Tech: ${project.keywords.join(', ')}`); } if (details.length > 0) { renderTextWithEmoji(doc, details.join(' | '), fontSize.body, obliqueFont, emojiAvailable); doc.moveDown(spacing.afterText); } doc.moveDown(spacing.betweenSections); } } // SKILLS if (resume.skills && resume.skills.length > 0) { addHeading('Skills'); for (const skill of resume.skills) { if (skill.name) { const skillText = skill.keywords ? `${skill.name}: ${skill.keywords.join(', ')}` : skill.name; addText(skillText); } } doc.moveDown(spacing.betweenSections); } // AWARDS if (resume.awards && resume.awards.length > 0) { addHeading('Awards'); for (const award of resume.awards) { if (award.title) { addSubheading(award.title); } const details = []; if (award.awarder) details.push(award.awarder); if (award.date) details.push(formatDate(award.date)); if (details.length > 0) { renderTextWithEmoji(doc, details.join(' | '), fontSize.body, regularFont, emojiAvailable); doc.moveDown(spacing.afterText); } if (award.summary) { addText(award.summary); } doc.moveDown(spacing.betweenSections); } } // CERTIFICATES if (resume.certificates && resume.certificates.length > 0) { addHeading('Certificates'); for (const cert of resume.certificates) { if (cert.name) { addSubheading(cert.name); } const details = []; if (cert.issuer) details.push(cert.issuer); if (cert.date) details.push(formatDate(cert.date)); if (cert.url) details.push(cert.url); if (details.length > 0) { renderTextWithEmoji(doc, details.join(' | '), fontSize.body, regularFont, emojiAvailable); doc.moveDown(spacing.afterText); } doc.moveDown(spacing.betweenSections); } } // LANGUAGES if (resume.languages && resume.languages.length > 0) { addHeading('Languages'); const langText = resume.languages .map((lang) => { if (lang.language && lang.fluency) { return `${lang.language} (${lang.fluency})`; } return lang.language || ''; }) .filter(Boolean) .join(', '); if (langText) { addText(langText); } doc.moveDown(spacing.betweenSections); } // Finalize doc.end(); // Return the PDF buffer return await pdfPromise; }
  • Zod input schema for the tool, defining parameters: filename, resume (JsonResume), font, and detailed styling options for fontsizes, spacing, alignment, margins.
    inputSchema: { filename: z.string().optional().describe('Optional logical filename (metadata only). Storage uses UUID. Defaults to "resume.pdf".'), resume: jsonResumeSchema.describe('Resume data in JSON Resume format'), font: z.string().optional().describe('Font for the PDF. Defaults to "auto" (system font detection). Built-ins are limited to ASCII; provide a path or URL for full Unicode.'), styling: z .object({ fontSize: z .object({ name: z.number().optional(), label: z.number().optional(), heading: z.number().optional(), subheading: z.number().optional(), body: z.number().optional(), contact: z.number().optional(), }) .optional(), spacing: z .object({ afterName: z.number().optional(), afterLabel: z.number().optional(), afterContact: z.number().optional(), afterHeading: z.number().optional(), afterSubheading: z.number().optional(), afterText: z.number().optional(), betweenSections: z.number().optional(), }) .optional(), alignment: z .object({ header: z.enum(['left', 'center', 'right']).optional(), }) .optional(), margins: z .object({ top: z.number().optional(), bottom: z.number().optional(), left: z.number().optional(), right: z.number().optional(), }) .optional(), }) .optional(), } as Record<string, z.ZodTypeAny>,
  • Tool registration call within registerGenerateResumePdfTool function, specifying name 'generate-resume-pdf', title, description, schema, and handler.
    server.registerTool( 'generate-resume-pdf', { title: 'Generate Resume PDF', description: 'Generate a professional resume PDF from JSON Resume format. Supports styling, fonts, spacing, and multiple sections.', inputSchema: { filename: z.string().optional().describe('Optional logical filename (metadata only). Storage uses UUID. Defaults to "resume.pdf".'), resume: jsonResumeSchema.describe('Resume data in JSON Resume format'), font: z.string().optional().describe('Font for the PDF. Defaults to "auto" (system font detection). Built-ins are limited to ASCII; provide a path or URL for full Unicode.'), styling: z .object({ fontSize: z .object({ name: z.number().optional(), label: z.number().optional(), heading: z.number().optional(), subheading: z.number().optional(), body: z.number().optional(), contact: z.number().optional(), }) .optional(), spacing: z .object({ afterName: z.number().optional(), afterLabel: z.number().optional(), afterContact: z.number().optional(), afterHeading: z.number().optional(), afterSubheading: z.number().optional(), afterText: z.number().optional(), betweenSections: z.number().optional(), }) .optional(), alignment: z .object({ header: z.enum(['left', 'center', 'right']).optional(), }) .optional(), margins: z .object({ top: z.number().optional(), bottom: z.number().optional(), left: z.number().optional(), right: z.number().optional(), }) .optional(), }) .optional(), } as Record<string, z.ZodTypeAny>, }, async (args: GenerateResumePdfArgs) => { const { filename = 'resume.pdf', resume, font, styling } = args; try { const pdfBuffer = await generateResumePDFBuffer(resume, font, styling); const uuid = crypto.randomUUID(); const storedFilename = `${uuid}.pdf`; const { fullPath } = await writePdfToFile(pdfBuffer, storedFilename, config.storageDir); const includePath = config.includePath; return { content: [ { type: 'text' as const, text: ['Resume PDF generated successfully', `Resource: mcp-pdf://${uuid}`, includePath ? `Output: ${fullPath}` : undefined, `Size: ${pdfBuffer.length} bytes`, filename !== 'resume.pdf' ? `Filename: ${filename}` : undefined].filter(Boolean).join('\n'), }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text' as const, text: `Error generating resume PDF: ${message}` }], isError: true }; } } );
  • src/server.ts:28-28 (registration)
    Invocation of the register function during server creation, which registers the tool on the MCP server.
    registerGenerateResumePdfTool(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