Skip to main content
Glama

MJML MCP Server

by shaunie2fly
index.js30.1 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { readFile, writeFile, access } from 'fs/promises'; import { join, resolve } from 'path'; import mjml from 'mjml'; import { z } from 'zod'; // Logging utility const log = { info: (message, data = null) => { console.error(`[INFO] ${new Date().toISOString()} - ${message}`, data || ''); }, error: (message, error = null) => { console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, error || ''); }, warn: (message, data = null) => { console.error(`[WARN] ${new Date().toISOString()} - ${message}`, data || ''); }, debug: (message, data = null) => { if (process.env.DEBUG) { console.error(`[DEBUG] ${new Date().toISOString()} - ${message}`, data || ''); } } }; // Validation schemas const CompileMjmlSchema = z.object({ input: z.string().describe('MJML content or file path'), beautify: z.boolean().optional().default(true).describe('Beautify the output HTML'), minify: z.boolean().optional().default(false).describe('Minify the output HTML'), validationLevel: z.enum(['skip', 'soft', 'strict']).optional().default('soft').describe('Validation level'), filePath: z.boolean().optional().default(false).describe('Treat input as file path'), keepComments: z.boolean().optional().default(false).describe('Keep comments in output'), fonts: z.record(z.string()).optional().describe('Custom fonts configuration'), outputPath: z.string().optional().describe('Save compiled HTML to file path'), }); const ValidateMjmlSchema = z.object({ input: z.string().describe('MJML content or file path'), filePath: z.boolean().optional().default(false).describe('Treat input as file path'), validationLevel: z.enum(['skip', 'soft', 'strict']).optional().default('strict').describe('Validation level'), }); const GenerateTemplateSchema = z.object({ template: z.enum([ 'newsletter', 'welcome', 'promotional', 'transactional', 'password-reset', 'verification', 'announcement', 'invitation' ]).describe('Template type'), variables: z.record(z.string()).optional().describe('Template variables to replace'), customColors: z.record(z.string()).optional().describe('Custom color scheme'), customFonts: z.record(z.string()).optional().describe('Custom fonts'), outputPath: z.string().optional().describe('Save template to file path'), }); const GetComponentInfoSchema = z.object({ component: z.string().optional().describe('Specific component name (optional)'), category: z.enum(['all', 'standard', 'advanced', 'structural']).optional().default('all').describe('Component category'), }); class MjmlMcpServer { constructor() { this.server = new Server( { name: 'mjml-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => { log.error('[MCP Error]', error); }; process.on('SIGINT', async () => { log.info('Shutting down MJML MCP server...'); await this.server.close(); process.exit(0); }); } async readInput(input, isFilePath) { if (isFilePath) { try { const fullPath = resolve(input); await access(fullPath); const content = await readFile(fullPath, 'utf-8'); log.debug(`Read file: ${fullPath}`); return content; } catch (error) { throw new McpError( ErrorCode.InvalidRequest, `Failed to read MJML file: ${error.message}` ); } } return input; } async saveOutput(content, outputPath) { if (outputPath) { try { const fullPath = resolve(outputPath); await writeFile(fullPath, content, 'utf-8'); log.info(`Output saved to: ${fullPath}`); return { saved: true, path: fullPath }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to save output: ${error.message}` ); } } return { saved: false }; } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'compile_mjml', description: 'Compile MJML to HTML with various options', inputSchema: { type: 'object', properties: { input: { type: 'string', description: 'MJML content or file path', }, beautify: { type: 'boolean', description: 'Beautify the output HTML', default: true, }, minify: { type: 'boolean', description: 'Minify the output HTML', default: false, }, validationLevel: { type: 'string', enum: ['skip', 'soft', 'strict'], description: 'Validation level', default: 'soft', }, filePath: { type: 'boolean', description: 'Treat input as file path', default: false, }, keepComments: { type: 'boolean', description: 'Keep comments in output', default: false, }, fonts: { type: 'object', description: 'Custom fonts configuration', }, outputPath: { type: 'string', description: 'Save compiled HTML to file path', }, }, required: ['input'], }, }, { name: 'validate_mjml', description: 'Validate MJML syntax and structure', inputSchema: { type: 'object', properties: { input: { type: 'string', description: 'MJML content or file path', }, filePath: { type: 'boolean', description: 'Treat input as file path', default: false, }, validationLevel: { type: 'string', enum: ['skip', 'soft', 'strict'], description: 'Validation level', default: 'strict', }, }, required: ['input'], }, }, { name: 'generate_template', description: 'Generate pre-built email templates', inputSchema: { type: 'object', properties: { template: { type: 'string', enum: ['newsletter', 'welcome', 'promotional', 'transactional', 'password-reset', 'verification', 'announcement', 'invitation'], description: 'Template type', }, variables: { type: 'object', description: 'Template variables to replace', }, customColors: { type: 'object', description: 'Custom color scheme', }, customFonts: { type: 'object', description: 'Custom fonts', }, outputPath: { type: 'string', description: 'Save template to file path', }, }, required: ['template'], }, }, { name: 'get_component_info', description: 'Get MJML component reference and documentation', inputSchema: { type: 'object', properties: { component: { type: 'string', description: 'Specific component name (optional)', }, category: { type: 'string', enum: ['all', 'standard', 'advanced', 'structural'], description: 'Component category', default: 'all', }, }, }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request; try { switch (name) { case 'compile_mjml': return await this.handleCompileMjml(args); case 'validate_mjml': return await this.handleValidateMjml(args); case 'generate_template': return await this.handleGenerateTemplate(args); case 'get_component_info': return await this.handleGetComponentInfo(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { log.error(`Tool execution error: ${name}`, error); if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error.message}` ); } }); } async handleCompileMjml(args) { const parsed = CompileMjmlSchema.parse(args); log.info('Compiling MJML', { template: parsed.template }); const mjmlContent = await this.readInput(parsed.input, parsed.filePath); const mjmlOptions = { beautify: parsed.beautify, minify: parsed.minify, validationLevel: parsed.validationLevel, keepComments: parsed.keepComments, fonts: parsed.fonts || {}, }; const result = mjml(mjmlContent, mjmlOptions); if (result.errors.length > 0) { log.warn('MJML compilation completed with errors', result.errors); } else { log.info('MJML compilation successful'); } const saveResult = await this.saveOutput(result.html, parsed.outputPath); return { content: [ { type: 'text', text: JSON.stringify({ success: true, html: result.html, errors: result.errors, warnings: result.warnings, mjmlOptions, saved: saveResult, }, null, 2), }, ], }; } async handleValidateMjml(args) { const parsed = ValidateMjmlSchema.parse(args); log.info('Validating MJML'); const mjmlContent = await this.readInput(parsed.input, parsed.filePath); const mjmlOptions = { validationLevel: parsed.validationLevel, }; const result = mjml(mjmlContent, mjmlOptions); const isValid = result.errors.length === 0; log.info(`MJML validation ${isValid ? 'passed' : 'failed'}`); return { content: [ { type: 'text', text: JSON.stringify({ valid: isValid, errors: result.errors, warnings: result.warnings, validationLevel: parsed.validationLevel, }, null, 2), }, ], }; } async handleGenerateTemplate(args) { const parsed = GenerateTemplateSchema.parse(args); log.info(`Generating template: ${parsed.template}`); const template = this.getTemplate(parsed.template, parsed.variables, parsed.customColors, parsed.customFonts); const saveResult = await this.saveOutput(template, parsed.outputPath); return { content: [ { type: 'text', text: JSON.stringify({ success: true, template: parsed.template, mjml: template, saved: saveResult, }, null, 2), }, ], }; } async handleGetComponentInfo(args) { const parsed = GetComponentInfoSchema.parse(args); log.info(`Getting component info: ${parsed.component || 'all'} (${parsed.category})`); const info = this.getComponentInfo(parsed.component, parsed.category); return { content: [ { type: 'text', text: JSON.stringify(info, null, 2), }, ], }; } getTemplate(type, variables = {}, customColors = {}, customFonts = {}) { const defaultColors = { primary: '#007bff', secondary: '#6c757d', success: '#28a745', danger: '#dc3545', warning: '#ffc107', info: '#17a2b8', light: '#f8f9fa', dark: '#343a40', }; const colors = { ...defaultColors, ...customColors }; const defaultFonts = { 'Lato': 'https://fonts.googleapis.com/css?family=Lato:300,400,700,900', 'Open Sans': 'https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,800', }; const fonts = { ...defaultFonts, ...customFonts }; const defaultVars = { company_name: 'Your Company', company_logo: 'https://via.placeholder.com/150x50', unsubscribe_url: '#', website_url: '#', support_email: 'support@example.com', current_year: new Date().getFullYear(), ...variables, }; const templates = { newsletter: `<mjml> <mj-head> <mj-attributes> <mj-all font-family="Lato, sans-serif" /> <mj-section background-color="#ffffff" /> <mj-column padding="0" /> <mj-text font-size="14px" line-height="22px" color="#4a5568" /> <mj-button background-color="${colors.primary}" color="#ffffff" border-radius="4px" /> </mj-attributes> <mj-font name="Lato" href="${fonts['Lato']}" /> <mj-title>{{company_name}} Newsletter</mj-title> <mj-preview>Our latest updates and news</mj-preview> </mj-head> <mj-body background-color="#f7fafc"> <mj-section background-color="${colors.primary}" padding="40px 0"> <mj-column> <mj-text align="center" color="#ffffff" font-size="24px" font-weight="bold"> {{company_name}} Newsletter </mj-text> <mj-text align="center" color="#ffffff" font-size="16px" padding-top="10px"> {{newsletter_date}} </mj-text> </mj-column> </mj-section> <mj-section padding="40px 30px"> <mj-column> <mj-text font-size="28px" font-weight="bold" color="#2d3748" align="center"> {{main_title}} </mj-text> <mj-text align="center" padding-top="10px"> {{main_subtitle}} </mj-text> </mj-column> </mj-section> <mj-section padding="0 30px 40px"> <mj-column> <mj-image src="{{featured_image}}" width="100%" border-radius="8px" /> <mj-text padding-top="20px" font-size="18px" font-weight="600"> {{featured_title}} </mj-text> <mj-text padding-top="10px"> {{featured_content}} </mj-text> <mj-button href="{{featured_link}}" padding-top="20px"> Read More </mj-button> </mj-column> </mj-section> <mj-section background-color="#edf2f7" padding="40px 30px"> <mj-column> <mj-text font-size="20px" font-weight="bold" color="#2d3748" align="center"> More Updates </mj-text> </mj-column> </mj-section> <mj-section background-color="#edf2f7" padding="0 30px 40px"> <mj-column> <mj-text font-size="16px" font-weight="600"> {{update1_title}} </mj-text> <mj-text padding-top="5px" font-size="14px"> {{update1_content}} </mj-text> <mj-divider border-color="#cbd5e0" border-width="1px" padding="20px 0" /> <mj-text font-size="16px" font-weight="600"> {{update2_title}} </mj-text> <mj-text padding-top="5px" font-size="14px"> {{update2_content}} </mj-text> </mj-column> </mj-section> <mj-section background-color="${colors.dark}" padding="40px 30px"> <mj-column> <mj-social-element-mode> <mj-social facebook-url="{{facebook_url}}" twitter-url="{{twitter_url}}" instagram-url="{{instagram_url}}" /> </mj-social-element-mode> <mj-text align="center" color="#ffffff" font-size="12px" padding-top="20px"> © {{current_year}} {{company_name}}. All rights reserved. </mj-text> <mj-text align="center" color="#ffffff" font-size="12px" padding-top="10px"> <a href="{{unsubscribe_url}}" style="color: #ffffff; text-decoration: underline;">Unsubscribe</a> </mj-text> </mj-column> </mj-section> </mj-body> </mjml>`, welcome: `<mjml> <mj-head> <mj-attributes> <mj-all font-family="Open Sans, sans-serif" /> <mj-section background-color="#ffffff" /> <mj-column padding="0" /> <mj-text font-size="14px" line-height="22px" color="#4a5568" /> <mj-button background-color="${colors.success}" color="#ffffff" border-radius="6px" font-weight="600" /> </mj-attributes> <mj-font name="Open Sans" href="${fonts['Open Sans']}" /> <mj-title>Welcome to {{company_name}}!</mj-title> </mj-head> <mj-body background-color="#f7fafc"> <mj-section background-color="${colors.success}" padding="60px 0"> <mj-column> <mj-text align="center" color="#ffffff" font-size="36px" font-weight="bold"> Welcome! </mj-text> <mj-text align="center" color="#ffffff" font-size="18px" padding-top="10px"> We're excited to have you on board </mj-text> </mj-column> </mj-section> <mj-section padding="50px 30px"> <mj-column> <mj-text align="center" font-size="24px" font-weight="600" color="#2d3748"> Hi {{user_name}}, </mj-text> <mj-text align="center" padding-top="20px" font-size="16px"> Thank you for joining {{company_name}}! We're thrilled to have you as part of our community. </mj-text> <mj-text align="center" padding-top="15px" font-size="16px"> Your account has been successfully created and you're ready to start exploring all the amazing features we have to offer. </mj-text> </mj-column> </mj-section> <mj-section background-color="#f7fafc" padding="40px 30px"> <mj-column> <mj-text align="center" font-size="20px" font-weight="600" color="#2d3748"> Next Steps </mj-text> <mj-text align="center" padding-top="20px"> Here are a few things you can do to get started: </mj-text> </mj-column> </mj-section> <mj-section padding="0 30px 40px"> <mj-column width="50%"> <mj-text align="center" font-size="16px" font-weight="600" padding-bottom="10px"> ✓ Complete Your Profile </mj-text> <mj-text align="center" font-size="14px"> Add your personal information and preferences </mj-text> </mj-column> <mj-column width="50%"> <mj-text align="center" font-size="16px" font-weight="600" padding-bottom="10px"> ✓ Explore Features </mj-text> <mj-text align="center" font-size="14px"> Discover everything {{company_name}} can do </mj-text> </mj-column> </mj-section> <mj-section padding="0 30px 40px"> <mj-column> <mj-button href="{{dashboard_url}}" font-size="16px" padding="15px 30px"> Go to Dashboard </mj-button> </mj-column> </mj-section> <mj-section background-color="#edf2f7" padding="30px"> <mj-column> <mj-text align="center" font-size="14px" color="#718096"> If you have any questions, don't hesitate to contact our support team at <a href="mailto:{{support_email}}" style="color: ${colors.primary};">{{support_email}}</a> </mj-text> </mj-column> </mj-section> <mj-section background-color="${colors.dark}" padding="30px"> <mj-column> <mj-text align="center" color="#ffffff" font-size="12px"> © {{current_year}} {{company_name}}. All rights reserved. </mj-text> </mj-column> </mj-section> </mj-body> </mjml>`, promotional: `<mjml> <mj-head> <mj-attributes> <mj-all font-family="Lato, sans-serif" /> <mj-section background-color="#ffffff" /> <mj-column padding="0" /> <mj-text font-size="14px" line-height="22px" color="#4a5568" /> <mj-button background-color="${colors.danger}" color="#ffffff" border-radius="4px" font-weight="bold" /> </mj-attributes> <mj-font name="Lato" href="${fonts['Lato']}" /> <mj-title>🔥 Limited Time Offer from {{company_name}}</mj-title> <mj-preview>Don't miss out on these amazing deals!</mj-preview> </mj-head> <mj-body background-color="#000000"> <mj-section background-color="${colors.danger}" padding="20px 0"> <mj-column> <mj-text align="center" color="#ffffff" font-size="24px" font-weight="bold"> 🔥 FLASH SALE </mj-text> <mj-text align="center" color="#ffffff" font-size="18px" padding-top="5px"> {{discount_percentage}} OFF EVERYTHING </mj-text> <mj-text align="center" color="#ffffff" font-size="16px" padding-top="5px"> Ends in {{time_remaining}} </mj-text> </mj-column> </mj-section> <mj-section background-color="#ffffff" padding="40px 30px"> <mj-column> <mj-text align="center" font-size="28px" font-weight="bold" color="#2d3748"> {{promotion_title}} </mj-text> <mj-text align="center" padding-top="15px" font-size="16px"> {{promotion_description}} </mj-text> </mj-column> </mj-section> <mj-section background-color="#fff3cd" padding="20px 30px"> <mj-column> <mj-text align="center" font-size="18px" font-weight="600" color="#856404"> ⏰ Hurry! Limited Time Offer </mj-text> </mj-column> </mj-section> <mj-section padding="30px"> <mj-column> <mj-image src="{{product_image}}" width="100%" /> <mj-text align="center" font-size="20px" font-weight="bold" padding-top="20px"> {{product_name}} </mj-text> <mj-text align="center" padding-top="10px"> {{product_description}} </mj-text> <mj-text align="center" padding-top="10px"> <span style="text-decoration: line-through; color: #718096;">${{original_price}}</span> <span style="font-size: 24px; font-weight: bold; color: ${colors.danger};">${{sale_price}}</span> </mj-text> <mj-button href="{{shop_url}}" background-color="${colors.danger}" padding-top="20px"> Shop Now - {{discount_percentage}} OFF </mj-button> </mj-column> </mj-section> <mj-section background-color="#f8f9fa" padding="40px 30px"> <mj-column> <mj-text align="center" font-size="18px" font-weight="600" color="#2d3748"> Why Choose {{company_name}}? </mj-text> </mj-column> </mj-section> <mj-section background-color="#f8f9fa" padding="0 30px 40px"> <mj-column width="33%"> <mj-text align="center" font-size="16px" font-weight="600"> ✓ Free Shipping </mj-text> </mj-column> <mj-column width="33%"> <mj-text align="center" font-size="16px" font-weight="600"> ✓ 30-Day Returns </mj-text> </mj-column> <mj-column width="34%"> <mj-text align="center" font-size="16px" font-weight="600"> ✓ 24/7 Support </mj-text> </mj-column> </mj-section> <mj-section background-color="#343a40" padding="30px"> <mj-column> <mj-text align="center" color="#ffffff" font-size="12px"> © {{current_year}} {{company_name}}. All rights reserved. </mj-text> <mj-text align="center" color="#ffffff" font-size="12px" padding-top="10px"> <a href="{{unsubscribe_url}}" style="color: #ffffff; text-decoration: underline;">Unsubscribe</a> </mj-text> </mj-column> </mj-section> </mj-body> </mjml>`, }; let template = templates[type] || templates.newsletter; // Replace variables Object.entries(defaultVars).forEach(([key, value]) => { const regex = new RegExp(`{{${key}}}`, 'g'); template = template.replace(regex, value); }); return template; } getComponentInfo(component = null, category = 'all') { const components = { standard: { 'mj-text': { description: 'Text component for displaying text content', attributes: { 'font-family': 'Font family', 'font-size': 'Font size', 'color': 'Text color', 'align': 'Text alignment (left, center, right)', 'padding': 'Padding around the text', }, example: '<mj-text>Hello World!</mj-text>', }, 'mj-button': { description: 'Button component for calls to action', attributes: { 'background-color': 'Button background color', 'color': 'Text color', 'href': 'Link URL', 'border-radius': 'Button border radius', 'font-size': 'Font size', }, example: '<mj-button href="#">Click me</mj-button>', }, 'mj-image': { description: 'Image component for displaying images', attributes: { 'src': 'Image URL', 'alt': 'Alt text', 'width': 'Image width', 'height': 'Image height', 'border-radius': 'Image border radius', }, example: '<mj-image src="https://example.com/image.jpg" />', }, 'mj-divider': { description: 'Divider component for creating horizontal lines', attributes: { 'border-width': 'Border width', 'border-color': 'Border color', 'padding': 'Padding around the divider', }, example: '<mj-divider border-color="#cccccc" />', }, 'mj-spacer': { description: 'Spacer component for adding vertical space', attributes: { 'height': 'Spacer height', }, example: '<mj-spacer height="20px" />', }, }, structural: { 'mj-section': { description: 'Section component for creating rows', attributes: { 'background-color': 'Section background color', 'padding': 'Section padding', 'full-width': 'Make section full width', 'direction': 'Layout direction (ltr, rtl)', }, example: '<mj-section background-color="#ffffff"><mj-column></mj-column></mj-section>', }, 'mj-column': { description: 'Column component for creating columns within sections', attributes: { 'width': 'Column width', 'background-color': 'Column background color', 'padding': 'Column padding', 'border': 'Column border', }, example: '<mj-column width="50%"><mj-text>Hello</mj-text></mj-column>', }, 'mj-wrapper': { description: 'Wrapper component for grouping sections', attributes: { 'background-color': 'Wrapper background color', 'padding': 'Wrapper padding', 'full-width': 'Make wrapper full width', }, example: '<mj-wrapper><mj-section></mj-section></mj-wrapper>', }, }, advanced: { 'mj-navbar': { description: 'Navigation bar component', attributes: { 'background-color': 'Navbar background color', 'align': 'Navbar alignment', 'padding': 'Navbar padding', }, example: '<mj-navbar><mj-navbar-link href="#">Home</mj-navbar-link></mj-navbar>', }, 'mj-social': { description: 'Social media icons component', attributes: { 'align': 'Social icons alignment', 'padding': 'Social icons padding', 'icon-size': 'Icon size', }, example: '<mj-social><mj-social-element name="facebook" href="#" /></mj-social>', }, 'mj-table': { description: 'Table component for displaying tabular data', attributes: { 'cellspacing': 'Cell spacing', 'cellpadding': 'Cell padding', 'width': 'Table width', 'align': 'Table alignment', }, example: '<mj-table><tr><td>Cell 1</td><td>Cell 2</td></tr></mj-table>', }, 'mj-carousel': { description: 'Image carousel component', attributes: { 'align': 'Carousel alignment', 'border-radius': 'Carousel border radius', 'icon-width': 'Navigation icon width', }, example: '<mj-carousel><mj-carousel-image src="image1.jpg" /></mj-carousel>', }, }, }; if (component) { for (const [cat, comps] of Object.entries(components)) { if (comps[component]) { return { component, category: cat, ...comps[component], }; } } return { error: `Component '${component}' not found` }; } if (category !== 'all') { return components[category] || {}; } return components; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); log.info('MJML MCP server started successfully'); } } const server = new MjmlMcpServer(); server.run().catch(console.error);

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/shaunie2fly/mjml_mcp'

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