Skip to main content
Glama
markdown2pdf_mcp_server.ts10.3 kB
#!/usr/bin/env node import fetch from 'node-fetch'; // JSON-RPC 2.0 error codes const RPC_ERRORS = { PARSE_ERROR: { code: -32700, message: "Parse error" }, INVALID_REQUEST: { code: -32600, message: "Invalid Request" }, METHOD_NOT_FOUND: { code: -32601, message: "Method not found" }, INVALID_PARAMS: { code: -32602, message: "Invalid params" }, INTERNAL_ERROR: { code: -32603, message: "Internal error" }, } as const; // JSON-RPC 2.0 interfaces interface McpRequest { jsonrpc: "2.0"; method: string; params?: any; id: string | number | null; } interface McpError { code: number; message: string; data?: any; } interface McpResponse { jsonrpc: "2.0"; result?: any; error?: McpError; id: string | number | null; } // Helper to create JSON-RPC 2.0 responses function createResponse(id: string | number | null, result?: any): McpResponse { return { jsonrpc: "2.0", result, id }; } function createErrorResponse(id: string | number | null, error: McpError): McpResponse { return { jsonrpc: "2.0", error, id }; } // Add debug logging function debug(msg: string) { console.error(`[DEBUG] ${msg}`); } 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)}` }); } } // MCP server main loop process.stdin.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', async (chunk) => { buffer += chunk; let boundary; while ((boundary = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 1); if (!line.trim()) continue; let req: McpRequest; try { req = JSON.parse(line); } catch (e) { process.stdout.write(JSON.stringify(createErrorResponse(null, RPC_ERRORS.PARSE_ERROR)) + '\n'); continue; } // Skip notifications/initialized messages - no response needed if (req.method === 'notifications/initialized') { continue; } // Validate JSON-RPC 2.0 if (req.jsonrpc !== "2.0") { process.stdout.write(JSON.stringify(createErrorResponse(null, RPC_ERRORS.INVALID_REQUEST)) + '\n'); continue; } if (!req.method || typeof req.method !== 'string') { process.stdout.write(JSON.stringify(createErrorResponse(req.id, RPC_ERRORS.INVALID_REQUEST)) + '\n'); continue; } // Protocol methods if (req.method === 'initialize') { debug(`Received initialize request: ${JSON.stringify(req)}`); const response = createResponse(req.id, { protocolVersion: "2024-11-05", capabilities: { tools: { listChanged: true }, prompts: { listChanged: true }, resources: {} }, serverInfo: { name: "markdown2pdf", version: "0.1.0" } }); debug(`Sending initialize response: ${JSON.stringify(response)}`); process.stdout.write(JSON.stringify(response) + '\n'); continue; } if (req.method === 'resources/list') { process.stdout.write(JSON.stringify(createResponse(req.id, { resources: [] })) + '\n'); continue; } if (req.method === 'prompts/list') { process.stdout.write(JSON.stringify(createResponse(req.id, { prompts: [{ name: 'convert_markdown', description: 'Convert markdown to PDF' }] })) + '\n'); continue; } if (req.method === 'prompts/get'){ if (!req.params?.name || req.params.name !== 'convert_markdown') { process.stdout.write(JSON.stringify(createErrorResponse(req.id, { code: RPC_ERRORS.INVALID_PARAMS.code, message: "Invalid prompt name" })) + '\n'); continue; } } 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; } 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; } if (req.method === 'ping') { process.stdout.write(JSON.stringify(createResponse(req.id, null)) + '\n'); continue; } // Handle markdown2pdf or report method not found const result = await handleRequest(req); process.stdout.write(JSON.stringify(result) + '\n'); } });

Implementation Reference

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/Serendipity-AI/markdown2pdf-mcp'

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