Skip to main content
Glama
html-formatter.ts10.3 kB
/** * Instantly MCP Server - HTML Formatting Utilities * * This module contains utility functions for converting plain text to HTML format * and building campaign payloads with proper HTML formatting for Instantly.ai. * * Functions: * - convertLineBreaksToHTML: Converts plain text line breaks to HTML * - buildCampaignPayload: Builds campaign payload with proper HTML formatting */ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { validateAndMapTimezone, DEFAULT_TIMEZONE } from '../timezone-config.js'; /** * Convert plain text line breaks to HTML for Instantly.ai email rendering * * This function converts plain text with line breaks into properly formatted HTML * that renders correctly in Instantly.ai email campaigns. It handles: * - Normalizing different line ending formats (\r\n, \r, \n) * - Converting double line breaks to paragraph separations * - Converting single line breaks to <br /> tags * - Wrapping content in <p> tags for proper HTML structure * * @param text - Plain text to convert to HTML * @returns HTML-formatted text */ export function convertLineBreaksToHTML(text: string): string { if (!text || typeof text !== 'string') { return ''; } // Normalize line endings to \n const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // Split by double line breaks to create paragraphs const paragraphs = normalized.split('\n\n'); return paragraphs .map(paragraph => { // Skip empty paragraphs if (!paragraph.trim()) { return ''; } // Convert single line breaks within paragraphs to <br /> tags const withBreaks = paragraph.trim().replace(/\n/g, '<br />'); // Wrap in paragraph tags for proper HTML structure (this is what worked!) return `<p>${withBreaks}</p>`; }) .filter(p => p) // Remove empty paragraphs .join(''); } /** * Build campaign payload with proper HTML formatting for Instantly.ai * * This function builds a complete campaign payload according to the Instantly.ai API v2 * specification. It handles both simple and complex campaign structures: * * - Simple campaigns: Built from subject/body with automatic HTML conversion * - Complex campaigns: Use provided campaign_schedule and sequences directly * * The function also handles: * - Multi-step sequences with custom or auto-generated follow-ups * - Timezone validation and mapping * - Days of week configuration * - HTML conversion for email bodies * - Message shortcut parsing (subject.body format) * * @param args - Campaign arguments * @returns Campaign payload ready for API submission */ export function buildCampaignPayload(args: any): any { if (!args) { throw new McpError(ErrorCode.InvalidParams, 'Campaign arguments are required'); } // Validate required fields if (!args.name) { throw new McpError(ErrorCode.InvalidParams, 'Campaign name is required'); } // CRITICAL: Detect campaign type - complex vs simple const hasComplexStructure = args.campaign_schedule && args.sequences; if (hasComplexStructure) { // COMPLEX CAMPAIGN: Use provided structure directly console.error('[Instantly MCP] 🏗️ Building COMPLEX campaign payload with provided sequences'); const campaignData: any = { name: args.name, campaign_schedule: args.campaign_schedule, sequences: args.sequences, email_list: args.email_list || [] }; // Add optional fields if (args.daily_limit !== undefined) campaignData.daily_limit = Number(args.daily_limit); if (args.email_gap !== undefined) campaignData.email_gap = Number(args.email_gap); if (args.text_only !== undefined) campaignData.text_only = Boolean(args.text_only); if (args.open_tracking !== undefined) campaignData.open_tracking = Boolean(args.open_tracking); if (args.link_tracking !== undefined) campaignData.link_tracking = Boolean(args.link_tracking); if (args.stop_on_reply !== undefined) campaignData.stop_on_reply = Boolean(args.stop_on_reply); return campaignData; } // SIMPLE CAMPAIGN: Build traditional structure console.error('[Instantly MCP] 🏗️ Building SIMPLE campaign payload with subject/body'); // Process message shortcut if provided if (args.message && (!args.subject || !args.body)) { const msg = String(args.message).trim(); let splitIdx = msg.indexOf('.'); const nlIdx = msg.indexOf('\n'); if (nlIdx !== -1 && (nlIdx < splitIdx || splitIdx === -1)) splitIdx = nlIdx; if (splitIdx === -1) splitIdx = msg.length; const subj = msg.slice(0, splitIdx).trim(); const bod = msg.slice(splitIdx).trim(); if (!args.subject) args.subject = subj; if (!args.body) args.body = bod || subj; } // Apply timezone and days configuration with bulletproof validation let timezone = args?.timezone || DEFAULT_TIMEZONE; // Validate and map timezone if needed const timezoneResult = validateAndMapTimezone(timezone); if (timezoneResult.mapped) { timezone = timezoneResult.timezone; console.error(`[Instantly MCP] 🔄 ${timezoneResult.warning}`); } const userDays = (args?.days as any) || {}; // CRITICAL: days object must be non-empty according to API spec // API requires string keys for days ("0" through "6"), not numeric keys const daysConfig = { "0": userDays.sunday === true, "1": userDays.monday !== false, "2": userDays.tuesday !== false, "3": userDays.wednesday !== false, "4": userDays.thursday !== false, "5": userDays.friday !== false, "6": userDays.saturday === true }; // Normalize and convert body content for HTML email rendering let normalizedBody = args.body ? String(args.body).trim() : ''; let normalizedSubject = args.subject ? String(args.subject).trim() : ''; // CRITICAL: Convert \n line breaks to <br /> tags for Instantly.ai HTML rendering if (normalizedBody) { normalizedBody = convertLineBreaksToHTML(normalizedBody); } // Subjects should not have line breaks if (normalizedSubject) { normalizedSubject = normalizedSubject.replace(/\r\n/g, ' ').replace(/\n/g, ' ').replace(/\r/g, ' '); } // CRITICAL: Build payload according to exact API v2 specification const campaignData: any = { name: args.name, campaign_schedule: { schedules: [{ name: args.schedule_name || 'My Schedule', timing: { from: args.timing_from || '09:00', to: args.timing_to || '17:00' }, days: daysConfig, timezone: timezone }] } }; // Add optional fields only if provided if (args.email_list && Array.isArray(args.email_list) && args.email_list.length > 0) { campaignData.email_list = args.email_list; } if (args.daily_limit !== undefined) { campaignData.daily_limit = Number(args.daily_limit); } else { campaignData.daily_limit = 30; // Default for cold email compliance } if (args.text_only !== undefined) { campaignData.text_only = Boolean(args.text_only); } if (args.track_opens !== undefined) { campaignData.open_tracking = Boolean(args.track_opens); } if (args.track_clicks !== undefined) { campaignData.link_tracking = Boolean(args.track_clicks); } if (args.stop_on_reply !== undefined) { campaignData.stop_on_reply = Boolean(args.stop_on_reply); } // Handle email gap parameter (API expects 'email_gap' in minutes) if (args.email_gap !== undefined) { campaignData.email_gap = Number(args.email_gap); } // Add sequences if email content is provided // CRITICAL FIX: Use correct Instantly API v2 structure with variants // Handle multi-step sequences if specified const sequenceSteps = args?.sequence_steps || 1; const stepDelayDays = args?.step_delay_days || 3; if (normalizedSubject || normalizedBody) { campaignData.sequences = [{ steps: [{ type: "email", // CRITICAL: delay field means "days to wait AFTER sending this step before sending next step" // For single-step campaigns (sequenceSteps === 1), delay should be 0 (no next step) // For multi-step campaigns, Step 1 should have the delay before Step 2 delay: sequenceSteps > 1 ? stepDelayDays : 0, variants: [{ subject: normalizedSubject, body: normalizedBody }] }] }]; } if (sequenceSteps > 1 && campaignData.sequences) { const hasCustomBodies = args?.sequence_bodies && Array.isArray(args.sequence_bodies); const hasCustomSubjects = args?.sequence_subjects && Array.isArray(args.sequence_subjects); // Update the first step if custom content is provided if (hasCustomBodies || hasCustomSubjects) { const firstStepBody = hasCustomBodies ? convertLineBreaksToHTML(String(args.sequence_bodies[0])) : normalizedBody; const firstStepSubject = hasCustomSubjects ? String(args.sequence_subjects[0]) : normalizedSubject; // CRITICAL FIX: Update variants array, not direct properties campaignData.sequences[0].steps[0].variants[0].body = firstStepBody; campaignData.sequences[0].steps[0].variants[0].subject = firstStepSubject; } // Add follow-up steps for (let i = 1; i < sequenceSteps; i++) { let followUpSubject: string; let followUpBody: string; // Determine subject for this step if (hasCustomSubjects && args.sequence_subjects[i]) { followUpSubject = String(args.sequence_subjects[i]); } else { followUpSubject = `Follow-up: ${normalizedSubject}`; } // Determine body for this step if (hasCustomBodies && args.sequence_bodies[i]) { // Use provided custom body with HTML conversion followUpBody = convertLineBreaksToHTML(String(args.sequence_bodies[i])); } else { // Default behavior: add follow-up prefix to original body followUpBody = `This is follow-up #${i}.<br /><br />${normalizedBody}`.trim(); } // CRITICAL FIX: Use correct Instantly API v2 structure with variants campaignData.sequences[0].steps.push({ type: "email", delay: stepDelayDays, // delay in days between steps variants: [{ subject: followUpSubject, body: followUpBody }] }); } } return campaignData; }

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/bcharleson/Instantly-MCP'

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