#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { marked } from 'marked';
import puppeteer from 'puppeteer';
import matter from 'gray-matter';
import { readFile, writeFile } from 'fs/promises';
import { resolve } from 'path';
// Create server instance
const server = new McpServer({
name: "markdown-pdf-converter",
version: "1.0.0",
capabilities: {
resources: {},
tools: {},
},
});
class MarkdownToPdfConverter {
private options: any;
constructor(options = {}) {
this.options = {
format: 'A4',
margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' },
displayHeaderFooter: false,
printBackground: true,
...options
};
}
async convert(markdownPath: string, outputPath: string) {
const markdownContent = await readFile(resolve(markdownPath), 'utf8');
const { data: frontMatter, content } = matter(markdownContent);
const html = this.generateHtml(content, frontMatter);
const pdf = await this.htmlToPdf(html);
await writeFile(resolve(outputPath), pdf);
return resolve(outputPath);
}
generateHtml(markdown: string, frontMatter: any = {}) {
marked.use({
renderer: {
code: (token: any) => {
const code = token.text;
const language = token.lang;
if (language === 'mermaid') {
return `<div class="mermaid">${code}</div>`;
}
return `<pre><code class="language-${language || ''}">${code}</code></pre>`;
}
},
gfm: true,
breaks: false
});
const htmlContent = marked.parse(markdown);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${frontMatter.title || 'Document'}</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Open+Sans:wght@400;600;700&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 12pt;
line-height: 1.0;
color: #222222;
margin: 0;
padding: 0;
background: white;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Inter', sans-serif;
color: #1a1a1a;
margin: 1.5em 0 0.5em 0;
page-break-after: avoid;
}
h1 { font-size: 22pt; font-weight: 700; margin-top: 0; }
h2 { font-size: 18pt; font-weight: 600; color: #2d3748; }
h3 { font-size: 15pt; font-weight: 600; }
h4 { font-size: 14pt; font-weight: 500; }
p { margin: 0 0 1em 0; }
ul, ol { margin: 0.5em 0; padding-left: 1.2em; }
li { margin: 0.2em 0; }
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 12pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 8px 12px;
text-align: left;
}
th {
background: #f7fafc;
font-weight: 600;
}
pre {
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 11pt;
background: #f8f9fa;
padding: 1em;
margin: 1em 0;
border-radius: 4px;
overflow-x: auto;
}
code {
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 11pt;
background: #f1f3f4;
padding: 2px 4px;
border-radius: 3px;
}
.mermaid {
text-align: center;
margin: 1.5em 0;
}
strong { font-weight: 600; }
em { font-style: italic; }
</style>
</head>
<body>
${htmlContent}
<script>
mermaid.initialize({ startOnLoad: true, theme: 'default' });
</script>
</body>
</html>`;
}
async htmlToPdf(html: string) {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--font-render-hinting=none']
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 1600, deviceScaleFactor: 2 });
await page.setContent(html, { waitUntil: 'networkidle2' });
await Promise.race([
page.evaluateHandle('document.fonts.ready'),
new Promise(resolve => setTimeout(resolve, 5000))
]);
try {
await page.waitForFunction(() => {
const mermaidElements = document.querySelectorAll('.mermaid');
return mermaidElements.length === 0 ||
Array.from(mermaidElements).every(el => el.querySelector('svg'));
}, { timeout: 10000 });
} catch (e) {
console.error('Mermaid rendering timeout, proceeding anyway');
}
return await page.pdf({
format: this.options.format as any,
margin: this.options.margin,
displayHeaderFooter: this.options.displayHeaderFooter,
printBackground: this.options.printBackground,
tagged: true,
outline: true
});
} finally {
await browser.close();
}
}
}
// Register markdown to PDF conversion tool
server.tool(
"convert_markdown_to_pdf",
"Convert a markdown file to PDF",
{
markdownPath: z.string().describe("Path to the markdown file to convert"),
outputPath: z.string().describe("Path where the PDF should be saved"),
format: z.enum(['A4', 'A3', 'A5', 'Letter', 'Legal', 'Tabloid']).optional().describe("PDF page format (default: A4)"),
margin: z.object({
top: z.string().optional(),
right: z.string().optional(),
bottom: z.string().optional(),
left: z.string().optional()
}).optional().describe("PDF margins (e.g., '0.5in', '20mm')")
},
async ({ markdownPath, outputPath, format, margin }) => {
try {
const options: any = {};
if (format) options.format = format;
if (margin) options.margin = margin;
const converter = new MarkdownToPdfConverter(options);
const resultPath = await converter.convert(markdownPath, outputPath);
return {
content: [
{
type: "text",
text: `Successfully converted markdown to PDF!\nInput: ${markdownPath}\nOutput: ${resultPath}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error converting markdown to PDF: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
// Register markdown content to PDF tool
server.tool(
"markdown_content_to_pdf",
"Convert markdown content directly to PDF",
{
markdownContent: z.string().describe("Markdown content to convert"),
outputPath: z.string().describe("Path where the PDF should be saved"),
title: z.string().optional().describe("Document title for the PDF"),
format: z.enum(['A4', 'A3', 'A5', 'Letter', 'Legal', 'Tabloid']).optional().describe("PDF page format (default: A4)"),
margin: z.object({
top: z.string().optional(),
right: z.string().optional(),
bottom: z.string().optional(),
left: z.string().optional()
}).optional().describe("PDF margins (e.g., '0.5in', '20mm')")
},
async ({ markdownContent, outputPath, title, format, margin }) => {
try {
const options: any = {};
if (format) options.format = format;
if (margin) options.margin = margin;
const converter = new MarkdownToPdfConverter(options);
const frontMatter = title ? { title } : {};
const html = converter.generateHtml(markdownContent, frontMatter);
const pdf = await converter.htmlToPdf(html);
await writeFile(resolve(outputPath), pdf);
const resultPath = resolve(outputPath);
return {
content: [
{
type: "text",
text: `Successfully converted markdown content to PDF!\nOutput: ${resultPath}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error converting markdown content to PDF: ${error instanceof Error ? error.message : String(error)}`
}
]
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Markdown to PDF MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});