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
| Name | Required | Description | Default |
|---|---|---|---|
| filename | No | Optional 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 | |
| font | No | Font 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 | |
| resume | Yes | Resume data in JSON Resume format | |
| styling | No | Optional 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
- src/tools/generate-resume-pdf.ts:95-115 (handler)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 }; } }
- src/resume-generator.ts:36-399 (helper)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>,
- src/tools/generate-resume-pdf.ts:46-116 (registration)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);