Instantly MCP Server

by bcharleson
Verified
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; // Response interfaces interface InstantlyResponse { items?: any[]; next_starting_after?: string; } interface LeadResponse { id: string; email: string; first_name?: string; last_name?: string; company_name?: string; personalization?: string; website?: string; phone?: string; campaign?: string; list_id?: string; organization?: string; status?: number; email_open_count?: number; email_reply_count?: number; email_click_count?: number; company_domain?: string; payload?: any; timestamp_created?: string; timestamp_updated?: string; } interface CampaignResponse { id: string; name: string; status: number; description?: string; organization_id?: string; step_template_id?: string; timestamp_created?: string; timestamp_updated?: string; } interface WarmupAnalyticsResponse { email_date_data: { [email: string]: { [date: string]: { sent?: number; landed_inbox?: number; landed_spam?: number; received?: number; } } }; aggregate_data: { [email: string]: { sent?: number; landed_inbox?: number; landed_spam?: number; received?: number; health_score?: number; health_score_label?: string; } }; } interface AccountVitalsResponse { status?: string; success_list?: Array<{ email: string; status?: string; provider_code?: number; provider_name?: string; last_used?: string; warmup_status?: number; error?: string; details?: string; }>; failure_list?: Array<{ email: string; error?: string; details?: string; status?: string; provider_code?: number; provider_name?: string; }>; } interface CampaignAnalyticsResponse extends Array<{ campaign_name?: string; campaign_id: string; leads_count?: number; contacted_count?: number; open_count?: number; reply_count?: number; bounced_count?: number; unsubscribed_count?: number; completed_count?: number; emails_sent_count?: number; new_leads_contacted_count?: number; total_opportunities?: number; total_opportunity_value?: number; }> {} interface AccountResponse { email: string; timestamp_created: string; timestamp_updated: string; first_name: string; last_name: string; organization: string; warmup_status: number; provider_code: number; setup_pending: boolean; is_managed_account: boolean; tracking_domain_name?: string; daily_limit?: number; } function getApiKey(): string { return process.env.INSTANTLY_API_KEY as string; } const INSTANTLY_API_KEY = getApiKey(); const BASE_URL = "https://api.instantly.ai/api/v2"; // Tool definitions const CREATE_LEAD_TOOL: Tool = { name: "instantly_create_lead", description: "Create a new lead in Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the lead" }, first_name: { type: "string", description: "First name of the lead" }, last_name: { type: "string", description: "Last name of the lead" }, company_name: { type: "string", description: "Company name of the lead" }, campaign: { type: "string", description: "Campaign ID associated with the lead (UUID)" }, list_id: { type: "string", description: "List ID associated with the lead (UUID)" }, personalization: { type: "string", description: "Personalization of the lead" }, website: { type: "string", description: "Website of the lead" }, phone: { type: "string", description: "Phone number of the lead" }, custom_variables: { type: "object", description: "Custom variables for the lead" } }, required: ["email"] } }; const GET_LEAD_TOOL: Tool = { name: "instantly_get_lead", description: "Get details of a lead by ID", inputSchema: { type: "object", properties: { id: { type: "string", description: "Lead ID (UUID)" } }, required: ["id"] } }; const LIST_LEADS_TOOL: Tool = { name: "instantly_list_leads", description: "List leads with optional filters", inputSchema: { type: "object", properties: { campaign: { type: "string", description: "Filter leads by campaign ID (UUID)" }, list_id: { type: "string", description: "Filter leads by list ID (UUID)" }, limit: { type: "number", description: "Maximum number of leads to return (default 10, max 100)" }, starting_after: { type: "string", description: "ID of the last lead from the previous page for pagination" } } } }; const UPDATE_LEAD_TOOL: Tool = { name: "instantly_update_lead", description: "Update a lead's information", inputSchema: { type: "object", properties: { id: { type: "string", description: "Lead ID (UUID)" }, first_name: { type: "string", description: "First name of the lead" }, last_name: { type: "string", description: "Last name of the lead" }, company_name: { type: "string", description: "Company name of the lead" }, personalization: { type: "string", description: "Personalization of the lead" }, website: { type: "string", description: "Website of the lead" }, phone: { type: "string", description: "Phone number of the lead" }, custom_variables: { type: "object", description: "Custom variables for the lead" } }, required: ["id"] } }; const DELETE_LEAD_TOOL: Tool = { name: "instantly_delete_lead", description: "Delete a lead", inputSchema: { type: "object", properties: { id: { type: "string", description: "Lead ID (UUID)" } }, required: ["id"] } }; const LIST_CAMPAIGNS_TOOL: Tool = { name: "instantly_list_campaigns", description: "List campaigns with pagination support", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of campaigns to return (default 5, max 100)" }, starting_after: { type: "string", description: "ID of the last campaign from the previous page for pagination. Use the next_starting_after value from previous response to get the next page." }, status: { type: "number", description: "Filter campaigns by status (0: Draft, 1: Active, 2: Paused, 3: Completed, 4: Running Subsequences)" } } } }; const GET_CAMPAIGN_TOOL: Tool = { name: "instantly_get_campaign", description: "Get details of a campaign", inputSchema: { type: "object", properties: { id: { type: "string", description: "Campaign ID (UUID)" } }, required: ["id"] } }; const GET_WARMUP_ANALYTICS_TOOL: Tool = { name: "instantly_get_warmup_analytics", description: "Get warmup analytics for specified email accounts", inputSchema: { type: "object", properties: { emails: { type: "array", items: { type: "string" }, description: "List of emails to get warmup analytics for (up to 100). The emails should be attached to accounts in your workspace." } }, required: ["emails"] } }; const TEST_ACCOUNT_VITALS_TOOL: Tool = { name: "instantly_test_account_vitals", description: "Test the health and connectivity of email accounts in your Instantly workspace", inputSchema: { type: "object", properties: { accounts: { type: "array", items: { type: "string" }, description: "List of email accounts to test. You can provide multiple email addresses to test them all at once (up to 10 recommended)." } }, required: ["accounts"] } }; const GET_CAMPAIGN_ANALYTICS_TOOL: Tool = { name: "instantly_get_campaign_analytics", description: "Get analytics for a specific campaign or all campaigns for a date range", inputSchema: { type: "object", properties: { id: { type: "string", description: "Campaign ID (UUID). If not provided, analytics for all campaigns will be returned." }, start_date: { type: "string", description: "Start date for analytics (YYYY-MM-DD format)" }, end_date: { type: "string", description: "End date for analytics (YYYY-MM-DD format)" } }, required: ["start_date", "end_date"] } }; // Account tool definitions const CREATE_ACCOUNT_TOOL: Tool = { name: "instantly_create_account", description: "Create a new email account in Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the account" }, first_name: { type: "string", description: "First name associated with the account" }, last_name: { type: "string", description: "Last name associated with the account" }, provider_code: { type: "number", description: "Provider code (1: Custom IMAP/SMTP, 2: Google, 3: Microsoft, 4: AWS)" }, imap_username: { type: "string", description: "IMAP username" }, imap_password: { type: "string", description: "IMAP password" }, imap_host: { type: "string", description: "IMAP host (e.g. imap.gmail.com)" }, imap_port: { type: "number", description: "IMAP port (e.g. 993)" }, smtp_username: { type: "string", description: "SMTP username" }, smtp_password: { type: "string", description: "SMTP password" }, smtp_host: { type: "string", description: "SMTP host (e.g. smtp.gmail.com)" }, smtp_port: { type: "number", description: "SMTP port (e.g. 587)" }, daily_limit: { type: "number", description: "Daily email sending limit" }, tracking_domain_name: { type: "string", description: "Tracking domain name" } }, required: ["email", "first_name", "last_name", "provider_code", "imap_username", "imap_password", "imap_host", "imap_port", "smtp_username", "smtp_password", "smtp_host", "smtp_port"] } }; const LIST_ACCOUNTS_TOOL: Tool = { name: "instantly_list_accounts", description: "List email accounts in Instantly", inputSchema: { type: "object", properties: { limit: { type: "number", description: "The number of accounts to return (max 100)" }, starting_after: { type: "string", description: "The ID of the last item in the previous page - used for pagination" }, search: { type: "string", description: "Search term to filter accounts" }, status: { type: "number", description: "Status filter (1: Active, 2: Paused, -1: Connection Error, -2: Soft Bounce Error, -3: Sending Error)" }, provider_code: { type: "number", description: "Provider code filter (1: Custom IMAP/SMTP, 2: Google, 3: Microsoft, 4: AWS)" }, fetch_all: { type: "boolean", description: "Whether to automatically fetch all pages and provide a comprehensive summary. Use this when you need information about all accounts." } } } }; const GET_ACCOUNT_TOOL: Tool = { name: "instantly_get_account", description: "Get details of a specific email account in Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the account to retrieve" } }, required: ["email"] } }; const UPDATE_ACCOUNT_TOOL: Tool = { name: "instantly_update_account", description: "Update an existing email account in Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the account to update" }, first_name: { type: "string", description: "First name associated with the account" }, last_name: { type: "string", description: "Last name associated with the account" }, daily_limit: { type: "number", description: "Daily email sending limit" }, tracking_domain_name: { type: "string", description: "Tracking domain name" }, skip_cname_check: { type: "boolean", description: "Whether to skip CNAME check for tracking domain" }, remove_tracking_domain: { type: "boolean", description: "Whether to remove tracking domain from the account" } }, required: ["email"] } }; const DELETE_ACCOUNT_TOOL: Tool = { name: "instantly_delete_account", description: "Delete an email account from Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the account to delete" } }, required: ["email"] } }; const PAUSE_ACCOUNT_TOOL: Tool = { name: "instantly_pause_account", description: "Pause an email account in Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the account to pause" } }, required: ["email"] } }; const RESUME_ACCOUNT_TOOL: Tool = { name: "instantly_resume_account", description: "Resume a paused email account in Instantly", inputSchema: { type: "object", properties: { email: { type: "string", description: "Email address of the account to resume" } }, required: ["email"] } }; const INSTANTLY_TOOLS = [ CREATE_LEAD_TOOL, GET_LEAD_TOOL, LIST_LEADS_TOOL, UPDATE_LEAD_TOOL, DELETE_LEAD_TOOL, LIST_CAMPAIGNS_TOOL, GET_CAMPAIGN_TOOL, GET_WARMUP_ANALYTICS_TOOL, TEST_ACCOUNT_VITALS_TOOL, GET_CAMPAIGN_ANALYTICS_TOOL, CREATE_ACCOUNT_TOOL, LIST_ACCOUNTS_TOOL, GET_ACCOUNT_TOOL, UPDATE_ACCOUNT_TOOL, DELETE_ACCOUNT_TOOL, PAUSE_ACCOUNT_TOOL, RESUME_ACCOUNT_TOOL ] as const; // API handlers async function handleCreateLead({ email, first_name, last_name, company_name, campaign, list_id, personalization, website, phone, custom_variables }: { email: string; first_name?: string; last_name?: string; company_name?: string; campaign?: string; list_id?: string; personalization?: string; website?: string; phone?: string; custom_variables?: any; }) { const url = `${BASE_URL}/leads`; const body: any = { email }; if (first_name) body.first_name = first_name; if (last_name) body.last_name = last_name; if (company_name) body.company_name = company_name; if (campaign) body.campaign = campaign; if (list_id) body.list_id = list_id; if (personalization) body.personalization = personalization; if (website) body.website = website; if (phone) body.phone = phone; if (custom_variables) body.custom_variables = custom_variables; try { const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to create lead: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as LeadResponse; return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error creating lead: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleGetLead(id: string) { const url = `${BASE_URL}/leads/${id}`; try { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}` } }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to get lead: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as LeadResponse; return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error getting lead: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleListLeads({ campaign, list_id, limit = 10, starting_after }: { campaign?: string; list_id?: string; limit?: number; starting_after?: string; }) { const url = `${BASE_URL}/leads/list`; const body: any = {}; if (campaign) body.campaign = campaign; if (list_id) body.list_id = list_id; if (limit) body.limit = limit; if (starting_after) body.starting_after = starting_after; try { const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list leads: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as InstantlyResponse; return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error listing leads: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleUpdateLead({ id, first_name, last_name, company_name, personalization, website, phone, custom_variables }: { id: string; first_name?: string; last_name?: string; company_name?: string; personalization?: string; website?: string; phone?: string; custom_variables?: any; }) { const url = `${BASE_URL}/leads/${id}`; const body: any = {}; if (first_name) body.first_name = first_name; if (last_name) body.last_name = last_name; if (company_name) body.company_name = company_name; if (personalization) body.personalization = personalization; if (website) body.website = website; if (phone) body.phone = phone; if (custom_variables) body.custom_variables = custom_variables; try { const response = await fetch(url, { method: 'PATCH', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to update lead: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as LeadResponse; return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error updating lead: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleDeleteLead(id: string) { const url = `${BASE_URL}/leads/${id}`; try { const response = await fetch(url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}` } }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to delete lead: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as LeadResponse; return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error deleting lead: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleListCampaigns({ limit = 5, starting_after, status }: { limit?: number; starting_after?: string; status?: number; }) { const url = new URL(`${BASE_URL}/campaigns`); if (limit) url.searchParams.append('limit', limit.toString()); if (starting_after) url.searchParams.append('starting_after', starting_after); if (status !== undefined) url.searchParams.append('status', status.toString()); try { const response = await fetch(url.toString(), { method: 'GET', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}` } }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to list campaigns: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as InstantlyResponse; // Format the campaigns in a more human-readable format let formattedText = "## Campaigns\n\n"; if (!data.items || data.items.length === 0) { formattedText += "No campaigns found.\n"; } else { // Add a summary count formattedText += `Found ${data.items.length} campaigns with the specified criteria.\n\n`; // Create a table-like structure with the most important fields formattedText += "| ID | Name | Status | Creation Date |\n"; formattedText += "|---|---|---|---|\n"; // Map status codes to readable strings const statusMap: {[key: number]: string} = { 0: "Draft", 1: "Active", 2: "Paused", 3: "Completed", 4: "Running Subsequences", "-99": "Account Suspended", "-1": "Accounts Unhealthy", "-2": "Bounce Protect" }; data.items.forEach(campaign => { const statusText = statusMap[campaign.status] || `Unknown (${campaign.status})`; const creationDate = campaign.timestamp_created ? new Date(campaign.timestamp_created).toLocaleDateString() : "N/A"; formattedText += `| ${campaign.id} | ${campaign.name} | ${statusText} | ${creationDate} |\n`; }); } // Add pagination information if (data.next_starting_after) { formattedText += `\n## Pagination\n\nThis is page ${starting_after ? 'n' : '1'} of results. To view the next page, call instantly_list_campaigns with:\n\`\`\`json\n{"starting_after": "${data.next_starting_after}"${status !== undefined ? `, "status": ${status}` : ""}}\n\`\`\`\n`; } else { formattedText += "\n## Pagination\n\nThis is the final page of results. No more pages available.\n"; } return { content: [{ type: "text", text: formattedText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error listing campaigns: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleGetCampaign(id: string) { const url = `${BASE_URL}/campaigns/${id}`; try { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}` } }); if (!response.ok) { const errorText = await response.text(); return { content: [{ type: "text", text: `Failed to get campaign: ${response.status} ${response.statusText} - ${errorText}` }], isError: true }; } const data = await response.json() as CampaignResponse; return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error getting campaign: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleGetWarmupAnalytics(emails: string[]) { const url = `${BASE_URL}/accounts/warmup-analytics`; try { const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ emails }) }).catch(err => { throw new Error(`Network error: ${err.message}`); }); if (!response.ok) { const errorText = await response.text().catch(e => 'Could not read error response'); // More specific error message based on status code let errorMessage = `Failed to get warmup analytics: ${response.status} ${response.statusText}`; if (response.status === 401) { errorMessage = "Authentication failed: Please check your API key."; } else if (response.status === 403) { errorMessage = "Not authorized: Your API key doesn't have permission to access warmup analytics."; } else if (response.status === 400) { errorMessage = `Bad request: ${errorText}`; } else if (response.status === 404) { errorMessage = "Endpoint not found: The warmup analytics API may have changed."; } else if (response.status === 429) { errorMessage = "Rate limit exceeded: Please try again later."; } return { content: [{ type: "text", text: `${errorMessage}\n\nTechnical details: ${errorText}` }], isError: true }; } const data = await response.json() as WarmupAnalyticsResponse; // Format the response in a readable way let formattedText = "## Warmup Analytics\n\n"; const emailList = new Set([ ...Object.keys(data.email_date_data || {}), ...Object.keys(data.aggregate_data || {}) ]); if (emailList.size === 0) { formattedText += "No data found for the specified emails. This could be because:\n\n"; formattedText += "- The emails don't exist in your Instantly account\n"; formattedText += "- The emails haven't been used for warmup yet\n"; formattedText += "- The analytics data is not yet available\n\n"; formattedText += "Please check that you've entered valid email addresses and that they are properly configured in your Instantly account."; } else { for (const email of emailList) { formattedText += `### ${email}\n\n`; // Add aggregate metrics and health score const aggregateData = data.aggregate_data?.[email]; if (aggregateData) { formattedText += "#### Aggregate Metrics\n\n"; // Add health score if available if (aggregateData.health_score !== undefined) { formattedText += `**Health Score**: ${aggregateData.health_score}${aggregateData.health_score_label ? ` (${aggregateData.health_score_label})` : ''}\n\n`; } formattedText += "| Metric | Value |\n|---|---|\n"; if (aggregateData.sent !== undefined) formattedText += `| Total Sent | ${aggregateData.sent} |\n`; if (aggregateData.landed_inbox !== undefined) formattedText += `| Landed in Inbox | ${aggregateData.landed_inbox} |\n`; if (aggregateData.landed_spam !== undefined) formattedText += `| Landed in Spam | ${aggregateData.landed_spam} |\n`; if (aggregateData.received !== undefined) formattedText += `| Total Received | ${aggregateData.received} |\n`; formattedText += "\n"; } // Add daily metrics const dailyData = data.email_date_data?.[email]; if (dailyData && Object.keys(dailyData).length > 0) { formattedText += "#### Daily Metrics\n\n"; formattedText += "| Date | Sent | Inbox | Spam | Received |\n|---|---|---|---|---|\n"; // Sort dates in descending order (newest first) const sortedDates = Object.keys(dailyData).sort((a, b) => new Date(b).getTime() - new Date(a).getTime()); for (const date of sortedDates) { const dayData = dailyData[date]; formattedText += `| ${date} | ${dayData.sent || 0} | ${dayData.landed_inbox || 0} | ${dayData.landed_spam || 0} | ${dayData.received || 0} |\n`; } formattedText += "\n"; } else if (!aggregateData) { formattedText += "No data available for this email.\n\n"; } } } return { content: [{ type: "text", text: formattedText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error getting warmup analytics: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleTestAccountVitals(accounts: string[]) { const url = `${BASE_URL}/accounts/test/vitals`; try { const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${INSTANTLY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ accounts }) }).catch(err => { throw new Error(`Network error: ${err.message}`); }); if (!response.ok) { const errorText = await response.text().catch(e => 'Could not read error response'); // More specific error message based on status code let errorMessage = `Failed to test account vitals: ${response.status} ${response.statusText}`; if (response.status === 401) { errorMessage = "Authentication failed: Please check your API key."; } else if (response.status === 403) { errorMessage = "Not authorized: Your API key doesn't have permission to access account vitals."; } else if (response.status === 400) { errorMessage = `Bad request: ${errorText}`; } else if (response.status === 404) { errorMessage = "Endpoint not found: The account vitals API may have changed."; } else if (response.status === 429) { errorMessage = "Rate limit exceeded: Please try again later."; } return { content: [{ type: "text", text: `${errorMessage}\n\nTechnical details: ${errorText}` }], isError: true }; } const data = await response.json() as AccountVitalsResponse; // Format the response in a readable way let formattedText = "## Account Vitals Test Results\n\n"; // Add overall status if available if (data.status) { formattedText += `**Overall Status**: ${data.status}\n\n`; } // Summary section const successCount = data.success_list?.length || 0; const failureCount = data.failure_list?.length || 0; formattedText += `**Summary**: Tested ${accounts.length} account(s). ${successCount} successful, ${failureCount} failed.\n\n`; // Handle successful accounts if (data.success_list && data.success_list.length > 0) { formattedText += "### ✅ Successful Accounts\n\n"; formattedText += "| Email | Status | Provider | Last Used |\n|---|---|---|---|\n"; for (const account of data.success_list) { const provider = account.provider_name || `Provider #${account.provider_code || 'unknown'}`; const lastUsed = account.last_used ? new Date(account.last_used).toLocaleString() : 'N/A'; formattedText += `| ${account.email} | ${account.status || 'Working'} | ${provider} | ${lastUsed} |\n`; } formattedText += "\n"; } // Handle failed accounts if (data.failure_list && data.failure_list.length > 0) { formattedText += "### ❌ Failed Accounts\n\n"; formattedText += "| Email | Error | Details | Provider |\n|---|---|---|---|\n"; for (const account of data.failure_list) { const provider = account.provider_name || `Provider #${account.provider_code || 'unknown'}`; formattedText += `| ${account.email} | ${account.error || 'Unknown error'} | ${account.details || 'No details provided'} | ${provider} |\n`; } formattedText += "\n"; } if ((!data.success_list || data.success_list.length === 0) && (!data.failure_list || data.failure_list.length === 0)) { formattedText += "No test results found for the specified accounts. Please verify that these email accounts exist in your Instantly workspace.\n"; } // Add recommendations if there are failures if (data.failure_list && data.failure_list.length > 0) { formattedText += "### 📋 Recommendations\n\n"; formattedText += "For failed accounts, consider the following steps:\n\n"; formattedText += "1. Verify the email credentials are correct in Instantly\n"; formattedText += "2. Check that the email provider allows API access\n"; formattedText += "3. If using Gmail or Google Workspace, verify that less secure app access is enabled or use OAuth\n"; formattedText += "4. For Office 365 accounts, check that the appropriate permissions are granted\n"; formattedText += "5. Contact Instantly support for persistent issues\n"; } return { content: [{ type: "text", text: formattedText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error testing account vitals: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleGetCampaignAnalytics({ id, start_date, end_date }: { id?: string; start_date: string; end_date: string; }) { try { const url = id ? `https://api.instantly.ai/api/v2/campaigns/${id}/analytics?start_date=${start_date}&end_date=${end_date}` : `https://api.instantly.ai/api/v2/campaigns/analytics?start_date=${start_date}&end_date=${end_date}`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` } }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } const data = await response.json() as CampaignAnalyticsResponse; let formattedText = '# Campaign Analytics\n\n'; if (data.length === 0) { formattedText += 'No analytics data available for the specified period.\n'; } else { formattedText += `**Period**: ${start_date} to ${end_date}\n\n`; for (const campaign of data) { formattedText += `### Campaign: ${campaign.campaign_name || 'Unnamed'}\n`; formattedText += `**ID**: ${campaign.campaign_id}\n\n`; // Create a table for key metrics formattedText += "#### Key Metrics\n\n"; formattedText += "| Metric | Value |\n|---|---|\n"; formattedText += `| Total Leads | ${campaign.leads_count || 0} |\n`; formattedText += `| Contacted | ${campaign.contacted_count || 0} |\n`; formattedText += `| Opens | ${campaign.open_count || 0} |\n`; formattedText += `| Replies | ${campaign.reply_count || 0} |\n`; formattedText += `| Bounced | ${campaign.bounced_count || 0} |\n`; formattedText += `| Unsubscribed | ${campaign.unsubscribed_count || 0} |\n`; formattedText += `| Completed | ${campaign.completed_count || 0} |\n`; formattedText += `| Emails Sent | ${campaign.emails_sent_count || 0} |\n`; formattedText += `| New Leads Contacted | ${campaign.new_leads_contacted_count || 0} |\n`; // Add opportunity data if available if (campaign.total_opportunities !== undefined || campaign.total_opportunity_value !== undefined) { formattedText += "\n#### Opportunity Metrics\n\n"; formattedText += "| Metric | Value |\n|---|---|\n"; if (campaign.total_opportunities !== undefined) { formattedText += `| Total Opportunities | ${campaign.total_opportunities} |\n`; } if (campaign.total_opportunity_value !== undefined) { formattedText += `| Total Opportunity Value | ${campaign.total_opportunity_value} |\n`; } } formattedText += "\n---\n\n"; } } return { content: [{ type: "text", text: formattedText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error getting campaign analytics: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } // Account handlers async function handleCreateAccount({ email, first_name, last_name, provider_code, imap_username, imap_password, imap_host, imap_port, smtp_username, smtp_password, smtp_host, smtp_port, daily_limit, tracking_domain_name }: { email: string; first_name: string; last_name: string; provider_code: number; imap_username: string; imap_password: string; imap_host: string; imap_port: number; smtp_username: string; smtp_password: string; smtp_host: string; smtp_port: number; daily_limit?: number; tracking_domain_name?: string; }) { try { const payload: any = { email, first_name, last_name, provider_code, imap_username, imap_password, imap_host, imap_port, smtp_username, smtp_password, smtp_host, smtp_port }; // Add optional fields if provided if (daily_limit !== undefined || tracking_domain_name !== undefined) { payload.warmup = {}; if (daily_limit !== undefined) { payload.warmup.daily_limit = daily_limit; } if (tracking_domain_name !== undefined) { payload.warmup.tracking_domain_name = tracking_domain_name; } } const response = await fetch('https://api.instantly.ai/api/v2/accounts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } const account = await response.json() as AccountResponse; return { content: [{ type: "text", text: `Account created successfully:\n\nEmail: ${account.email}\nName: ${account.first_name} ${account.last_name}\nProvider: ${getProviderNameFromCode(account.provider_code)}\nWarmup Status: ${getWarmupStatusFromCode(account.warmup_status)}\nCreated: ${account.timestamp_created}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error creating account: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleListAccounts({ limit = 10, starting_after, search, status, provider_code, fetch_all = false }: { limit?: number; starting_after?: string; search?: string; status?: number; provider_code?: number; fetch_all?: boolean; }) { try { // This will store all accounts when fetching all pages const allAccounts: AccountResponse[] = []; let nextStartingAfter = starting_after; let hasMorePages = true; let pageCount = 0; // If fetch_all is true, retrieve all pages while ((fetch_all && hasMorePages) || pageCount === 0) { let url = `https://api.instantly.ai/api/v2/accounts?limit=${limit}`; if (nextStartingAfter) { url += `&starting_after=${nextStartingAfter}`; } if (search) { url += `&search=${encodeURIComponent(search)}`; } if (status !== undefined) { url += `&status=${status}`; } if (provider_code !== undefined) { url += `&provider_code=${provider_code}`; } const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` } }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } const data = await response.json() as InstantlyResponse; const accounts = data.items as AccountResponse[]; // Add this page's accounts to our collection allAccounts.push(...accounts); // Prepare for next page if needed nextStartingAfter = data.next_starting_after; hasMorePages = !!nextStartingAfter && accounts.length > 0; pageCount++; // Safety check to avoid infinite loops if (pageCount > 100) { break; } } // Generate formatted output let formattedText = ''; if (fetch_all) { formattedText = generateAccountsSummary(allAccounts); } else { formattedText = generateAccountsTable(allAccounts, nextStartingAfter); } return { content: [{ type: "text", text: formattedText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error listing accounts: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } // Helper function to generate a table of accounts with pagination info function generateAccountsTable(accounts: AccountResponse[], nextStartingAfter?: string): string { let formattedText = '# Email Accounts\n\n'; if (accounts.length === 0) { formattedText += 'No accounts found.\n'; return formattedText; } // Create a table for accounts formattedText += "| Email | Name | Provider | Status | Daily Limit |\n|---|---|---|---|---|\n"; for (const account of accounts) { const providerName = getProviderNameFromCode(account.provider_code); const statusText = getWarmupStatusFromCode(account.warmup_status); const dailyLimit = account.daily_limit || 'N/A'; formattedText += `| ${account.email} | ${account.first_name} ${account.last_name} | ${providerName} | ${statusText} | ${dailyLimit} |\n`; } formattedText += `\n**Total accounts in this page**: ${accounts.length}\n`; if (nextStartingAfter) { formattedText += `\n*For more results, use starting_after=${nextStartingAfter} or set fetch_all=true to automatically retrieve all pages*\n`; } return formattedText; } // Helper function to generate a comprehensive summary of all accounts function generateAccountsSummary(accounts: AccountResponse[]): string { let formattedText = '# Email Accounts Summary\n\n'; if (accounts.length === 0) { formattedText += 'No accounts found.\n'; return formattedText; } // Count accounts by provider const providerCounts: Record<string, number> = {}; // Count accounts by status const statusCounts: Record<string, number> = {}; for (const account of accounts) { const providerName = getProviderNameFromCode(account.provider_code); const statusText = getWarmupStatusFromCode(account.warmup_status); providerCounts[providerName] = (providerCounts[providerName] || 0) + 1; statusCounts[statusText] = (statusCounts[statusText] || 0) + 1; } // Summary statistics formattedText += `**Total Accounts**: ${accounts.length}\n\n`; // Provider breakdown formattedText += "## Accounts by Provider\n\n"; formattedText += "| Provider | Count | Percentage |\n|---|---|---|\n"; for (const [provider, count] of Object.entries(providerCounts)) { const percentage = ((count / accounts.length) * 100).toFixed(1); formattedText += `| ${provider} | ${count} | ${percentage}% |\n`; } // Status breakdown formattedText += "\n## Accounts by Status\n\n"; formattedText += "| Status | Count | Percentage |\n|---|---|---|\n"; for (const [status, count] of Object.entries(statusCounts)) { const percentage = ((count / accounts.length) * 100).toFixed(1); formattedText += `| ${status} | ${count} | ${percentage}% |\n`; } // Add the first few accounts as examples const sampleSize = Math.min(5, accounts.length); formattedText += `\n## Sample Accounts (${sampleSize} of ${accounts.length})\n\n`; formattedText += "| Email | Name | Provider | Status | Daily Limit |\n|---|---|---|---|---|\n"; for (let i = 0; i < sampleSize; i++) { const account = accounts[i]; const providerName = getProviderNameFromCode(account.provider_code); const statusText = getWarmupStatusFromCode(account.warmup_status); const dailyLimit = account.daily_limit || 'N/A'; formattedText += `| ${account.email} | ${account.first_name} ${account.last_name} | ${providerName} | ${statusText} | ${dailyLimit} |\n`; } return formattedText; } async function handleGetAccount(email: string) { try { const response = await fetch(`https://api.instantly.ai/api/v2/accounts/${encodeURIComponent(email)}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` } }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } const account = await response.json() as AccountResponse; // Format the account details in markdown let formattedText = `# Account Details: ${account.email}\n\n`; formattedText += `**Name:** ${account.first_name} ${account.last_name}\n`; formattedText += `**Provider:** ${getProviderNameFromCode(account.provider_code)}\n`; formattedText += `**Status:** ${getWarmupStatusFromCode(account.warmup_status)}\n`; formattedText += `**Created:** ${account.timestamp_created}\n`; formattedText += `**Last Updated:** ${account.timestamp_updated}\n`; if (account.daily_limit) { formattedText += `**Daily Sending Limit:** ${account.daily_limit}\n`; } if (account.tracking_domain_name) { formattedText += `**Tracking Domain:** ${account.tracking_domain_name}\n`; } formattedText += `**Setup Pending:** ${account.setup_pending ? 'Yes' : 'No'}\n`; formattedText += `**Managed Account:** ${account.is_managed_account ? 'Yes' : 'No'}\n`; return { content: [{ type: "text", text: formattedText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error retrieving account: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleUpdateAccount({ email, first_name, last_name, daily_limit, tracking_domain_name, skip_cname_check, remove_tracking_domain }: { email: string; first_name?: string; last_name?: string; daily_limit?: number; tracking_domain_name?: string; skip_cname_check?: boolean; remove_tracking_domain?: boolean; }) { try { const payload: any = {}; if (first_name !== undefined) { payload.first_name = first_name; } if (last_name !== undefined) { payload.last_name = last_name; } if (daily_limit !== undefined || tracking_domain_name !== undefined) { payload.warmup = {}; if (daily_limit !== undefined) { payload.warmup.daily_limit = daily_limit; } if (tracking_domain_name !== undefined) { payload.warmup.tracking_domain_name = tracking_domain_name; } } if (skip_cname_check !== undefined) { payload.skip_cname_check = skip_cname_check; } if (remove_tracking_domain !== undefined) { payload.remove_tracking_domain = remove_tracking_domain; } const response = await fetch(`https://api.instantly.ai/api/v2/accounts/${encodeURIComponent(email)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } const account = await response.json() as AccountResponse; return { content: [{ type: "text", text: `Account ${account.email} updated successfully.\n\nCurrent details:\nName: ${account.first_name} ${account.last_name}\nStatus: ${getWarmupStatusFromCode(account.warmup_status)}\nDaily Limit: ${account.daily_limit || 'N/A'}\nTracking Domain: ${account.tracking_domain_name || 'None'}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error updating account: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleDeleteAccount(email: string) { try { const response = await fetch(`https://api.instantly.ai/api/v2/accounts/${encodeURIComponent(email)}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` } }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } return { content: [{ type: "text", text: `Account ${email} has been deleted successfully.` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error deleting account: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handlePauseAccount(email: string) { try { const response = await fetch(`https://api.instantly.ai/api/v2/accounts/${encodeURIComponent(email)}/pause`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` } }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } return { content: [{ type: "text", text: `Account ${email} has been paused successfully.` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error pausing account: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async function handleResumeAccount(email: string) { try { const response = await fetch(`https://api.instantly.ai/api/v2/accounts/${encodeURIComponent(email)}/resume`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getApiKey()}` } }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}: ${await response.text()}`); } return { content: [{ type: "text", text: `Account ${email} has been resumed successfully.` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error resuming account: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } // Helper functions for account status and provider codes function getProviderNameFromCode(code: number): string { switch (code) { case 1: return 'Custom IMAP/SMTP'; case 2: return 'Google'; case 3: return 'Microsoft'; case 4: return 'AWS'; default: return 'Unknown'; } } function getWarmupStatusFromCode(code: number): string { switch (code) { case 0: return 'Paused'; case 1: return 'Active'; case -1: return 'Banned'; case -2: return 'Spam Folder Unknown'; case -3: return 'Permanent Suspension'; default: return 'Unknown'; } } // Server setup const server = new Server( { name: "mcp-server/instantly", version: "0.1.0", }, { capabilities: { tools: { instantly_create_lead: {}, instantly_get_lead: {}, instantly_list_leads: {}, instantly_update_lead: {}, instantly_delete_lead: {}, instantly_list_campaigns: {}, instantly_get_campaign: {}, instantly_get_warmup_analytics: {}, instantly_test_account_vitals: {}, instantly_get_campaign_analytics: {}, instantly_create_account: {}, instantly_list_accounts: {}, instantly_get_account: {}, instantly_update_account: {}, instantly_delete_account: {}, instantly_pause_account: {}, instantly_resume_account: {}, }, }, }, ); // Set up request handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: INSTANTLY_TOOLS, })); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case "instantly_create_lead": { const params = request.params.arguments as { email: string; first_name?: string; last_name?: string; company_name?: string; campaign?: string; list_id?: string; personalization?: string; website?: string; phone?: string; custom_variables?: any; }; return await handleCreateLead(params); } case "instantly_get_lead": { const { id } = request.params.arguments as { id: string }; return await handleGetLead(id); } case "instantly_list_leads": { const params = request.params.arguments as { campaign?: string; list_id?: string; limit?: number; starting_after?: string; }; return await handleListLeads(params); } case "instantly_update_lead": { const params = request.params.arguments as { id: string; first_name?: string; last_name?: string; company_name?: string; personalization?: string; website?: string; phone?: string; custom_variables?: any; }; return await handleUpdateLead(params); } case "instantly_delete_lead": { const { id } = request.params.arguments as { id: string }; return await handleDeleteLead(id); } case "instantly_list_campaigns": { const params = request.params.arguments as { limit?: number; starting_after?: string; status?: number; }; return await handleListCampaigns(params); } case "instantly_get_campaign": { const { id } = request.params.arguments as { id: string }; return await handleGetCampaign(id); } case "instantly_get_warmup_analytics": { const params = request.params.arguments as { emails: string[]; }; return await handleGetWarmupAnalytics(params.emails); } case "instantly_test_account_vitals": { const params = request.params.arguments as { accounts: string[]; }; return await handleTestAccountVitals(params.accounts); } case "instantly_get_campaign_analytics": { const params = request.params.arguments as { id?: string; start_date: string; end_date: string; }; return await handleGetCampaignAnalytics(params); } case "instantly_create_account": { const params = request.params.arguments as { email: string; first_name: string; last_name: string; provider_code: number; imap_username: string; imap_password: string; imap_host: string; imap_port: number; smtp_username: string; smtp_password: string; smtp_host: string; smtp_port: number; daily_limit?: number; tracking_domain_name?: string; }; return await handleCreateAccount(params); } case "instantly_list_accounts": { const params = request.params.arguments as { limit?: number; starting_after?: string; search?: string; status?: number; provider_code?: number; fetch_all?: boolean; }; return await handleListAccounts(params); } case "instantly_get_account": { const { email } = request.params.arguments as { email: string }; return await handleGetAccount(email); } case "instantly_update_account": { const params = request.params.arguments as { email: string; first_name?: string; last_name?: string; daily_limit?: number; tracking_domain_name?: string; skip_cname_check?: boolean; remove_tracking_domain?: boolean; }; return await handleUpdateAccount(params); } case "instantly_delete_account": { const { email } = request.params.arguments as { email: string }; return await handleDeleteAccount(email); } case "instantly_pause_account": { const { email } = request.params.arguments as { email: string }; return await handlePauseAccount(email); } case "instantly_resume_account": { const { email } = request.params.arguments as { email: string }; return await handleResumeAccount(email); } default: return { content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }], isError: true }; } } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Instantly MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });