markdown2pdf
Convert Markdown text into high-quality PDFs with LaTeX rendering. Include a title and optional date for professional document creation. Payments are processed via the Lightning Network, eliminating the need for sign-ups or credit cards.
Instructions
Convert markdown to PDF, and pay with Lightning
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| date | No | Document date (YYYY-MM-DD) | |
| text_body | Yes | Markdown text to convert | |
| title | Yes | Document title |
Implementation Reference
- src/markdown2pdf_mcp_server.ts:56-199 (handler)Core handler function that implements the 'markdown2pdf' tool logic: validates input (text_body, title), sends markdown to external API, handles Lightning payment (402 status), polls for PDF generation, and returns the download URL or payment details.async function handleRequest(req: McpRequest): Promise<McpResponse> { if (!req.method) { return createErrorResponse(req.id, RPC_ERRORS.INVALID_REQUEST); } if (req.method !== 'markdown2pdf') { return createErrorResponse(req.id, RPC_ERRORS.METHOD_NOT_FOUND); } const { text_body, title } = req.params ?? {}; if (!text_body || !title) { return createErrorResponse(req.id, { code: RPC_ERRORS.INVALID_PARAMS.code, message: "Invalid params: text_body and title are required" }); } const today = req.params?.date || new Date().toISOString().split('T')[0]; const payload = { data: { text_body, meta: { title, date: today } }, options: { document_name: title } }; let response; let pollUrl = 'https://api.markdown2pdf.ai/markdown'; let headers = { 'Content-Type': 'application/json' }; // Payment loop while (true) { try { response = await fetch(pollUrl, { method: 'POST', headers, body: JSON.stringify(payload) }); if (response.status === 402) { const resJson = await response.json(); const payment_request_url = resJson.payment_request_url const payment_context_token = resJson.payment_context_token; const offers = resJson.offers || []; const offer_id = offers.length > 0 ? offers[0].id : null; const offer_amount = offers.length > 0 ? offers[0].amount : null; const offer_currency = offers.length > 0 ? offers[0].currency : null; const description = offers.length > 0 ? offers[0].description : "Markdown to PDF conversion"; const payment_request_payload = { payment_context_token: payment_context_token, offer_id: offer_id } let payment_request_response = await fetch(payment_request_url, { method: 'POST', headers, body: JSON.stringify(payment_request_payload) }); const payment_request_json = await payment_request_response.json(); debug("Payment request response: `${payment_request_json}`"); debug("Payment request status: `${payment_request_response.status}`"); debug("Payment request headers: `${payment_request_response.status}`"); debug("Payment request URL: `${payment_request_url}`"); debug("Payment request payload: `${payment_request_payload}`"); const lightning_invoice = payment_request_json.payment_request.payment_request; const lightning_invoice_qr = payment_request_json.payment_request.payment_qr_svg; return createResponse(req.id, { content: [{ type: "text", text: JSON.stringify({ status: "Payment required. Please pay the invoice and try the same request again to continue.", qr_svg_url: lightning_invoice_qr, payment_request: lightning_invoice, detail: description }) }] }); } else if (response.status === 200) { const resJson = await response.json(); if (resJson.path) { pollUrl = resJson.path; break; } } else { throw new Error(`Unexpected response: ${response.status}`); } } catch (err) { return createErrorResponse(req.id, { code: RPC_ERRORS.INTERNAL_ERROR.code, message: `Request failed: ${err instanceof Error ? err.message : String(err)}` }); } } // Poll status while (true) { try { response = await fetch(pollUrl, { method: 'GET', headers }); const resJson = await response.json(); if (resJson.status === 'Done' && resJson.path) { pollUrl = resJson.path; break; } await new Promise(res => setTimeout(res, 3000)); } catch (err) { return createErrorResponse(req.id, { code: RPC_ERRORS.INTERNAL_ERROR.code, message: `Polling failed: ${err instanceof Error ? err.message : String(err)}` }); } } // Fetch final output try { response = await fetch(pollUrl, { method: 'GET', headers }); const resJson = await response.json(); if (resJson.url) { return createResponse(req.id, { content: [{ type: "text", text: JSON.stringify({ status: "complete", url: resJson.url }) }] }); } else { return createErrorResponse(req.id, { code: RPC_ERRORS.INTERNAL_ERROR.code, message: "PDF URL not found in response" }); } } catch (err) { return createErrorResponse(req.id, { code: RPC_ERRORS.INTERNAL_ERROR.code, message: `Failed to fetch PDF: ${err instanceof Error ? err.message : String(err)}` }); } }
- Input schema for the 'markdown2pdf' tool defining required parameters text_body (markdown content) and title, with optional date.inputSchema: { type: 'object', required: ['text_body', 'title'], properties: { text_body: { type: 'string', description: 'Markdown text to convert' }, title: { type: 'string', description: 'Document title' }, date: { type: 'string', description: 'Document date (YYYY-MM-DD)' } } }
- src/markdown2pdf_mcp_server.ts:287-313 (registration)MCP 'tools/list' handler that registers the 'markdown2pdf' tool with its name, description, and input schema.if (req.method === 'tools/list') { process.stdout.write(JSON.stringify(createResponse(req.id, { tools: [{ name: 'markdown2pdf', description: 'Convert markdown to PDF, and pay with Lightning', inputSchema: { type: 'object', required: ['text_body', 'title'], properties: { text_body: { type: 'string', description: 'Markdown text to convert' }, title: { type: 'string', description: 'Document title' }, date: { type: 'string', description: 'Document date (YYYY-MM-DD)' } } } }] })) + '\n'); continue; }
- src/markdown2pdf_mcp_server.ts:315-335 (registration)MCP 'tools/call' handler that validates the tool name 'markdown2pdf' and forwards the call to the core handleRequest function.if (req.method === 'tools/call') { if (!req.params?.name || req.params.name !== 'markdown2pdf') { process.stdout.write(JSON.stringify(createErrorResponse(req.id, { code: RPC_ERRORS.INVALID_PARAMS.code, message: "Invalid tool name" })) + '\n'); continue; } // Forward the parameters to our existing handler const toolRequest: McpRequest = { jsonrpc: "2.0", method: 'markdown2pdf', params: req.params.arguments, id: req.id }; const result = await handleRequest(toolRequest); process.stdout.write(JSON.stringify(result) + '\n'); continue; }
- src/markdown2pdf_mcp_server.ts:252-255 (registration)Server information in 'initialize' response identifies the server as 'markdown2pdf'.name: "markdown2pdf", version: "0.1.0" } });