Skip to main content
Glama

MCP Printer Server

by steveclarke
markdown.tsβ€’5.99 kB
/** * @fileoverview Markdown file renderer. * Converts markdown files to PDF using crossnote. * Provides beautiful Markdown Preview Enhanced-quality output with Mermaid diagram support. * Automatically adds page numbering to all rendered PDFs. */ import { basename, join } from "path" import { readFileSync, writeFileSync, mkdtempSync, unlinkSync } from "fs" import { tmpdir } from "os" import matter from "gray-matter" import he from "he" import { findChrome } from "../utils.js" import { validateFilePath } from "../file-security.js" import { config } from "../config.js" import { Notebook } from "crossnote" /** * Page numbering configuration function for Puppeteer PDF generation. * Generates header/footer templates with inline styles (required by Puppeteer). * Filename is HTML-escaped to prevent XSS and rendering issues. * * @param filename - The name of the file being rendered (displayed in footer) * @returns Configuration object with header/footer templates */ function getPageNumberConfig(filename: string) { // Puppeteer PDF headers/footers require inline styles (no external CSS support) const footerStyles = { container: [ "font-size: 9px", "width: 100%", "margin: 0", "padding: 0 1cm", "font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'", "display: flex", "justify-content: space-between", "align-items: center", ].join("; "), filename: "font-size: 8px; color: #666;", } return { displayHeaderFooter: true, headerTemplate: "<div></div>", footerTemplate: ` <div style="${footerStyles.container}"> <span style="${footerStyles.filename}">${he.encode(filename)}</span> <span><span class="pageNumber"></span> / <span class="totalPages"></span></span> </div> `, margin: { top: "1cm", bottom: "1.5cm", left: "1cm", right: "1cm", }, } } /** * Injects page numbering configuration into markdown content. * Properly merges with existing front-matter if present. * Uses gray-matter's stringify for robust formatting. * @param content - Original markdown content * @param filename - Name of the file being rendered (displayed in footer) * @returns Markdown content with page numbering front-matter added/merged */ function injectPageNumbering(content: string, filename: string): string { const { data, content: body } = matter(content) // Check if user already has chrome or puppeteer config in their frontmatter - // respect their settings if (data.chrome || data.puppeteer) { return content // Don't modify user's existing config } // Merge in the chrome config with existing front-matter (even if empty) const mergedFrontMatter = { ...data, chrome: getPageNumberConfig(filename), } // Use gray-matter's stringify to properly format the document return matter.stringify(body, mergedFrontMatter) } /** * Renders a markdown file to PDF using crossnote. * Provides beautiful Markdown Preview Enhanced-quality output with * comprehensive diagram support. Automatically adds page numbering (Page X / * Y) to the footer of each page. * * @param filePath - Path to the markdown file to render * @returns Path to the generated temporary PDF file * @throws {Error} If Chrome is not found or rendering fails */ export async function renderMarkdownToPdf(filePath: string): Promise<string> { // Validate file path security validateFilePath(filePath) // Read the original markdown content const originalContent = readFileSync(filePath, "utf-8") // Inject page numbering configuration if not already present const contentWithPageNumbers = injectPageNumbering(originalContent, basename(filePath)) // Create a temporary directory for the modified markdown file const tempDir = mkdtempSync(join(tmpdir(), "mcp-printer-markdown-")) const tempFileName = basename(filePath) const tempFilePath = join(tempDir, tempFileName) // Write the modified content to temp file writeFileSync(tempFilePath, contentWithPageNumbers, "utf-8") // Find Chrome executable (required by crossnote/puppeteer) const chromePath = config.chromePath || (await findChrome()) let outputPath: string try { // Initialize crossnote notebook with configuration, pointing to temp directory const notebook = await Notebook.init({ notebookPath: tempDir, config: { // Hardcoded themes optimized for printing (light background, clean styling) previewTheme: "github-light.css", codeBlockTheme: "github.css", mermaidTheme: "default", // Math rendering mathRenderingOption: "KaTeX", // Rendering options printBackground: true, breakOnSingleNewLine: true, enableEmojiSyntax: true, enableWikiLinkSyntax: false, enableExtendedTableSyntax: false, // Chrome configuration chromePath, // Security: disable script execution enableScriptExecution: false, }, }) // Get the markdown engine for the temp file const engine = notebook.getNoteMarkdownEngine(tempFileName) // Export to PDF using Chrome/Puppeteer // crossnote returns the path to the generated PDF outputPath = await engine.chromeExport({ fileType: "pdf", runAllCodeChunks: false, // Don't execute code chunks for security }) } catch (error: unknown) { // Clean up temp file before throwing try { unlinkSync(tempFilePath) } catch { // Ignore cleanup errors } throw new Error( `Failed to render markdown with crossnote: ${error instanceof Error ? error.message : String(error)}` ) } // Clean up temp markdown file (PDF is in a different location) try { unlinkSync(tempFilePath) } catch { // Ignore cleanup errors - temp files will be cleaned up by OS eventually } return outputPath }

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/steveclarke/mcp-printer'

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