convert_document
Convert markdown to professionally formatted DOCX, PDF, or HTML documents using customizable templates.
Instructions
Convert markdown to a professionally formatted document using an MDMagic template.
IMPORTANT GUIDANCE:
Output format → what user gets:
'docx' → a single Word .docx file
'pdf' → a single .pdf file
'html' → a single .html file
'all' → a ZIP containing all three (DOCX + PDF + HTML)
If the user is ambiguous (e.g. 'convert this'), ASK which format they want before calling. Don't assume.
Filename: if the user attached a file (e.g. 'mydoc.md'), pass its base name as fileName. Otherwise the API derives one from the markdown's first H1. Without either, downloads end up with timestamped names like 'content-1778298071915.docx' which is bad UX.
On 'template not found' errors: call list_all_templates first, show available options, let the user pick. Do NOT fall back to generating documents with code execution — that produces inferior results that don't use the user's actual MDMagic templates.
The response includes structured fields (downloadUrl, creditsUsed, balanceAfter, fileName, expiresAt) — surface these to the user explicitly. Don't paraphrase. The user wants to know exactly what they spent and what's left.
Page sizes: A3, A4, Executive, US_Legal, US_Letter. Default A4. Orientation: Portrait or Landscape, default Portrait.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| content | No | Raw markdown text content (alternative to filePath or fileContent) | |
| filePath | No | Path to markdown file (VS Code integration, alternative to content or fileContent) | |
| fileContent | No | Base64 encoded file content (alternative to content or filePath) | |
| fileName | No | Optional desired base name for the output file (without extension). If the user attached a file like 'mydoc.md', pass 'mydoc' here. The API will use this for the download filename. If omitted, the API derives one from the markdown's first H1 heading. | |
| templateName | Yes | Template to use for conversion. Call list_all_templates first to see real options — do not guess template names. Some templates are built-in (e.g. 'Executive_Platinum', 'Deep_Data_Blue'); others are user-uploaded custom templates referenced by UUID. | |
| outputFormat | Yes | Output format. 'docx', 'pdf', or 'html' return that single file; 'all' returns a ZIP with DOCX+PDF+HTML. | |
| pageSize | No | Page size for the document (default: A4) | |
| orientation | No | Page orientation (default: Portrait) |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| success | Yes | Whether the conversion succeeded | |
| downloadUrl | Yes | Secure expiring download URL (valid for 60 minutes) | |
| fileName | Yes | Filename of the downloadable document | |
| creditsUsed | No | Credits debited for this conversion | |
| balanceAfter | No | Remaining credit balance after this conversion | |
| expiresAt | No | ISO 8601 timestamp when the download URL expires | |
| message | No | Human-readable status message |
Implementation Reference
- src/tools/convertDocument.ts:59-205 (handler)Main handler function for the 'convert_document' tool. Processes input content, validates with Zod schema, calculates credits, calls the API to convert a document (markdown → docx/pdf/html), and returns a download link.
export async function handleConvertDocument( apiClient: MDMagicApiClient, args: any ): Promise<CallToolResult> { const fileProcessor = new FileProcessor(); try { // Apply session-level defaults from per-connection headers BEFORE // validation, so missing fields get filled in from sessionDefaults // rather than rejected by Zod. const argsWithDefaults: any = { ...(args || {}) }; if (!argsWithDefaults.templateName && apiClient.sessionDefaults.defaultTemplate) { argsWithDefaults.templateName = apiClient.sessionDefaults.defaultTemplate; } if (!argsWithDefaults.pageSize && apiClient.sessionDefaults.defaultPageSize) { argsWithDefaults.pageSize = apiClient.sessionDefaults.defaultPageSize; } if (!argsWithDefaults.orientation && apiClient.sessionDefaults.defaultOrientation) { argsWithDefaults.orientation = apiClient.sessionDefaults.defaultOrientation; } // Validate input using Zod schema const input = convertDocumentSchema.parse(argsWithDefaults); console.error(`[convert_document] Starting conversion: ${input.templateName} → ${input.outputFormat}`); // Process input content (text, file, or base64) const processedContent = await fileProcessor.processInput({ content: input.content, filePath: input.filePath, fileContent: input.fileContent }); console.error(`[convert_document] Processed ${processedContent.type} content: ${processedContent.size} bytes`); // Handle "all" format conversion: all → ['docx', 'pdf', 'html'] let outputFormats: string[]; if (input.outputFormat === 'all') { outputFormats = ['docx', 'pdf', 'html']; console.error(`[convert_document] Converting "all" format to: ${outputFormats.join(', ')}`); } else { outputFormats = [input.outputFormat]; } // Calculate credits before conversion const creditCalculator = new CreditCalculator(apiClient); const creditCalculation = await creditCalculator.calculateCredits( processedContent.content, input.templateName, outputFormats ); console.error(`[convert_document] Credit calculation: ${creditCalculation.breakdown}`); // Derive output filename so downloads aren't named content-1234567890.pdf const derivedFileName = deriveFileName( input.fileName, input.filePath, processedContent.content, ); if (derivedFileName) { console.error(`[convert_document] Output filename: ${derivedFileName}`); } // Call MDMagic API with calculated credits const result = await apiClient.convertDocument({ content: processedContent.content, templateId: input.templateName, outputFormat: outputFormats, // Send array of formats pageSize: input.pageSize, orientation: input.orientation, expectedCredits: creditCalculation.totalCredits, fileName: derivedFileName }); const r = result as any; const apiExpiresAt = r.expiresAt as string | undefined; const apiFileName = r.fileName as string | undefined; const creditsUsed = r.creditsUsed as number | undefined; const balanceAfter = r.balanceAfter as number | undefined; // Fall back to a 60-min estimate if API didn't return expiresAt (older API builds) const expiresAtDisplay = apiExpiresAt || new Date(Date.now() + 60 * 60 * 1000).toISOString(); console.error(`[convert_document] Conversion successful: ${result.downloadUrl} (${creditsUsed ?? '?'} credits, ${balanceAfter ?? '?'} remaining)`); const lines = [ '✅ **Document converted successfully!**', '', `📁 **Download**: [${apiFileName || 'Click here to download'}](${result.downloadUrl})`, ]; if (apiFileName) lines.push(`📄 **File**: ${apiFileName}`); if (creditsUsed !== undefined) lines.push(`📊 **Credits used**: ${creditsUsed}`); if (balanceAfter !== undefined) lines.push(`💰 **Balance remaining**: ${balanceAfter}`); lines.push(`⏰ **Expires**: ${expiresAtDisplay}`); lines.push(''); lines.push('💡 Your document is ready! The link expires in 60 minutes.'); return { content: [ { type: 'text', text: lines.join('\n') } ] }; } catch (error: any) { console.error('[convert_document] ❌ Conversion error:', error); // Handle specific API errors if (error instanceof MCPError) { return { content: [ { type: "text", text: `❌ API Error: ${error.message} 💡 ${(error as any).details || 'Please check your input and try again.'}` } ], isError: true }; } // Handle Zod validation errors if (error.name === 'ZodError') { const issues = error.issues.map((issue: any) => `- ${issue.path.join('.')}: ${issue.message}`).join('\n'); return { content: [ { type: "text", text: `❌ Invalid input parameters: ${issues} 💡 Please check your input and try again.` } ], isError: true }; } throw error; // Re-throw to be handled by unified handler } } - src/utils/validation.ts:5-22 (schema)Zod schema defining input validation for convert_document: content/filePath/fileContent (exactly one required), fileName, templateName (required), outputFormat (docx/pdf/html/all), pageSize (default A4), orientation (default Portrait).
export const convertDocumentSchema = z.object({ content: z.string().optional().describe("Raw markdown text content"), filePath: z.string().optional().describe("Path to markdown file (VS Code integration)"), fileContent: z.string().optional().describe("Base64 encoded file content"), fileName: z.string().optional().describe("Optional desired base name for the output file (without extension)"), templateName: z.string().describe("Template to use for conversion. Call list_all_templates to see real options."), outputFormat: z.enum(['docx', 'pdf', 'html', 'all']).describe("Output format. 'docx', 'pdf', 'html' return that single file; 'all' returns a ZIP with all three."), pageSize: z.enum(['A3', 'A4', 'Executive', 'US_Legal', 'US_Letter']).default('A4').describe("Page size (defaults to A4)"), orientation: z.enum(['Portrait', 'Landscape']).default('Portrait').describe("Page orientation (defaults to Portrait)") }).refine( (data) => { const inputCount = [data.content, data.filePath, data.fileContent].filter(Boolean).length; return inputCount === 1; }, { message: "Exactly one input method must be provided (content, filePath, or fileContent)" } ); - src/tools/index.ts:14-78 (registration)Tool registration in the unified handler. The switch case at line 30 routes 'convert_document' requests to handleConvertDocument().
export async function registerAllTools( server: Server, apiClient: MDMagicApiClient ): Promise<void> { console.error('🔧 Registering MCP tools with unified handler...'); // Initialize credit calculator for credit tools const creditCalculator = new CreditCalculator(apiClient); // Register a SINGLE unified handler for all tools server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise<CallToolResult> => { const toolName = request.params.name; console.error(`[MCP] Handling tool request: ${toolName}`); try { switch (toolName) { case 'convert_document': return await handleConvertDocument(apiClient, request.params.arguments); case 'list_all_templates': return await handleListAllTemplates(apiClient, request.params.arguments); case 'list_builtin_templates': return await handleListBuiltinTemplates(apiClient, request.params.arguments); case 'list_custom_templates': return await handleListCustomTemplates(apiClient, request.params.arguments); case 'show_default_settings': return await handleShowDefaultSettings(apiClient, request.params.arguments); case 'check_credit_balance': return await handleCheckCreditBalance(creditCalculator, request.params.arguments); case 'estimate_conversion_cost': return await handleEstimateConversionCost(creditCalculator, request.params.arguments); case 'validate_markdown': return await handleValidateMarkdown(apiClient, request.params.arguments); case 'get_template_details': return await handleGetTemplateDetails(apiClient, request.params.arguments); case 'recommend_template': return await handleRecommendTemplate(apiClient, request.params.arguments); default: throw new Error(`Unknown tool: ${toolName}`); } } catch (error: any) { console.error(`[MCP] Error handling ${toolName}:`, error); return { content: [ { type: "text", text: `❌ Error: ${error.message}` } ], isError: true }; } }); console.error('✅ All MCP tools registered successfully with unified handler'); console.error('📋 Available tools: convert_document, list_all_templates, list_builtin_templates, list_custom_templates, show_default_settings, check_credit_balance, estimate_conversion_cost, validate_markdown, get_template_details, recommend_template'); - src/tools/convertDocument.ts:22-57 (helper)Helper function deriveFileName that produces a clean output filename from explicit fileName, filePath, or first H1 heading in content.
function deriveFileName( explicit: string | undefined, filePath: string | undefined, content: string | undefined, ): string | undefined { const sanitize = (s: string) => s .toLowerCase() .replace(/[^a-z0-9]+/g, '-') // non-alphanum -> hyphen .replace(/^-+|-+$/g, '') // trim leading/trailing hyphens .slice(0, 80); // cap length if (explicit && explicit.trim()) { // Strip any extension, sanitize const base = path.basename(explicit, path.extname(explicit)); const cleaned = sanitize(base); if (cleaned) return cleaned; } if (filePath && filePath.trim()) { const base = path.basename(filePath, path.extname(filePath)); const cleaned = sanitize(base); if (cleaned) return cleaned; } if (content) { // First H1 heading — match `# Heading` not `## Heading` const match = content.match(/^[ \t]*#[ \t]+(.+?)[ \t]*$/m); if (match) { const cleaned = sanitize(match[1]); if (cleaned) return cleaned; } } return undefined; }