This is page 1 of 3. Use http://codebase.md/jean-technologies/smartlead-mcp-server-local?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .gitignore ├── DEVELOPER_ONBOARDING.md ├── Dockerfile ├── jest.config.js ├── llms-install.md ├── mcp_settings_example.json ├── package-lock.json ├── package.json ├── README.md ├── server │ └── license-server.js ├── smithery.yaml ├── src │ ├── cli.ts │ ├── config │ │ └── feature-config.ts │ ├── handlers │ │ ├── campaign.ts │ │ ├── clientManagement.ts │ │ ├── email.ts │ │ ├── lead.ts │ │ ├── smartDelivery.ts │ │ ├── smartSenders.ts │ │ ├── statistics.ts │ │ └── webhooks.ts │ ├── index.ts │ ├── licensing │ │ ├── index.ts │ │ └── stripe-integration.js │ ├── n8n │ │ └── index.ts │ ├── registry │ │ └── tool-registry.ts │ ├── supergateway-mock.ts │ ├── supergateway.ts │ ├── tools │ │ ├── campaign.ts │ │ ├── clientManagement.ts │ │ ├── email.d.ts │ │ ├── email.ts │ │ ├── lead.d.ts │ │ ├── lead.ts │ │ ├── smartDelivery.d.ts │ │ ├── smartDelivery.ts │ │ ├── smartSenders.ts │ │ ├── statistics.d.ts │ │ ├── statistics.ts │ │ └── webhooks.ts │ ├── types │ │ ├── campaign.ts │ │ ├── clientManagement.ts │ │ ├── common.ts │ │ ├── email.d.ts │ │ ├── email.ts │ │ ├── lead.ts │ │ ├── smartDelivery.ts │ │ ├── smartSenders.ts │ │ ├── statistics.d.ts │ │ ├── statistics.ts │ │ ├── supergateway.d.ts │ │ └── webhooks.ts │ └── utils │ └── download-tracker.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | 7 | # Environment variables 8 | .env 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage/ 23 | 24 | # IDE files 25 | .idea/ 26 | .vscode/ 27 | *.swp 28 | *.swo 29 | 30 | # OS files 31 | .DS_Store 32 | Thumbs.db 33 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Smartlead API Key (required) 2 | SMARTLEAD_API_KEY=your_api_key_here 3 | 4 | # Optional: Custom API URL (defaults to https://server.smartlead.ai/api/v1) 5 | # SMARTLEAD_API_URL=https://custom-server.smartlead.ai/api/v1 6 | 7 | # Retry configuration 8 | SMARTLEAD_RETRY_MAX_ATTEMPTS=3 9 | SMARTLEAD_RETRY_INITIAL_DELAY=1000 10 | SMARTLEAD_RETRY_MAX_DELAY=10000 11 | SMARTLEAD_RETRY_BACKOFF_FACTOR=2 12 | 13 | # Supergateway Integration 14 | # Set to 'true' to enable Supergateway integration 15 | USE_SUPERGATEWAY=false 16 | # Required if USE_SUPERGATEWAY is true 17 | 18 | # Download Tracking Configuration (optional) 19 | # DOWNLOAD_LOG_PATH=/custom/path/to/downloads.json 20 | 21 | # Enable Features 22 | # All features are enabled by default. No license key required. 23 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/jean-technologies-smartlead-mcp-server-local) 2 | 3 | # Smartlead Simplified MCP Server 4 | 5 | [](https://smithery.ai/server/@jean-technologies/smartlead-mcp-server-local) 6 | 7 | This application provides a simplified interface to the Smartlead API, allowing AI assistants and automation tools to interact with Smartlead's email marketing features. We welcome contribution from the community. 8 | 9 | **Licensing:** All features are now enabled by default with maximum permissiveness! No license key required. 10 | 11 | > **For developer details:** See [DEVELOPER_ONBOARDING.md](./DEVELOPER_ONBOARDING.md) 12 | 13 | ## Quick Start 14 | 15 | ### Installation 16 | ```bash 17 | npm install [email protected] 18 | ``` 19 | 20 | or use directly with npx (no installation needed): 21 | 22 | 23 | ### Installing via Smithery 24 | 25 | To install Smartlead Campaign Management Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@jean-technologies/smartlead-mcp-server-local): 26 | 27 | ```bash 28 | npx -y @smithery/cli install @jean-technologies/smartlead-mcp-server-local --client claude 29 | ``` 30 | 31 | ### With Claude: 32 | ```bash 33 | npx smartlead-mcp-server start 34 | ``` 35 | 36 | ### With n8n: 37 | ```bash 38 | npx smartlead-mcp-server sse 39 | ``` 40 | 41 | First run will prompt for your Smartlead API Key. No license key is required. 42 | 43 | ## Integration Examples 44 | 45 | ### Claude Extension: 46 | ```json 47 | { 48 | "mcpServers": { 49 | "smartlead": { 50 | "command": "npx", 51 | "args": ["smartlead-mcp-server", "start"], 52 | "env": { 53 | "SMARTLEAD_API_KEY": "your_api_key_here" 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ### n8n Setup: 61 | 1. Start the server: `npx smartlead-mcp-server sse` 62 | 2. Configure n8n MCP Client node with: 63 | - SSE URL: `http://localhost:3000/sse` 64 | - Message URL: `http://localhost:3000/message` 65 | 66 | ## Available Features 67 | 68 | **All features are now enabled by default, including:** 69 | - Campaign & Lead Management 70 | - Statistics and Analytics 71 | - Smart Delivery & Webhooks 72 | - n8n Integration 73 | - Client Management 74 | - Smart Senders 75 | - Download Tracking and Analytics 76 | 77 | ## New Download Tracking Features 78 | 79 | This release adds new download tracking capabilities: 80 | 81 | ### Download Campaign Data 82 | Download campaign data with tracking using the `smartlead_download_campaign_data` tool: 83 | ```json 84 | { 85 | "campaign_id": 12345, 86 | "download_type": "analytics", // "analytics", "leads", "sequence", "full_export" 87 | "format": "json", // "json" or "csv" 88 | "user_id": "optional-user-identifier" 89 | } 90 | ``` 91 | 92 | ### View Download Statistics 93 | View download statistics using the `smartlead_view_download_statistics` tool: 94 | ```json 95 | { 96 | "time_period": "all", // "all", "today", "week", "month" 97 | "group_by": "type" // "type", "format", "campaign", "date" 98 | } 99 | ``` 100 | 101 | All downloads are tracked in `~/.smartlead-mcp/downloads.json` for analytics. 102 | 103 | ## Need Help? 104 | 105 | - Run `npx smartlead-mcp-server config` to set up credentials 106 | - Use `--api-key` option for non-interactive setup 107 | - Contact: [email protected] 108 | - Website: [jeantechnologies.com](https://jeantechnologies.com) 109 | 110 | ## License 111 | 112 | This software is proprietary and confidential. Unauthorized copying, redistribution, or use of this software, in whole or in part, via any medium, is strictly prohibited without the express permission of Jean Technologies. 113 | 114 | Copyright © 2025 Jean Technologies. All rights reserved. 115 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript 1 | export default { 2 | transform: {}, 3 | extensionsToTreatAsEsm: ['.ts'], 4 | moduleNameMapper: { 5 | '^(\\.{1,2}/.*)\\.js$': '$1', 6 | }, 7 | testEnvironment: 'node', 8 | verbose: true, 9 | testTimeout: 10000 10 | }; ``` -------------------------------------------------------------------------------- /mcp_settings_example.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "smartlead": { 4 | "command": "node", 5 | "args": ["E:/mcp-servers/smartlead/dist/index.js"], 6 | "env": { 7 | "SMARTLEAD_API_KEY": "your_api_key_here" 8 | }, 9 | "disabled": false, 10 | "autoApprove": [], 11 | "name": "@jean-technologies/smartlead-mcp" 12 | } 13 | } 14 | } 15 | 16 | 17 | 18 | ``` -------------------------------------------------------------------------------- /src/types/statistics.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface CampaignMailboxStatisticsParams { 2 | campaign_id: number; 3 | } 4 | 5 | export interface DownloadCampaignDataParams { 6 | campaign_id: number; 7 | download_type: 'analytics' | 'leads' | 'sequence' | 'full_export'; 8 | format: 'json' | 'csv'; 9 | user_id?: string; 10 | } 11 | 12 | export interface ViewDownloadStatisticsParams { 13 | time_period?: 'all' | 'today' | 'week' | 'month'; 14 | group_by?: 'type' | 'format' | 'campaign' | 'date'; 15 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "typeRoots": ["./node_modules/@types", "./src/types"] 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 17 | } 18 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config 2 | FROM node:lts-alpine 3 | WORKDIR /app 4 | 5 | # Install build tools and dependencies 6 | COPY package.json package-lock.json tsconfig.json ./ 7 | COPY src ./src 8 | 9 | RUN apk add --no-cache python3 make g++ \ 10 | && npm ci --ignore-scripts \ 11 | && npm run build \ 12 | && npm prune --production \ 13 | && apk del python3 make g++ 14 | 15 | # Default command for stdio usage 16 | CMD ["node", "dist/index.js"] 17 | ``` -------------------------------------------------------------------------------- /src/tools/lead.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type declarations for Lead tools 2 | declare module './tools/lead.js' { 3 | import { CategoryTool } from '../types/common.js'; 4 | 5 | export const LIST_LEADS_TOOL: CategoryTool; 6 | export const GET_LEAD_TOOL: CategoryTool; 7 | export const ADD_LEAD_TO_CAMPAIGN_TOOL: CategoryTool; 8 | export const UPDATE_LEAD_TOOL: CategoryTool; 9 | export const UPDATE_LEAD_STATUS_TOOL: CategoryTool; 10 | export const BULK_IMPORT_LEADS_TOOL: CategoryTool; 11 | export const DELETE_LEAD_TOOL: CategoryTool; 12 | 13 | export const leadTools: CategoryTool[]; 14 | } ``` -------------------------------------------------------------------------------- /src/types/common.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | 3 | // Extended Tool type with category information 4 | export interface CategoryTool extends Tool { 5 | category: string; 6 | } 7 | 8 | // Categories enum 9 | export enum ToolCategory { 10 | CAMPAIGN_MANAGEMENT = 'campaignManagement', 11 | EMAIL_ACCOUNT_MANAGEMENT = 'emailAccountManagement', 12 | LEAD_MANAGEMENT = 'leadManagement', 13 | CAMPAIGN_STATISTICS = 'campaignStatistics', 14 | SMART_DELIVERY = 'smartDelivery', 15 | WEBHOOKS = 'webhooks', 16 | CLIENT_MANAGEMENT = 'clientManagement', 17 | SMART_SENDERS = 'smartSenders' 18 | } ``` -------------------------------------------------------------------------------- /src/tools/statistics.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type declarations for Statistics tools 2 | declare module './tools/statistics.js' { 3 | import { CategoryTool } from '../types/common.js'; 4 | 5 | export const CAMPAIGN_STATISTICS_TOOL: CategoryTool; 6 | export const CAMPAIGN_STATISTICS_BY_DATE_TOOL: CategoryTool; 7 | export const WARMUP_STATS_BY_EMAIL_TOOL: CategoryTool; 8 | export const CAMPAIGN_TOP_LEVEL_ANALYTICS_TOOL: CategoryTool; 9 | export const CAMPAIGN_TOP_LEVEL_ANALYTICS_BY_DATE_TOOL: CategoryTool; 10 | export const CAMPAIGN_LEAD_STATISTICS_TOOL: CategoryTool; 11 | export const CAMPAIGN_MAILBOX_STATISTICS_TOOL: CategoryTool; 12 | 13 | export const statisticsTools: CategoryTool[]; 14 | } ``` -------------------------------------------------------------------------------- /src/tools/email.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type declarations for Email tools 2 | declare module './tools/email.js' { 3 | import { CategoryTool } from '../types/common.js'; 4 | 5 | export const LIST_EMAIL_ACCOUNTS_CAMPAIGN_TOOL: CategoryTool; 6 | export const ADD_EMAIL_TO_CAMPAIGN_TOOL: CategoryTool; 7 | export const REMOVE_EMAIL_FROM_CAMPAIGN_TOOL: CategoryTool; 8 | export const FETCH_EMAIL_ACCOUNTS_TOOL: CategoryTool; 9 | export const CREATE_EMAIL_ACCOUNT_TOOL: CategoryTool; 10 | export const UPDATE_EMAIL_ACCOUNT_TOOL: CategoryTool; 11 | export const FETCH_EMAIL_ACCOUNT_BY_ID_TOOL: CategoryTool; 12 | export const UPDATE_EMAIL_WARMUP_TOOL: CategoryTool; 13 | export const RECONNECT_EMAIL_ACCOUNT_TOOL: CategoryTool; 14 | export const UPDATE_EMAIL_ACCOUNT_TAG_TOOL: CategoryTool; 15 | 16 | export const emailTools: CategoryTool[]; 17 | } ``` -------------------------------------------------------------------------------- /src/types/supergateway.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Type definitions for Supergateway 3 | */ 4 | 5 | declare module 'supergateway' { 6 | export class SuperGateway { 7 | /** 8 | * Create a new SuperGateway instance 9 | * @param options Configuration options including API key 10 | */ 11 | constructor(options?: { apiKey?: string, [key: string]: any }); 12 | 13 | /** 14 | * Process a request 15 | * @param input Input text to process 16 | * @param options Processing options 17 | * @returns Processed text 18 | */ 19 | process(input: string, options?: Record<string, any>): Promise<string>; 20 | 21 | /** 22 | * Stream a request response 23 | * @param input Input text to process 24 | * @param options Processing options 25 | * @returns Readable stream with response chunks 26 | */ 27 | stream(input: string, options?: Record<string, any>): Promise<ReadableStream>; 28 | 29 | /** 30 | * Close the client connection 31 | */ 32 | close(): Promise<void>; 33 | } 34 | } ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config 2 | 3 | startCommand: 4 | type: stdio 5 | commandFunction: 6 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 7 | |- 8 | (config) => ({ command: 'node', args: ['dist/index.js'], env: { SMARTLEAD_API_KEY: config.smartleadApiKey, JEAN_LICENSE_KEY: config.jeanLicenseKey || 'JEANPARTNER', ...(config.smartleadApiUrl ? { SMARTLEAD_API_URL: config.smartleadApiUrl } : {}) } }) 9 | configSchema: 10 | # JSON Schema defining the configuration options for the MCP. 11 | type: object 12 | required: 13 | - smartleadApiKey 14 | properties: 15 | smartleadApiKey: 16 | type: string 17 | description: Smartlead API Key 18 | jeanLicenseKey: 19 | type: string 20 | default: JEANPARTNER 21 | description: Jean License Key 22 | smartleadApiUrl: 23 | type: string 24 | default: https://server.smartlead.ai/api/v1 25 | description: Optional Smartlead API URL 26 | exampleConfig: 27 | smartleadApiKey: YOUR_SMARTLEAD_API_KEY 28 | jeanLicenseKey: JEANPARTNER 29 | smartleadApiUrl: https://server.smartlead.ai/api/v1 30 | ``` -------------------------------------------------------------------------------- /src/types/clientManagement.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type definitions for Client Management functionality 2 | 3 | // Client permission types 4 | export type ClientPermission = 'reply_master_inbox' | 'full_access' | string; 5 | 6 | // Add Client To System 7 | export interface AddClientParams { 8 | name: string; 9 | email: string; 10 | permission: ClientPermission[]; 11 | logo?: string; 12 | logo_url?: string | null; 13 | password: string; 14 | } 15 | 16 | // Fetch all clients 17 | export interface FetchAllClientsParams { 18 | // This endpoint doesn't require specific parameters beyond the API key 19 | // which is handled at the API client level 20 | } 21 | 22 | // Type guards 23 | export function isAddClientParams(args: unknown): args is AddClientParams { 24 | if (typeof args !== 'object' || args === null) return false; 25 | 26 | const params = args as Partial<AddClientParams>; 27 | 28 | return ( 29 | typeof params.name === 'string' && 30 | typeof params.email === 'string' && 31 | Array.isArray(params.permission) && 32 | params.permission.every(perm => typeof perm === 'string') && 33 | typeof params.password === 'string' && 34 | (params.logo === undefined || typeof params.logo === 'string') && 35 | (params.logo_url === undefined || params.logo_url === null || typeof params.logo_url === 'string') 36 | ); 37 | } 38 | 39 | export function isFetchAllClientsParams(args: unknown): args is FetchAllClientsParams { 40 | // Since this endpoint doesn't require specific parameters beyond the API key 41 | // Any object is valid 42 | return typeof args === 'object' && args !== null; 43 | } ``` -------------------------------------------------------------------------------- /src/tools/clientManagement.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from '../types/common.js'; 2 | 3 | // Client Management Tools 4 | 5 | export const ADD_CLIENT_TOOL: CategoryTool = { 6 | name: 'smartlead_add_client', 7 | description: 'Add a new client to the system, optionally with white-label settings.', 8 | category: ToolCategory.CLIENT_MANAGEMENT, 9 | inputSchema: { 10 | type: 'object', 11 | properties: { 12 | name: { 13 | type: 'string', 14 | description: 'Name of the client', 15 | }, 16 | email: { 17 | type: 'string', 18 | description: 'Email address of the client', 19 | }, 20 | permission: { 21 | type: 'array', 22 | items: { type: 'string' }, 23 | description: 'Array of permissions to grant to the client. Use ["full_access"] for full permissions.', 24 | }, 25 | logo: { 26 | type: 'string', 27 | description: 'Logo text or identifier', 28 | }, 29 | logo_url: { 30 | type: ['string', 'null'], 31 | description: 'URL to the client\'s logo image', 32 | }, 33 | password: { 34 | type: 'string', 35 | description: 'Password for the client\'s account', 36 | }, 37 | }, 38 | required: ['name', 'email', 'permission', 'password'], 39 | }, 40 | }; 41 | 42 | export const FETCH_ALL_CLIENTS_TOOL: CategoryTool = { 43 | name: 'smartlead_fetch_all_clients', 44 | description: 'Retrieve a list of all clients in the system.', 45 | category: ToolCategory.CLIENT_MANAGEMENT, 46 | inputSchema: { 47 | type: 'object', 48 | properties: { 49 | // This endpoint doesn't require specific parameters beyond the API key 50 | // which is handled at the API client level 51 | }, 52 | required: [], 53 | }, 54 | }; 55 | 56 | // Export all tools as an array for easy registration 57 | export const clientManagementTools = [ 58 | ADD_CLIENT_TOOL, 59 | FETCH_ALL_CLIENTS_TOOL, 60 | ]; ``` -------------------------------------------------------------------------------- /src/config/feature-config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { isCategoryEnabled } from '../licensing/index.js'; 2 | 3 | // Configuration for enabling/disabling feature categories 4 | // These are the default values when license validation is unavailable 5 | export const enabledCategories = { 6 | campaignManagement: true, 7 | emailAccountManagement: true, 8 | leadManagement: true, 9 | campaignStatistics: true, 10 | smartDelivery: true, 11 | webhooks: true, 12 | clientManagement: true, 13 | smartSenders: true 14 | }; 15 | 16 | // Configuration for enabling/disabling individual tools 17 | // This overrides category settings for specific tools 18 | export const enabledTools: Record<string, boolean> = { 19 | // Override specific tools if needed 20 | // Example: To enable a specific tool in a disabled category: 21 | // smartlead_fetch_campaign_sequence: true, 22 | }; 23 | 24 | // Feature flags for experimental features 25 | export const featureFlags = { 26 | betaFeatures: process.env.ENABLE_BETA_FEATURES === 'true', 27 | extendedLogging: process.env.EXTENDED_LOGGING === 'true', 28 | n8nIntegration: false // Will be set by license validation 29 | }; 30 | 31 | // Helper function to check if a tool should be enabled 32 | export async function isToolEnabled(toolName: string, category: string): Promise<boolean> { 33 | // Always enable all tools 34 | return true; 35 | 36 | // The following code is kept for reference but will never execute 37 | // // Check if the tool has a specific override 38 | // if (enabledTools[toolName] !== undefined) { 39 | // return enabledTools[toolName]; 40 | // } 41 | // 42 | // // Otherwise, check if the category is enabled by the license 43 | // try { 44 | // return await isCategoryEnabled(category); 45 | // } catch (error) { 46 | // // Fallback to default configuration if license check fails 47 | // console.error(`License validation failed, using default configuration: ${error}`); 48 | // const categoryKey = category as keyof typeof enabledCategories; 49 | // return enabledCategories[categoryKey] || false; 50 | // } 51 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "smartlead-mcp-server", 3 | "version": "1.2.1", 4 | "description": "MCP server for Smartlead campaign management integration. Features include creating campaigns, updating campaign settings, and managing campaign sequences.", 5 | "author": "Jonathan Politzki", 6 | "type": "module", 7 | "bin": { 8 | "smartlead-mcp": "dist/cli.js" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "tsc", 15 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 16 | "test:supergateway": "node test_supergateway.js", 17 | "start": "node dist/index.js", 18 | "start:supergateway": "USE_SUPERGATEWAY=true SUPERGATEWAY_API_KEY=test_key node dist/index.js", 19 | "start:sse": "npx -y supergateway --stdio \"node dist/index.js\" --port 3000", 20 | "start:sse-supergateway": "npx -y supergateway --stdio \"USE_SUPERGATEWAY=true SUPERGATEWAY_API_KEY=test_key node dist/index.js\" --port 3000", 21 | "dev": "npm run build && npm start", 22 | "dev:supergateway": "npm run build && npm run start:supergateway", 23 | "dev:sse": "npm run build && npm run start:sse", 24 | "dev:sse-supergateway": "npm run build && npm run start:sse-supergateway", 25 | "lint": "eslint src/**/*.ts", 26 | "lint:fix": "eslint src/**/*.ts --fix", 27 | "format": "prettier --write .", 28 | "prepare": "npm run build" 29 | }, 30 | "license": "MIT", 31 | "dependencies": { 32 | "@modelcontextprotocol/sdk": "^1.4.1", 33 | "axios": "^1.6.2", 34 | "commander": "^13.1.0", 35 | "dotenv": "^16.4.7", 36 | "mcp-proxy-auth": "^1.0.2", 37 | "p-queue": "^8.0.1", 38 | "uuid": "^11.1.0" 39 | }, 40 | "devDependencies": { 41 | "@jest/globals": "^29.7.0", 42 | "@types/jest": "^29.5.14", 43 | "@types/node": "^20.10.5", 44 | "@types/uuid": "^10.0.0", 45 | "@typescript-eslint/eslint-plugin": "^7.0.0", 46 | "@typescript-eslint/parser": "^7.0.0", 47 | "eslint": "^8.56.0", 48 | "eslint-config-prettier": "^9.1.0", 49 | "jest": "^29.7.0", 50 | "jest-mock-extended": "^4.0.0-beta1", 51 | "prettier": "^3.1.1", 52 | "ts-jest": "^29.1.1", 53 | "typescript": "^5.3.3" 54 | }, 55 | "engines": { 56 | "node": ">=18.0.0" 57 | }, 58 | "keywords": [ 59 | "mcp", 60 | "smartlead", 61 | "campaign-management", 62 | "email-marketing" 63 | ] 64 | } 65 | ``` -------------------------------------------------------------------------------- /src/supergateway-mock.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Mock implementation of the SuperGateway client 3 | * This is used as a fallback when the real SuperGateway package is not available 4 | */ 5 | 6 | /** 7 | * SuperGateway client mock implementation 8 | */ 9 | export class SuperGateway { 10 | private apiKey: string | undefined; 11 | private options: Record<string, any>; 12 | 13 | /** 14 | * Create a new SuperGateway instance 15 | * @param options Configuration options 16 | */ 17 | constructor(options: { apiKey?: string, [key: string]: any } = {}) { 18 | console.error('[Supergateway] Initialized with API key:', options.apiKey ? '[REDACTED]' : 'undefined'); 19 | this.apiKey = options.apiKey; 20 | this.options = { ...options }; 21 | 22 | // Remove apiKey from options to avoid logging it 23 | delete this.options.apiKey; 24 | } 25 | 26 | /** 27 | * Process a request 28 | * @param input Input text to process 29 | * @param options Processing options 30 | * @returns Processed text 31 | */ 32 | async process(input: string, options?: Record<string, any>): Promise<string> { 33 | console.error('[Supergateway] Processing request'); 34 | console.error('[Supergateway] Input:', input); 35 | console.error('[Supergateway] Options:', options); 36 | 37 | // Simulate processing delay 38 | await new Promise(resolve => setTimeout(resolve, 500)); 39 | 40 | // Return a processed response 41 | return `[Supergateway Mock] Processed: ${input.substring(0, 30)}...`; 42 | } 43 | 44 | /** 45 | * Stream a request response 46 | * @param input Input text to process 47 | * @param options Processing options 48 | * @returns Readable stream with response chunks 49 | */ 50 | async stream(input: string, options?: Record<string, any>): Promise<ReadableStream> { 51 | console.error('[Supergateway] Streaming request'); 52 | console.error('[Supergateway] Input:', input); 53 | console.error('[Supergateway] Options:', options); 54 | 55 | // Create a readable stream for the response 56 | return new ReadableStream({ 57 | start(controller) { 58 | // Simulate streaming with delays 59 | const encoder = new TextEncoder(); 60 | 61 | // Send chunks with delays 62 | setTimeout(() => { 63 | controller.enqueue(encoder.encode('[Supergateway] Stream started\n\n')); 64 | }, 100); 65 | 66 | setTimeout(() => { 67 | controller.enqueue(encoder.encode('Processing input...\n\n')); 68 | }, 500); 69 | 70 | setTimeout(() => { 71 | controller.enqueue(encoder.encode('Generating response...\n\n')); 72 | }, 1000); 73 | 74 | setTimeout(() => { 75 | controller.enqueue(encoder.encode('Completed processing\n\n')); 76 | controller.close(); 77 | }, 1500); 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * Close the client connection 84 | */ 85 | async close(): Promise<void> { 86 | console.error('[Supergateway] Connection closed'); 87 | } 88 | } ``` -------------------------------------------------------------------------------- /llms-install.md: -------------------------------------------------------------------------------- ```markdown 1 | # Smartlead MCP Server - Installation Guide for AI Assistants 2 | 3 | This guide helps AI assistants like Cline set up the Smartlead MCP Server. This server allows you to interact with Smartlead's email marketing API through the Model Context Protocol (MCP). 4 | 5 | ## Prerequisites 6 | 7 | - Node.js (v18 or higher) 8 | - npm (comes with Node.js) 9 | - A Smartlead API Key (user will need to provide this) 10 | - Optional: A License Key (free tier is available, paid tiers have more features) 11 | 12 | ## One-Click Installation Method 13 | 14 | The easiest way to install this MCP server is using the npx command: 15 | 16 | ```bash 17 | # Create a directory for configurations if desired (recommended) 18 | mkdir -p ~/.config/smartlead-mcp 19 | cd ~/.config/smartlead-mcp 20 | 21 | # Run the configuration helper 22 | npx smartlead-mcp-server config 23 | ``` 24 | 25 | This will guide you through an interactive setup process to configure the Smartlead API key and license key. The user will need to provide their Smartlead API key when prompted. 26 | 27 | ## Start the Server 28 | 29 | After installation and configuration, you can start the server in different modes depending on the intended use: 30 | 31 | ### For Claude and other AI assistants (STDIO mode): 32 | 33 | ```bash 34 | npx smartlead-mcp-server start 35 | ``` 36 | 37 | ### For n8n automation (SSE mode): 38 | 39 | ```bash 40 | npx smartlead-mcp-server sse 41 | ``` 42 | 43 | ## Integration with Cline 44 | 45 | To set up Cline to use this MCP server: 46 | 47 | 1. You'll need to add it to Cline's MCP settings: 48 | 49 | ```json 50 | { 51 | "mcpServers": { 52 | "smartlead": { 53 | "command": "npx", 54 | "args": ["smartlead-mcp-server", "start"], 55 | "env": { 56 | "SMARTLEAD_API_KEY": "user_provides_api_key_here", 57 | "JEAN_LICENSE_KEY": "optional_license_key_here" 58 | } 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | 2. Ask the user for their Smartlead API key and update the configuration accordingly. 65 | 66 | 3. For the license key, you can use "JEANPARTNER" for the free tier or ask the user if they have a paid license key. 67 | 68 | ## Features and Capabilities 69 | 70 | This MCP server provides tools for: 71 | 72 | - **FREE Tier (20+ tools):** 73 | - Campaign Management 74 | - Lead Management 75 | 76 | - **BASIC Tier (50+ tools):** 77 | - All FREE tier features 78 | - Campaign Statistics 79 | - Smart Delivery (Spam Tests, DNS Checks) 80 | - Webhooks 81 | - n8n Integration 82 | 83 | - **PREMIUM Tier (All tools):** 84 | - All BASIC tier features 85 | - Client Management 86 | - Smart Senders (Domain/Mailbox Purchase) 87 | - Advanced Features 88 | 89 | ## Troubleshooting 90 | 91 | - If you encounter issues with configuration, run: `npx smartlead-mcp-server config` 92 | - For license-related issues, use "JEANPARTNER" as the license key for free tier access 93 | - If you need more tools, direct the user to [https://sea-turtle-app-64etr.ondigitalocean.app/](https://sea-turtle-app-64etr.ondigitalocean.app/) to purchase a license 94 | 95 | ## License Information 96 | 97 | This MCP server is available under the MIT license. The number of machines allowed varies by license tier: 98 | - FREE tier: 1 machine 99 | - BASIC tier: 2 machines 100 | - PREMIUM tier: 5 machines ``` -------------------------------------------------------------------------------- /src/supergateway.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SuperGateway as MockSuperGateway } from './supergateway-mock.js'; 2 | import { isFeatureEnabled, LicenseLevel } from './licensing/index.js'; 3 | 4 | /** 5 | * Interface for SupergateWay options 6 | */ 7 | export interface SuperGatewayOptions { 8 | apiKey?: string; 9 | [key: string]: any; 10 | } 11 | 12 | /** 13 | * Interface for SuperGateway API 14 | */ 15 | export interface SuperGateway { 16 | process(input: string, options?: Record<string, any>): Promise<string>; 17 | stream(input: string, options?: Record<string, any>): Promise<ReadableStream>; 18 | close(): Promise<void>; 19 | } 20 | 21 | /** 22 | * Try to dynamically import Supergateway package 23 | */ 24 | export async function tryImportSupergateway(): Promise<any> { 25 | try { 26 | // First try to import the real package 27 | return await import('supergateway'); 28 | } catch (error) { 29 | console.error(`Failed to import Supergateway: ${error instanceof Error ? error.message : String(error)}`); 30 | console.error('Falling back to mock Supergateway implementation'); 31 | 32 | // Return our mock implementation as a fallback 33 | return { 34 | SuperGateway: MockSuperGateway 35 | }; 36 | } 37 | } 38 | 39 | /** 40 | * Create a Supergateway instance 41 | */ 42 | export async function createSupergateway(apiKey?: string): Promise<SuperGateway | null> { 43 | try { 44 | // Check if the current license allows n8n integration 45 | const n8nIntegrationEnabled = await isFeatureEnabled('n8nIntegration'); 46 | 47 | if (!n8nIntegrationEnabled) { 48 | console.error('============================================================='); 49 | console.error('ERROR: Your license does not include n8n integration features'); 50 | console.error('This feature requires a Basic or Premium license subscription.'); 51 | console.error('Visit https://yourservice.com/pricing to upgrade your plan.'); 52 | console.error(''); 53 | console.error('For testing purposes, you can obtain a Basic license from'); 54 | console.error('https://yourservice.com/pricing'); 55 | console.error('============================================================='); 56 | return null; 57 | } 58 | 59 | // Try to dynamically import the Supergateway module 60 | const supergateModule = await tryImportSupergateway(); 61 | 62 | if (!supergateModule) { 63 | console.error('Supergateway module not found.'); 64 | return null; 65 | } 66 | 67 | // Check if API key is provided 68 | if (!apiKey) { 69 | console.error('SUPERGATEWAY_API_KEY not provided'); 70 | console.error('Please set the SUPERGATEWAY_API_KEY environment variable in your .env file.'); 71 | return null; 72 | } 73 | 74 | // Create Supergateway instance 75 | const { SuperGateway } = supergateModule; 76 | const gateway = new SuperGateway({ 77 | apiKey, 78 | // Additional configuration options can be added here 79 | }); 80 | 81 | console.error('Supergateway initialized successfully'); 82 | return gateway; 83 | } catch (error) { 84 | console.error(`Error initializing Supergateway: ${error instanceof Error ? error.message : String(error)}`); 85 | return null; 86 | } 87 | } ``` -------------------------------------------------------------------------------- /DEVELOPER_ONBOARDING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Smartlead MCP Server - Developer Guide 2 | 3 | ## Overview 4 | 5 | Smartlead MCP Server provides an organized interface to the Smartlead API using the Model Context Protocol (MCP), enabling AI assistants and automation tools to manage email marketing campaigns. 6 | 7 | ## Prerequisites 8 | 9 | - Node.js (v18+) 10 | - A Smartlead API Key 11 | 12 | ## Quick Start Options 13 | 14 | ### Option 1: Use npx (Recommended) 15 | 16 | ```bash 17 | # For Claude 18 | npx smartlead-mcp-server start 19 | 20 | # For n8n 21 | npx smartlead-mcp-server sse 22 | ``` 23 | 24 | ### Option 2: Development Setup 25 | 26 | ```bash 27 | # Clone and install 28 | git clone https://github.com/jean-technologies/smartlead-mcp-server-local.git 29 | cd smartlead-mcp-server-local 30 | npm install 31 | 32 | # Configure and build 33 | cp .env.example .env # Then edit with your API key 34 | npm run build 35 | 36 | # Run 37 | npm start # For Claude 38 | # OR 39 | npm run start:sse # For n8n 40 | ``` 41 | 42 | ## Two Integration Pathways 43 | 44 | ### 1. Claude Integration 45 | 46 | Add to Claude settings JSON: 47 | ```json 48 | { 49 | "mcp": { 50 | "name": "smartlead", 51 | "command": "npx", 52 | "args": ["smartlead-mcp-server", "start"], 53 | "env": { 54 | "SMARTLEAD_API_KEY": "your_api_key_here" 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ### 2. n8n Integration 61 | 62 | #### Local n8n: 63 | 1. Run server: `npx smartlead-mcp-server sse` 64 | 2. Configure n8n MCP node with: 65 | - SSE URL: `http://localhost:3000/sse` 66 | - Message URL: `http://localhost:3000/message` 67 | 68 | #### n8n Cloud: 69 | 1. Run server: `npx smartlead-mcp-server sse` 70 | 2. Create tunnel: `npx ngrok http 3000` 71 | 3. Use ngrok URL in n8n MCP node 72 | 73 | ## Available Features 74 | 75 | All features are now enabled by default, including: 76 | - Campaign & Lead Management 77 | - Statistics and Analytics 78 | - Smart Delivery & Webhooks 79 | - n8n Integration 80 | - Client Management 81 | - Smart Senders 82 | - Download Tracking and Analytics 83 | 84 | ## Download Tracking System 85 | 86 | ### Implementation Details 87 | 88 | The download tracking system stores records in `~/.smartlead-mcp/downloads.json` with the following structure: 89 | 90 | ```json 91 | { 92 | "downloads": [ 93 | { 94 | "id": "unique-download-id", 95 | "timestamp": "2023-06-15T12:34:56.789Z", 96 | "campaignId": 12345, 97 | "downloadType": "analytics", 98 | "format": "json", 99 | "userId": "optional-user-id", 100 | "machineId": "machine-identifier" 101 | } 102 | ] 103 | } 104 | ``` 105 | 106 | ### Available Tools 107 | 108 | 1. **Download Campaign Data**: `smartlead_download_campaign_data` 109 | - Fetches data from Smartlead API 110 | - Converts to CSV if requested 111 | - Automatically tracks download details 112 | 113 | 2. **View Download Statistics**: `smartlead_view_download_statistics` 114 | - Filter by time period (all, today, week, month) 115 | - Group by various dimensions (type, format, campaign, date) 116 | - Get usage insights and recent downloads 117 | 118 | ## Troubleshooting 119 | 120 | ### Common Solutions 121 | - Configure settings: `npx smartlead-mcp-server config` 122 | - Set API key directly: `npx smartlead-mcp-server sse --api-key=YOUR_API_KEY` 123 | - Debug mode: `DEBUG=smartlead:* npx smartlead-mcp-server start` 124 | 125 | ## Resources 126 | 127 | - [Smartlead API Docs](https://docs.smartlead.ai) 128 | - [MCP Specification](https://github.com/modelcontextprotocol/spec) 129 | - [n8n Integration Guide](https://docs.n8n.io) ``` -------------------------------------------------------------------------------- /src/handlers/clientManagement.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AxiosInstance } from 'axios'; 2 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; 3 | import { 4 | isAddClientParams, 5 | isFetchAllClientsParams 6 | } from '../types/clientManagement.js'; 7 | 8 | // SmartLead API base URL 9 | const SMARTLEAD_API_URL = 'https://server.smartlead.ai/api/v1'; 10 | 11 | // Handler for Client Management-related tools 12 | export async function handleClientManagementTool( 13 | toolName: string, 14 | args: unknown, 15 | apiClient: AxiosInstance, 16 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 17 | ) { 18 | switch (toolName) { 19 | case 'smartlead_add_client': { 20 | return handleAddClient(args, apiClient, withRetry); 21 | } 22 | case 'smartlead_fetch_all_clients': { 23 | return handleFetchAllClients(args, apiClient, withRetry); 24 | } 25 | default: 26 | throw new Error(`Unknown Client Management tool: ${toolName}`); 27 | } 28 | } 29 | 30 | // Create a modified client for SmartLead API with the correct base URL 31 | function createSmartLeadClient(apiClient: AxiosInstance) { 32 | return { 33 | get: (url: string, config?: any) => 34 | apiClient.get(`${SMARTLEAD_API_URL}${url}`, config), 35 | post: (url: string, data?: any, config?: any) => 36 | apiClient.post(`${SMARTLEAD_API_URL}${url}`, data, config), 37 | put: (url: string, data?: any, config?: any) => 38 | apiClient.put(`${SMARTLEAD_API_URL}${url}`, data, config), 39 | delete: (url: string, config?: any) => 40 | apiClient.delete(`${SMARTLEAD_API_URL}${url}`, config) 41 | }; 42 | } 43 | 44 | // Individual handlers for each tool 45 | async function handleAddClient( 46 | args: unknown, 47 | apiClient: AxiosInstance, 48 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 49 | ) { 50 | if (!isAddClientParams(args)) { 51 | throw new McpError( 52 | ErrorCode.InvalidParams, 53 | 'Invalid arguments for smartlead_add_client' 54 | ); 55 | } 56 | 57 | try { 58 | const smartLeadClient = createSmartLeadClient(apiClient); 59 | 60 | const response = await withRetry( 61 | async () => smartLeadClient.post('/client/save', args), 62 | 'add client' 63 | ); 64 | 65 | return { 66 | content: [ 67 | { 68 | type: 'text', 69 | text: JSON.stringify(response.data, null, 2), 70 | }, 71 | ], 72 | isError: false, 73 | }; 74 | } catch (error: any) { 75 | return { 76 | content: [{ 77 | type: 'text', 78 | text: `API Error: ${error.response?.data?.message || error.message}` 79 | }], 80 | isError: true, 81 | }; 82 | } 83 | } 84 | 85 | async function handleFetchAllClients( 86 | args: unknown, 87 | apiClient: AxiosInstance, 88 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 89 | ) { 90 | if (!isFetchAllClientsParams(args)) { 91 | throw new McpError( 92 | ErrorCode.InvalidParams, 93 | 'Invalid arguments for smartlead_fetch_all_clients' 94 | ); 95 | } 96 | 97 | try { 98 | const smartLeadClient = createSmartLeadClient(apiClient); 99 | 100 | const response = await withRetry( 101 | async () => smartLeadClient.get('/client/'), 102 | 'fetch all clients' 103 | ); 104 | 105 | return { 106 | content: [ 107 | { 108 | type: 'text', 109 | text: JSON.stringify(response.data, null, 2), 110 | }, 111 | ], 112 | isError: false, 113 | }; 114 | } catch (error: any) { 115 | return { 116 | content: [{ 117 | type: 'text', 118 | text: `API Error: ${error.response?.data?.message || error.message}` 119 | }], 120 | isError: true, 121 | }; 122 | } 123 | } ``` -------------------------------------------------------------------------------- /src/registry/tool-registry.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool } from '../types/common.js'; 2 | import { isToolEnabled } from '../config/feature-config.js'; 3 | import { validateLicense, LicenseLevel } from '../licensing/index.js'; 4 | 5 | /** 6 | * Tool Registry manages the registration and querying of all available tools. 7 | * It provides a central place to register, filter, and retrieve tools by various criteria. 8 | */ 9 | export class ToolRegistry { 10 | private tools: Map<string, CategoryTool> = new Map(); 11 | private cachedEnabledTools: CategoryTool[] | null = null; 12 | private lastCacheTime = 0; 13 | private readonly CACHE_TTL = 60000; // 1 minute 14 | 15 | /** 16 | * Register a single tool in the registry 17 | */ 18 | register(tool: CategoryTool): void { 19 | this.tools.set(tool.name, tool); 20 | // Invalidate cache when tools change 21 | this.cachedEnabledTools = null; 22 | } 23 | 24 | /** 25 | * Register multiple tools at once 26 | */ 27 | registerMany(tools: CategoryTool[]): void { 28 | tools.forEach(tool => this.register(tool)); 29 | } 30 | 31 | /** 32 | * Get a tool by its name 33 | */ 34 | getByName(name: string): CategoryTool | undefined { 35 | return this.tools.get(name); 36 | } 37 | 38 | /** 39 | * Get all tools that belong to a specific category 40 | */ 41 | getByCategory(category: string): CategoryTool[] { 42 | return Array.from(this.tools.values()) 43 | .filter(tool => tool.category === category); 44 | } 45 | 46 | /** 47 | * Get all registered tools 48 | */ 49 | getAllTools(): CategoryTool[] { 50 | return Array.from(this.tools.values()); 51 | } 52 | 53 | /** 54 | * Get all tools that are enabled based on the license and configuration 55 | * This method now returns a subset based on the current license 56 | */ 57 | async getEnabledToolsAsync(): Promise<CategoryTool[]> { 58 | const now = Date.now(); 59 | 60 | // Use cache if available and not expired 61 | if (this.cachedEnabledTools && (now - this.lastCacheTime < this.CACHE_TTL)) { 62 | return this.cachedEnabledTools; 63 | } 64 | 65 | // Get license to check allowed categories 66 | const license = await validateLicense(); 67 | 68 | // Filter tools based on license-allowed categories 69 | const enabledTools = this.getAllTools().filter(tool => 70 | license.features.allowedCategories.includes(tool.category) 71 | ); 72 | 73 | // Cache results 74 | this.cachedEnabledTools = enabledTools; 75 | this.lastCacheTime = now; 76 | 77 | return enabledTools; 78 | } 79 | 80 | /** 81 | * Get enabled tools (synchronous version, falls back to configuration) 82 | * This is used for backward compatibility 83 | */ 84 | getEnabledTools(): CategoryTool[] { 85 | // If we have a cache, use it 86 | if (this.cachedEnabledTools) { 87 | return this.cachedEnabledTools; 88 | } 89 | 90 | // Otherwise fall back to config-based filtering 91 | // This uses the local configuration as a fallback 92 | return this.getAllTools().filter(tool => { 93 | try { 94 | return isToolEnabled(tool.name, tool.category); 95 | } catch (e) { 96 | // If async check fails, use default behavior 97 | return false; 98 | } 99 | }); 100 | } 101 | 102 | /** 103 | * Check if a tool with the given name exists in the registry 104 | */ 105 | hasToolWithName(name: string): boolean { 106 | return this.tools.has(name); 107 | } 108 | 109 | /** 110 | * Check if a tool belongs to the specified category 111 | */ 112 | isToolInCategory(name: string, category: string): boolean { 113 | const tool = this.getByName(name); 114 | return !!tool && tool.category === category; 115 | } 116 | } 117 | 118 | // Create a singleton instance 119 | export const toolRegistry = new ToolRegistry(); ``` -------------------------------------------------------------------------------- /src/n8n/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios from 'axios'; 2 | import { LicenseLevel, validateLicense, getFeatureToken } from '../licensing/index.js'; 3 | 4 | const N8N_CONFIG = { 5 | apiUrl: process.env.N8N_API_URL || 'https://n8n.yourservice.com/api/v1', 6 | }; 7 | 8 | /** 9 | * Get workflows from n8n 10 | * This is a premium feature that requires server-side validation 11 | */ 12 | export async function getWorkflows() { 13 | // First check if n8n integration is available based on license 14 | const licenseInfo = await validateLicense(); 15 | if (licenseInfo.level !== LicenseLevel.PREMIUM) { 16 | throw new Error( 17 | `N8n integration requires a Premium license. Your current license level is ${licenseInfo.level}. ` + 18 | 'Please upgrade to access this feature.' 19 | ); 20 | } 21 | 22 | // Get a feature token for server-side validation 23 | const featureToken = await getFeatureToken(); 24 | if (!featureToken) { 25 | throw new Error('Unable to validate premium feature access. Please try again later.'); 26 | } 27 | 28 | try { 29 | // Make the API call with the feature token 30 | const response = await axios.get(`${N8N_CONFIG.apiUrl}/workflows`, { 31 | headers: { 32 | 'Authorization': `Bearer ${process.env.JEAN_LICENSE_KEY}`, 33 | 'X-Feature-Token': featureToken.token 34 | } 35 | }); 36 | 37 | return response.data; 38 | } catch (error) { 39 | // Handle API errors 40 | if (axios.isAxiosError(error) && error.response) { 41 | // If server rejected the feature token 42 | if (error.response.status === 403) { 43 | throw new Error('Premium feature access denied. Please check your license status.'); 44 | } 45 | 46 | // Other API errors 47 | throw new Error(`Failed to fetch n8n workflows: ${error.response.data?.message || error.message}`); 48 | } 49 | 50 | // Generic error 51 | throw new Error(`Error accessing n8n integration: ${error instanceof Error ? error.message : String(error)}`); 52 | } 53 | } 54 | 55 | /** 56 | * Execute a workflow in n8n 57 | * This is a premium feature that requires server-side validation 58 | */ 59 | export async function executeWorkflow(workflowId: string, data: any) { 60 | // First check if n8n integration is available based on license 61 | const licenseInfo = await validateLicense(); 62 | if (licenseInfo.level !== LicenseLevel.PREMIUM) { 63 | throw new Error( 64 | `N8n integration requires a Premium license. Your current license level is ${licenseInfo.level}. ` + 65 | 'Please upgrade to access this feature.' 66 | ); 67 | } 68 | 69 | // Get a feature token for server-side validation 70 | const featureToken = await getFeatureToken(); 71 | if (!featureToken) { 72 | throw new Error('Unable to validate premium feature access. Please try again later.'); 73 | } 74 | 75 | try { 76 | // Make the API call with the feature token 77 | const response = await axios.post( 78 | `${N8N_CONFIG.apiUrl}/workflows/${workflowId}/execute`, 79 | data, 80 | { 81 | headers: { 82 | 'Authorization': `Bearer ${process.env.JEAN_LICENSE_KEY}`, 83 | 'X-Feature-Token': featureToken.token, 84 | 'Content-Type': 'application/json' 85 | } 86 | } 87 | ); 88 | 89 | return response.data; 90 | } catch (error) { 91 | // Handle API errors 92 | if (axios.isAxiosError(error) && error.response) { 93 | // If server rejected the feature token 94 | if (error.response.status === 403) { 95 | throw new Error('Premium feature access denied. Please check your license status.'); 96 | } 97 | 98 | // Other API errors 99 | throw new Error(`Failed to execute n8n workflow: ${error.response.data?.message || error.message}`); 100 | } 101 | 102 | // Generic error 103 | throw new Error(`Error accessing n8n integration: ${error instanceof Error ? error.message : String(error)}`); 104 | } 105 | } ``` -------------------------------------------------------------------------------- /src/types/webhooks.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type definitions for Webhooks functionality 2 | 3 | // Webhook event types 4 | export enum WebhookEventType { 5 | EMAIL_SENT = 'EMAIL_SENT', 6 | EMAIL_OPEN = 'EMAIL_OPEN', 7 | EMAIL_LINK_CLICK = 'EMAIL_LINK_CLICK', 8 | EMAIL_REPLY = 'EMAIL_REPLY', 9 | LEAD_UNSUBSCRIBED = 'LEAD_UNSUBSCRIBED', 10 | LEAD_CATEGORY_UPDATED = 'LEAD_CATEGORY_UPDATED' 11 | } 12 | 13 | // Fetch Webhooks By Campaign ID 14 | export interface FetchWebhooksByCampaignParams { 15 | campaign_id: string; 16 | } 17 | 18 | // Add / Update Campaign Webhook 19 | export interface UpsertCampaignWebhookParams { 20 | campaign_id: string; 21 | id?: number | null; // Set to null to create a new webhook 22 | name: string; 23 | webhook_url: string; 24 | event_types: WebhookEventType[]; 25 | categories?: string[]; 26 | } 27 | 28 | // Delete Campaign Webhook 29 | export interface DeleteCampaignWebhookParams { 30 | campaign_id: string; 31 | id: number; // Webhook ID to delete 32 | } 33 | 34 | // Get Webhooks Publish Summary [Private Beta] 35 | export interface GetWebhooksPublishSummaryParams { 36 | campaign_id: string; 37 | fromTime?: string; // ISO 8601 date-time string 38 | toTime?: string; // ISO 8601 date-time string 39 | } 40 | 41 | // Retrigger Failed Events [Private Beta] 42 | export interface RetriggerFailedEventsParams { 43 | campaign_id: string; 44 | fromTime: string; // ISO 8601 date-time string 45 | toTime: string; // ISO 8601 date-time string 46 | } 47 | 48 | // Type guards 49 | export function isFetchWebhooksByCampaignParams(args: unknown): args is FetchWebhooksByCampaignParams { 50 | return ( 51 | typeof args === 'object' && 52 | args !== null && 53 | 'campaign_id' in args && 54 | typeof (args as FetchWebhooksByCampaignParams).campaign_id === 'string' 55 | ); 56 | } 57 | 58 | export function isUpsertCampaignWebhookParams(args: unknown): args is UpsertCampaignWebhookParams { 59 | if (typeof args !== 'object' || args === null) return false; 60 | 61 | const params = args as Partial<UpsertCampaignWebhookParams>; 62 | 63 | return ( 64 | typeof params.campaign_id === 'string' && 65 | typeof params.name === 'string' && 66 | typeof params.webhook_url === 'string' && 67 | Array.isArray(params.event_types) && 68 | params.event_types.every(type => 69 | Object.values(WebhookEventType).includes(type as WebhookEventType) 70 | ) && 71 | (params.categories === undefined || 72 | (Array.isArray(params.categories) && 73 | params.categories.every(category => typeof category === 'string') 74 | ) 75 | ) && 76 | (params.id === undefined || params.id === null || typeof params.id === 'number') 77 | ); 78 | } 79 | 80 | export function isDeleteCampaignWebhookParams(args: unknown): args is DeleteCampaignWebhookParams { 81 | return ( 82 | typeof args === 'object' && 83 | args !== null && 84 | 'campaign_id' in args && 85 | typeof (args as DeleteCampaignWebhookParams).campaign_id === 'string' && 86 | 'id' in args && 87 | typeof (args as DeleteCampaignWebhookParams).id === 'number' 88 | ); 89 | } 90 | 91 | export function isGetWebhooksPublishSummaryParams(args: unknown): args is GetWebhooksPublishSummaryParams { 92 | if (typeof args !== 'object' || args === null) return false; 93 | 94 | const params = args as Partial<GetWebhooksPublishSummaryParams>; 95 | 96 | return ( 97 | typeof params.campaign_id === 'string' && 98 | (params.fromTime === undefined || typeof params.fromTime === 'string') && 99 | (params.toTime === undefined || typeof params.toTime === 'string') 100 | ); 101 | } 102 | 103 | export function isRetriggerFailedEventsParams(args: unknown): args is RetriggerFailedEventsParams { 104 | if (typeof args !== 'object' || args === null) return false; 105 | 106 | const params = args as Partial<RetriggerFailedEventsParams>; 107 | 108 | return ( 109 | typeof params.campaign_id === 'string' && 110 | typeof params.fromTime === 'string' && 111 | typeof params.toTime === 'string' 112 | ); 113 | } ``` -------------------------------------------------------------------------------- /src/types/email.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type declarations for Email Account parameters 2 | declare module '../types/email.js' { 3 | export function isListEmailAccountsParams(args: unknown): args is ListEmailAccountsParams; 4 | export function isAddEmailToCampaignParams(args: unknown): args is AddEmailToCampaignParams; 5 | export function isRemoveEmailFromCampaignParams(args: unknown): args is RemoveEmailFromCampaignParams; 6 | export function isFetchEmailAccountsParams(args: unknown): args is FetchEmailAccountsParams; 7 | export function isCreateEmailAccountParams(args: unknown): args is CreateEmailAccountParams; 8 | export function isUpdateEmailAccountParams(args: unknown): args is UpdateEmailAccountParams; 9 | export function isFetchEmailAccountByIdParams(args: unknown): args is FetchEmailAccountByIdParams; 10 | export function isUpdateEmailWarmupParams(args: unknown): args is UpdateEmailWarmupParams; 11 | export function isReconnectEmailAccountParams(args: unknown): args is ReconnectEmailAccountParams; 12 | export function isUpdateEmailAccountTagParams(args: unknown): args is UpdateEmailAccountTagParams; 13 | 14 | export interface ListEmailAccountsParams { 15 | campaign_id?: number; 16 | status?: string; 17 | limit?: number; 18 | offset?: number; 19 | } 20 | 21 | export interface AddEmailToCampaignParams { 22 | campaign_id: number; 23 | email_account_id: number; 24 | } 25 | 26 | export interface RemoveEmailFromCampaignParams { 27 | campaign_id: number; 28 | email_account_id: number; 29 | } 30 | 31 | export interface FetchEmailAccountsParams { 32 | status?: string; 33 | limit?: number; 34 | offset?: number; 35 | username?: string; 36 | client_id?: number; 37 | } 38 | 39 | export interface CreateEmailAccountParams { 40 | from_name: string; 41 | from_email: string; 42 | user_name: string; 43 | password: string; 44 | smtp_host: string; 45 | smtp_port: number; 46 | imap_host: string; 47 | imap_port: number; 48 | max_email_per_day?: number; 49 | custom_tracking_url?: string; 50 | bcc?: string; 51 | signature?: string; 52 | warmup_enabled?: boolean; 53 | total_warmup_per_day?: number; 54 | daily_rampup?: number; 55 | reply_rate_percentage?: number; 56 | client_id?: number; 57 | } 58 | 59 | export interface UpdateEmailAccountParams { 60 | email_account_id: number; 61 | max_email_per_day?: number; 62 | custom_tracking_url?: string; 63 | bcc?: string; 64 | signature?: string; 65 | client_id?: number | null; 66 | time_to_wait_in_mins?: number; 67 | } 68 | 69 | export interface FetchEmailAccountByIdParams { 70 | email_account_id: number; 71 | } 72 | 73 | export interface UpdateEmailWarmupParams { 74 | email_account_id: number; 75 | warmup_enabled: string; 76 | total_warmup_per_day?: number; 77 | daily_rampup?: number; 78 | reply_rate_percentage?: string; 79 | warmup_key_id?: string; 80 | } 81 | 82 | export interface ReconnectEmailAccountParams { 83 | email_account_id: number; 84 | connection_details?: { 85 | smtp_host?: string; 86 | smtp_port?: number; 87 | smtp_username?: string; 88 | smtp_password?: string; 89 | imap_host?: string; 90 | imap_port?: number; 91 | imap_username?: string; 92 | imap_password?: string; 93 | oauth_token?: string; 94 | }; 95 | } 96 | 97 | export interface UpdateEmailAccountTagParams { 98 | id: number; 99 | name: string; 100 | color: string; 101 | } 102 | 103 | export interface EmailAccount { 104 | id: number; 105 | email: string; 106 | name?: string; 107 | provider: string; 108 | status: string; 109 | created_at: string; 110 | updated_at: string; 111 | last_checked_at?: string; 112 | warmup_enabled: boolean; 113 | daily_limit?: number; 114 | tags?: string[]; 115 | } 116 | 117 | export interface EmailAccountResponse { 118 | success: boolean; 119 | data: EmailAccount; 120 | message?: string; 121 | } 122 | 123 | export interface EmailAccountListResponse { 124 | success: boolean; 125 | data: { 126 | accounts: EmailAccount[]; 127 | total: number; 128 | }; 129 | message?: string; 130 | } 131 | 132 | export interface EmailAccountActionResponse { 133 | success: boolean; 134 | message: string; 135 | } 136 | } ``` -------------------------------------------------------------------------------- /src/types/smartSenders.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type definitions for Smart Senders functionality 2 | 3 | // Get Vendors 4 | export interface GetVendorsParams { 5 | // This endpoint doesn't require specific parameters beyond the API key 6 | // which is handled at the API client level 7 | } 8 | 9 | // Search Domain 10 | export interface SearchDomainParams { 11 | domain_name: string; 12 | vendor_id: number; 13 | } 14 | 15 | // Mailbox details for auto-generate and order placement 16 | export interface MailboxDetail { 17 | first_name: string; 18 | last_name: string; 19 | profile_pic?: string; 20 | mailbox?: string; // Required only for place-order 21 | } 22 | 23 | // Domain with mailbox details for auto-generate 24 | export interface DomainWithMailboxes { 25 | domain_name: string; 26 | mailbox_details: MailboxDetail[]; 27 | } 28 | 29 | // Auto-generate Mailboxes 30 | export interface AutoGenerateMailboxesParams { 31 | vendor_id: number; 32 | domains: DomainWithMailboxes[]; 33 | } 34 | 35 | // Place order for mailboxes 36 | export interface PlaceOrderParams { 37 | vendor_id: number; 38 | forwarding_domain: string; 39 | domains: DomainWithMailboxes[]; 40 | } 41 | 42 | // Get Domain List 43 | export interface GetDomainListParams { 44 | // This endpoint doesn't require specific parameters beyond the API key 45 | // which is handled at the API client level 46 | } 47 | 48 | // Type guards 49 | export function isGetVendorsParams(args: unknown): args is GetVendorsParams { 50 | // Since this endpoint doesn't require specific parameters beyond the API key 51 | // Any object is valid 52 | return typeof args === 'object' && args !== null; 53 | } 54 | 55 | export function isSearchDomainParams(args: unknown): args is SearchDomainParams { 56 | if (typeof args !== 'object' || args === null) return false; 57 | 58 | const params = args as Partial<SearchDomainParams>; 59 | 60 | return ( 61 | typeof params.domain_name === 'string' && 62 | typeof params.vendor_id === 'number' 63 | ); 64 | } 65 | 66 | export function isMailboxDetail(obj: unknown): obj is MailboxDetail { 67 | if (typeof obj !== 'object' || obj === null) return false; 68 | 69 | const detail = obj as Partial<MailboxDetail>; 70 | 71 | return ( 72 | typeof detail.first_name === 'string' && 73 | typeof detail.last_name === 'string' && 74 | (detail.profile_pic === undefined || typeof detail.profile_pic === 'string') && 75 | (detail.mailbox === undefined || typeof detail.mailbox === 'string') 76 | ); 77 | } 78 | 79 | export function isDomainWithMailboxes(obj: unknown): obj is DomainWithMailboxes { 80 | if (typeof obj !== 'object' || obj === null) return false; 81 | 82 | const domain = obj as Partial<DomainWithMailboxes>; 83 | 84 | return ( 85 | typeof domain.domain_name === 'string' && 86 | Array.isArray(domain.mailbox_details) && 87 | domain.mailbox_details.every(detail => isMailboxDetail(detail)) 88 | ); 89 | } 90 | 91 | export function isAutoGenerateMailboxesParams(args: unknown): args is AutoGenerateMailboxesParams { 92 | if (typeof args !== 'object' || args === null) return false; 93 | 94 | const params = args as Partial<AutoGenerateMailboxesParams>; 95 | 96 | return ( 97 | typeof params.vendor_id === 'number' && 98 | Array.isArray(params.domains) && 99 | params.domains.every(domain => isDomainWithMailboxes(domain)) 100 | ); 101 | } 102 | 103 | export function isPlaceOrderParams(args: unknown): args is PlaceOrderParams { 104 | if (typeof args !== 'object' || args === null) return false; 105 | 106 | const params = args as Partial<PlaceOrderParams>; 107 | 108 | // Check if domains have mailbox property in mailbox_details 109 | const domainsHaveMailboxes = Array.isArray(params.domains) && 110 | params.domains.every(domain => 111 | isDomainWithMailboxes(domain) && 112 | domain.mailbox_details.every(detail => typeof detail.mailbox === 'string') 113 | ); 114 | 115 | return ( 116 | typeof params.vendor_id === 'number' && 117 | typeof params.forwarding_domain === 'string' && 118 | domainsHaveMailboxes 119 | ); 120 | } 121 | 122 | export function isGetDomainListParams(args: unknown): args is GetDomainListParams { 123 | // Since this endpoint doesn't require specific parameters beyond the API key 124 | // Any object is valid 125 | return typeof args === 'object' && args !== null; 126 | } ``` -------------------------------------------------------------------------------- /src/tools/webhooks.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from '../types/common.js'; 2 | import { WebhookEventType } from '../types/webhooks.js'; 3 | 4 | // Webhook Tools 5 | 6 | export const FETCH_WEBHOOKS_BY_CAMPAIGN_TOOL: CategoryTool = { 7 | name: 'smartlead_fetch_webhooks_by_campaign', 8 | description: 'Fetch all the webhooks associated with a campaign using the campaign ID.', 9 | category: ToolCategory.WEBHOOKS, 10 | inputSchema: { 11 | type: 'object', 12 | properties: { 13 | campaign_id: { 14 | type: 'string', 15 | description: 'ID of the campaign to fetch webhooks for', 16 | }, 17 | }, 18 | required: ['campaign_id'], 19 | }, 20 | }; 21 | 22 | export const UPSERT_CAMPAIGN_WEBHOOK_TOOL: CategoryTool = { 23 | name: 'smartlead_upsert_campaign_webhook', 24 | description: 'Add or update a webhook for a specific campaign.', 25 | category: ToolCategory.WEBHOOKS, 26 | inputSchema: { 27 | type: 'object', 28 | properties: { 29 | campaign_id: { 30 | type: 'string', 31 | description: 'ID of the campaign to add/update webhook for', 32 | }, 33 | id: { 34 | type: ['integer', 'null'], 35 | description: 'ID of the webhook to update. Set to null to create a new webhook.', 36 | }, 37 | name: { 38 | type: 'string', 39 | description: 'Name for the webhook', 40 | }, 41 | webhook_url: { 42 | type: 'string', 43 | description: 'URL to call when the webhook event occurs', 44 | }, 45 | event_types: { 46 | type: 'array', 47 | items: { 48 | type: 'string', 49 | enum: Object.values(WebhookEventType), 50 | }, 51 | description: `Types of events to trigger the webhook. Options: ${Object.values(WebhookEventType).join(', ')}`, 52 | }, 53 | categories: { 54 | type: 'array', 55 | items: { type: 'string' }, 56 | description: 'Categories for filtering webhook events (e.g. ["Interested", "NotInterested"])', 57 | }, 58 | }, 59 | required: ['campaign_id', 'name', 'webhook_url', 'event_types'], 60 | }, 61 | }; 62 | 63 | export const DELETE_CAMPAIGN_WEBHOOK_TOOL: CategoryTool = { 64 | name: 'smartlead_delete_campaign_webhook', 65 | description: 'Delete a specific webhook from a campaign.', 66 | category: ToolCategory.WEBHOOKS, 67 | inputSchema: { 68 | type: 'object', 69 | properties: { 70 | campaign_id: { 71 | type: 'string', 72 | description: 'ID of the campaign containing the webhook', 73 | }, 74 | id: { 75 | type: 'integer', 76 | description: 'ID of the webhook to delete', 77 | }, 78 | }, 79 | required: ['campaign_id', 'id'], 80 | }, 81 | }; 82 | 83 | export const GET_WEBHOOKS_PUBLISH_SUMMARY_TOOL: CategoryTool = { 84 | name: 'smartlead_get_webhooks_publish_summary', 85 | description: 'Get a summary of webhook publish events (Private Beta feature).', 86 | category: ToolCategory.WEBHOOKS, 87 | inputSchema: { 88 | type: 'object', 89 | properties: { 90 | campaign_id: { 91 | type: 'string', 92 | description: 'ID of the campaign to get webhook publish summary for', 93 | }, 94 | fromTime: { 95 | type: 'string', 96 | description: 'Start date/time in ISO 8601 format (e.g. 2025-03-21T00:00:00Z)', 97 | }, 98 | toTime: { 99 | type: 'string', 100 | description: 'End date/time in ISO 8601 format (e.g. 2025-04-04T23:59:59Z)', 101 | }, 102 | }, 103 | required: ['campaign_id'], 104 | }, 105 | }; 106 | 107 | export const RETRIGGER_FAILED_EVENTS_TOOL: CategoryTool = { 108 | name: 'smartlead_retrigger_failed_events', 109 | description: 'Retrigger failed webhook events (Private Beta feature).', 110 | category: ToolCategory.WEBHOOKS, 111 | inputSchema: { 112 | type: 'object', 113 | properties: { 114 | campaign_id: { 115 | type: 'string', 116 | description: 'ID of the campaign to retrigger failed webhook events for', 117 | }, 118 | fromTime: { 119 | type: 'string', 120 | description: 'Start date/time in ISO 8601 format (e.g. 2025-03-21T00:00:00Z)', 121 | }, 122 | toTime: { 123 | type: 'string', 124 | description: 'End date/time in ISO 8601 format (e.g. 2025-04-04T23:59:59Z)', 125 | }, 126 | }, 127 | required: ['campaign_id', 'fromTime', 'toTime'], 128 | }, 129 | }; 130 | 131 | // Export all tools as an array for easy registration 132 | export const webhookTools = [ 133 | FETCH_WEBHOOKS_BY_CAMPAIGN_TOOL, 134 | UPSERT_CAMPAIGN_WEBHOOK_TOOL, 135 | DELETE_CAMPAIGN_WEBHOOK_TOOL, 136 | GET_WEBHOOKS_PUBLISH_SUMMARY_TOOL, 137 | RETRIGGER_FAILED_EVENTS_TOOL, 138 | ]; ``` -------------------------------------------------------------------------------- /src/utils/download-tracker.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | 6 | // Interface for download tracking data 7 | export interface DownloadRecord { 8 | id: string; 9 | timestamp: string; 10 | campaignId: number; 11 | downloadType: string; 12 | format: string; 13 | userId?: string; 14 | machineId: string; 15 | ipAddress?: string; 16 | } 17 | 18 | // Path to store download records 19 | const DOWNLOAD_LOG_PATH = process.env.DOWNLOAD_LOG_PATH || path.join(os.homedir(), '.smartlead-mcp', 'downloads.json'); 20 | 21 | // Ensure directory exists 22 | const ensureDirectoryExists = (filePath: string) => { 23 | const dirname = path.dirname(filePath); 24 | if (!fs.existsSync(dirname)) { 25 | fs.mkdirSync(dirname, { recursive: true }); 26 | } 27 | }; 28 | 29 | // Initialize the download log file if it doesn't exist 30 | const initializeLogFile = () => { 31 | ensureDirectoryExists(DOWNLOAD_LOG_PATH); 32 | if (!fs.existsSync(DOWNLOAD_LOG_PATH)) { 33 | fs.writeFileSync(DOWNLOAD_LOG_PATH, JSON.stringify({ downloads: [] }, null, 2)); 34 | console.log(`Created download tracking file at: ${DOWNLOAD_LOG_PATH}`); 35 | } 36 | }; 37 | 38 | // Get a unique machine identifier 39 | const getMachineId = (): string => { 40 | try { 41 | const os = process.platform; 42 | const cpus = process.env.NUMBER_OF_PROCESSORS || ''; 43 | const username = process.env.USER || process.env.USERNAME || ''; 44 | const hostname = process.env.HOSTNAME || ''; 45 | 46 | // Create a simple hash of these values 47 | const combinedString = `${os}-${cpus}-${username}-${hostname}`; 48 | let hash = 0; 49 | for (let i = 0; i < combinedString.length; i++) { 50 | hash = ((hash << 5) - hash) + combinedString.charCodeAt(i); 51 | hash |= 0; // Convert to 32bit integer 52 | } 53 | 54 | return Math.abs(hash).toString(16); 55 | } catch (e) { 56 | // Fallback to a random ID if we can't get system info 57 | return Math.random().toString(36).substring(2, 15); 58 | } 59 | }; 60 | 61 | /** 62 | * Track a download event 63 | * @param campaignId The ID of the campaign being downloaded 64 | * @param downloadType The type of download (analytics, leads, etc.) 65 | * @param format The format of the download (json, csv) 66 | * @param userId Optional user identifier 67 | * @param ipAddress Optional IP address of the requester 68 | * @returns The unique ID of the download record 69 | */ 70 | export const trackDownload = ( 71 | campaignId: number, 72 | downloadType: string, 73 | format: string, 74 | userId?: string, 75 | ipAddress?: string 76 | ): string => { 77 | try { 78 | // Initialize log file if it doesn't exist 79 | initializeLogFile(); 80 | 81 | // Read existing records 82 | const data = JSON.parse(fs.readFileSync(DOWNLOAD_LOG_PATH, 'utf8')); 83 | 84 | // Create new download record 85 | const downloadId = uuidv4(); 86 | const downloadRecord: DownloadRecord = { 87 | id: downloadId, 88 | timestamp: new Date().toISOString(), 89 | campaignId, 90 | downloadType, 91 | format, 92 | userId, 93 | machineId: getMachineId(), 94 | ipAddress 95 | }; 96 | 97 | // Add to records and save 98 | data.downloads.push(downloadRecord); 99 | fs.writeFileSync(DOWNLOAD_LOG_PATH, JSON.stringify(data, null, 2)); 100 | 101 | console.log(`Tracked download: ${downloadId} for campaign ${campaignId}`); 102 | return downloadId; 103 | } catch (error) { 104 | console.error('Failed to track download:', error); 105 | return ''; 106 | } 107 | }; 108 | 109 | /** 110 | * Get all download records 111 | * @returns Array of download records 112 | */ 113 | export const getDownloadRecords = (): DownloadRecord[] => { 114 | try { 115 | initializeLogFile(); 116 | const data = JSON.parse(fs.readFileSync(DOWNLOAD_LOG_PATH, 'utf8')); 117 | return data.downloads || []; 118 | } catch (error) { 119 | console.error('Failed to get download records:', error); 120 | return []; 121 | } 122 | }; 123 | 124 | /** 125 | * Get download statistics 126 | * @returns Statistics about downloads 127 | */ 128 | export const getDownloadStats = () => { 129 | const records = getDownloadRecords(); 130 | 131 | // Count downloads by type 132 | const byType: Record<string, number> = {}; 133 | 134 | // Count downloads by format 135 | const byFormat: Record<string, number> = {}; 136 | 137 | // Count downloads by campaign 138 | const byCampaign: Record<number, number> = {}; 139 | 140 | // Count unique users 141 | const uniqueUsers = new Set<string>(); 142 | 143 | // Count by date (YYYY-MM-DD) 144 | const byDate: Record<string, number> = {}; 145 | 146 | records.forEach(record => { 147 | // Count by type 148 | byType[record.downloadType] = (byType[record.downloadType] || 0) + 1; 149 | 150 | // Count by format 151 | byFormat[record.format] = (byFormat[record.format] || 0) + 1; 152 | 153 | // Count by campaign 154 | byCampaign[record.campaignId] = (byCampaign[record.campaignId] || 0) + 1; 155 | 156 | // Track unique users (either by userId or machineId) 157 | if (record.userId) { 158 | uniqueUsers.add(record.userId); 159 | } else { 160 | uniqueUsers.add(record.machineId); 161 | } 162 | 163 | // Count by date 164 | const date = record.timestamp.split('T')[0]; 165 | byDate[date] = (byDate[date] || 0) + 1; 166 | }); 167 | 168 | return { 169 | totalDownloads: records.length, 170 | uniqueUsers: uniqueUsers.size, 171 | byType, 172 | byFormat, 173 | byCampaign, 174 | byDate 175 | }; 176 | }; ``` -------------------------------------------------------------------------------- /src/licensing/stripe-integration.js: -------------------------------------------------------------------------------- ```javascript 1 | import Stripe from 'stripe'; 2 | import axios from 'axios'; 3 | import { LicenseLevel } from './index.js'; 4 | 5 | // Initialize Stripe with your secret key 6 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || ''); 7 | 8 | // Your license server URL 9 | const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || 'https://api.yourservice.com/licensing'; 10 | 11 | /** 12 | * Check subscription status with Stripe 13 | * @param {string} customerId The Stripe customer ID 14 | * @returns {Promise<Object>} Subscription information 15 | */ 16 | export async function getSubscriptionStatus(customerId) { 17 | try { 18 | // Get all active subscriptions for this customer 19 | const subscriptions = await stripe.subscriptions.list({ 20 | customer: customerId, 21 | status: 'active', 22 | expand: ['data.plan.product'] 23 | }); 24 | 25 | if (subscriptions.data.length === 0) { 26 | return { 27 | active: false, 28 | level: LicenseLevel.FREE, 29 | message: 'No active subscription found' 30 | }; 31 | } 32 | 33 | // Get the highest tier subscription if there are multiple 34 | const subscription = subscriptions.data[0]; 35 | const product = subscription.items.data[0].plan.product; 36 | 37 | // Determine license level based on product ID or metadata 38 | let level = LicenseLevel.FREE; 39 | if (product.metadata.license_level) { 40 | level = product.metadata.license_level; 41 | } else { 42 | // Map product IDs to license levels if metadata not available 43 | const productMapping = { 44 | 'prod_basic123': LicenseLevel.BASIC, 45 | 'prod_premium456': LicenseLevel.PREMIUM 46 | }; 47 | level = productMapping[product.id] || LicenseLevel.FREE; 48 | } 49 | 50 | return { 51 | active: true, 52 | level, 53 | subscriptionId: subscription.id, 54 | currentPeriodEnd: new Date(subscription.current_period_end * 1000), 55 | cancelAtPeriodEnd: subscription.cancel_at_period_end, 56 | message: 'Subscription active' 57 | }; 58 | } catch (error) { 59 | console.error('Error checking subscription status:', error); 60 | return { 61 | active: false, 62 | level: LicenseLevel.FREE, 63 | message: 'Error checking subscription status' 64 | }; 65 | } 66 | } 67 | 68 | /** 69 | * Generate a new license key for a customer 70 | * @param {string} customerId The Stripe customer ID 71 | * @param {string} level The license level 72 | * @returns {Promise<Object>} The generated license information 73 | */ 74 | export async function generateLicense(customerId, level) { 75 | try { 76 | // Call your license server to generate a new license 77 | const response = await axios.post(`${LICENSE_SERVER_URL}/generate`, { 78 | customerId, 79 | level 80 | }, { 81 | headers: { 82 | 'Content-Type': 'application/json', 83 | 'X-API-Key': process.env.LICENSE_API_KEY 84 | } 85 | }); 86 | 87 | return response.data; 88 | } catch (error) { 89 | console.error('Error generating license:', error); 90 | throw new Error('Failed to generate license'); 91 | } 92 | } 93 | 94 | /** 95 | * Handle Stripe webhook events 96 | * @param {Object} event The Stripe webhook event 97 | * @returns {Promise<Object>} Result of webhook handling 98 | */ 99 | export async function handleStripeWebhook(event) { 100 | try { 101 | switch (event.type) { 102 | case 'customer.subscription.created': 103 | case 'customer.subscription.updated': { 104 | const subscription = event.data.object; 105 | const customerId = subscription.customer; 106 | 107 | // Get product information to determine license level 108 | const product = await stripe.products.retrieve( 109 | subscription.items.data[0].plan.product 110 | ); 111 | 112 | const level = product.metadata.license_level || LicenseLevel.BASIC; 113 | 114 | // Generate or update license 115 | const licenseInfo = await generateLicense(customerId, level); 116 | 117 | // Update customer metadata with license key 118 | await stripe.customers.update(customerId, { 119 | metadata: { 120 | license_key: licenseInfo.key, 121 | license_level: level 122 | } 123 | }); 124 | 125 | return { success: true, message: 'License updated', licenseInfo }; 126 | } 127 | 128 | case 'customer.subscription.deleted': { 129 | const subscription = event.data.object; 130 | const customerId = subscription.customer; 131 | 132 | // Downgrade to free tier or deactivate license 133 | const response = await axios.post(`${LICENSE_SERVER_URL}/downgrade`, { 134 | customerId 135 | }, { 136 | headers: { 137 | 'Content-Type': 'application/json', 138 | 'X-API-Key': process.env.LICENSE_API_KEY 139 | } 140 | }); 141 | 142 | // Update customer metadata 143 | await stripe.customers.update(customerId, { 144 | metadata: { 145 | license_level: LicenseLevel.FREE 146 | } 147 | }); 148 | 149 | return { success: true, message: 'License downgraded', response: response.data }; 150 | } 151 | 152 | default: 153 | return { success: true, message: 'Event ignored' }; 154 | } 155 | } catch (error) { 156 | console.error('Error handling webhook:', error); 157 | return { success: false, message: error.message }; 158 | } 159 | } ``` -------------------------------------------------------------------------------- /src/tools/smartSenders.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from '../types/common.js'; 2 | 3 | // Smart Senders Tools 4 | 5 | export const GET_VENDORS_TOOL: CategoryTool = { 6 | name: 'smartlead_get_vendors', 7 | description: 'Retrieve all active domain vendors with their corresponding IDs.', 8 | category: ToolCategory.SMART_SENDERS, 9 | inputSchema: { 10 | type: 'object', 11 | properties: { 12 | // This endpoint doesn't require specific parameters beyond the API key 13 | // which is handled at the API client level 14 | }, 15 | required: [], 16 | }, 17 | }; 18 | 19 | export const SEARCH_DOMAIN_TOOL: CategoryTool = { 20 | name: 'smartlead_search_domain', 21 | description: 'Search for available domains under $15 that match a given domain name pattern.', 22 | category: ToolCategory.SMART_SENDERS, 23 | inputSchema: { 24 | type: 'object', 25 | properties: { 26 | domain_name: { 27 | type: 'string', 28 | description: 'The domain name pattern you want to search for', 29 | }, 30 | vendor_id: { 31 | type: 'integer', 32 | description: 'ID of the vendor from whom you want to purchase the domain (use Get Vendors API to retrieve this ID)', 33 | }, 34 | }, 35 | required: ['domain_name', 'vendor_id'], 36 | }, 37 | }; 38 | 39 | export const AUTO_GENERATE_MAILBOXES_TOOL: CategoryTool = { 40 | name: 'smartlead_auto_generate_mailboxes', 41 | description: 'Auto-generate mailboxes based on the domain name and personal details provided.', 42 | category: ToolCategory.SMART_SENDERS, 43 | inputSchema: { 44 | type: 'object', 45 | properties: { 46 | vendor_id: { 47 | type: 'integer', 48 | description: 'ID of the vendor from whom you want to purchase the domains and mailboxes', 49 | }, 50 | domains: { 51 | type: 'array', 52 | items: { 53 | type: 'object', 54 | properties: { 55 | domain_name: { 56 | type: 'string', 57 | description: 'The domain name for which you want to generate mailboxes (e.g., example.com)', 58 | }, 59 | mailbox_details: { 60 | type: 'array', 61 | items: { 62 | type: 'object', 63 | properties: { 64 | first_name: { 65 | type: 'string', 66 | description: 'First name for the mailbox owner (should be more than 2 characters and without spaces)', 67 | }, 68 | last_name: { 69 | type: 'string', 70 | description: 'Last name for the mailbox owner (should be more than 2 characters and without spaces)', 71 | }, 72 | profile_pic: { 73 | type: 'string', 74 | description: 'URL or identifier for profile picture (optional)', 75 | }, 76 | }, 77 | required: ['first_name', 'last_name'], 78 | }, 79 | description: 'Details for each mailbox you want to generate', 80 | }, 81 | }, 82 | required: ['domain_name', 'mailbox_details'], 83 | }, 84 | description: 'List of domains and associated mailbox details', 85 | }, 86 | }, 87 | required: ['vendor_id', 'domains'], 88 | }, 89 | }; 90 | 91 | export const PLACE_ORDER_MAILBOXES_TOOL: CategoryTool = { 92 | name: 'smartlead_place_order_mailboxes', 93 | description: 'Confirm and place order for domains and mailboxes to be purchased.', 94 | category: ToolCategory.SMART_SENDERS, 95 | inputSchema: { 96 | type: 'object', 97 | properties: { 98 | vendor_id: { 99 | type: 'integer', 100 | description: 'ID of the vendor from whom you want to purchase the domains and mailboxes', 101 | }, 102 | forwarding_domain: { 103 | type: 'string', 104 | description: 'The domain to forward to when users access purchased domains', 105 | }, 106 | domains: { 107 | type: 'array', 108 | items: { 109 | type: 'object', 110 | properties: { 111 | domain_name: { 112 | type: 'string', 113 | description: 'The domain name you want to purchase', 114 | }, 115 | mailbox_details: { 116 | type: 'array', 117 | items: { 118 | type: 'object', 119 | properties: { 120 | mailbox: { 121 | type: 'string', 122 | description: 'The complete mailbox address (e.g., [email protected])', 123 | }, 124 | first_name: { 125 | type: 'string', 126 | description: 'First name for the mailbox owner', 127 | }, 128 | last_name: { 129 | type: 'string', 130 | description: 'Last name for the mailbox owner', 131 | }, 132 | profile_pic: { 133 | type: 'string', 134 | description: 'URL or identifier for profile picture (optional)', 135 | }, 136 | }, 137 | required: ['mailbox', 'first_name', 'last_name'], 138 | }, 139 | description: 'Details for each mailbox you want to purchase', 140 | }, 141 | }, 142 | required: ['domain_name', 'mailbox_details'], 143 | }, 144 | description: 'List of domains and associated mailbox details for purchase', 145 | }, 146 | }, 147 | required: ['vendor_id', 'forwarding_domain', 'domains'], 148 | }, 149 | }; 150 | 151 | export const GET_DOMAIN_LIST_TOOL: CategoryTool = { 152 | name: 'smartlead_get_domain_list', 153 | description: 'Retrieve a list of all domains purchased through SmartSenders.', 154 | category: ToolCategory.SMART_SENDERS, 155 | inputSchema: { 156 | type: 'object', 157 | properties: { 158 | // This endpoint doesn't require specific parameters beyond the API key 159 | // which is handled at the API client level 160 | }, 161 | required: [], 162 | }, 163 | }; 164 | 165 | // Export all tools as an array for easy registration 166 | export const smartSendersTools = [ 167 | GET_VENDORS_TOOL, 168 | SEARCH_DOMAIN_TOOL, 169 | AUTO_GENERATE_MAILBOXES_TOOL, 170 | PLACE_ORDER_MAILBOXES_TOOL, 171 | GET_DOMAIN_LIST_TOOL, 172 | ]; ``` -------------------------------------------------------------------------------- /src/tools/smartDelivery.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | // Tool schemas for Smart Delivery category 4 | export const regionWiseProviderIdsSchema: { 5 | name: string; 6 | description: string; 7 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 8 | }; 9 | 10 | export const createManualPlacementTestSchema: { 11 | name: string; 12 | description: string; 13 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 14 | }; 15 | 16 | export const createAutomatedPlacementTestSchema: { 17 | name: string; 18 | description: string; 19 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 20 | }; 21 | 22 | export const getSpamTestDetailsSchema: { 23 | name: string; 24 | description: string; 25 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 26 | }; 27 | 28 | export const deleteSmartDeliveryTestsInBulkSchema: { 29 | name: string; 30 | description: string; 31 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 32 | }; 33 | 34 | export const stopAutomatedSmartDeliveryTestSchema: { 35 | name: string; 36 | description: string; 37 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 38 | }; 39 | 40 | export const listAllTestsSchema: { 41 | name: string; 42 | description: string; 43 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 44 | }; 45 | 46 | export const providerWiseReportSchema: { 47 | name: string; 48 | description: string; 49 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 50 | }; 51 | 52 | export const geoWiseReportSchema: { 53 | name: string; 54 | description: string; 55 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 56 | }; 57 | 58 | export const senderAccountWiseReportSchema: { 59 | name: string; 60 | description: string; 61 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 62 | }; 63 | 64 | export const spamFilterReportSchema: { 65 | name: string; 66 | description: string; 67 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 68 | }; 69 | 70 | export const dkimDetailsSchema: { 71 | name: string; 72 | description: string; 73 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 74 | }; 75 | 76 | export const spfDetailsSchema: { 77 | name: string; 78 | description: string; 79 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 80 | }; 81 | 82 | export const rdnsReportSchema: { 83 | name: string; 84 | description: string; 85 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 86 | }; 87 | 88 | export const senderAccountListSchema: { 89 | name: string; 90 | description: string; 91 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 92 | }; 93 | 94 | export const blacklistsSchema: { 95 | name: string; 96 | description: string; 97 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 98 | }; 99 | 100 | export const spamTestEmailContentSchema: { 101 | name: string; 102 | description: string; 103 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 104 | }; 105 | 106 | export const spamTestIpBlacklistCountSchema: { 107 | name: string; 108 | description: string; 109 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 110 | }; 111 | 112 | export const emailReplyHeadersSchema: { 113 | name: string; 114 | description: string; 115 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 116 | }; 117 | 118 | export const scheduleHistoryForAutomatedTestsSchema: { 119 | name: string; 120 | description: string; 121 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 122 | }; 123 | 124 | export const ipDetailsSchema: { 125 | name: string; 126 | description: string; 127 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 128 | }; 129 | 130 | export const mailboxSummarySchema: { 131 | name: string; 132 | description: string; 133 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 134 | }; 135 | 136 | export const mailboxCountApiSchema: { 137 | name: string; 138 | description: string; 139 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 140 | }; 141 | 142 | export const getAllFoldersSchema: { 143 | name: string; 144 | description: string; 145 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 146 | }; 147 | 148 | export const createFoldersSchema: { 149 | name: string; 150 | description: string; 151 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 152 | }; 153 | 154 | export const getFolderByIdSchema: { 155 | name: string; 156 | description: string; 157 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 158 | }; 159 | 160 | export const deleteFolderSchema: { 161 | name: string; 162 | description: string; 163 | schema: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>; 164 | }; 165 | 166 | // Type declarations for Smart Delivery tools 167 | declare module './tools/smartDelivery.js' { 168 | import { CategoryTool } from '../types/common.js'; 169 | 170 | export const REGION_WISE_PROVIDER_IDS_TOOL: string; 171 | export const CREATE_MANUAL_PLACEMENT_TEST_TOOL: string; 172 | export const CREATE_AUTOMATED_PLACEMENT_TEST_TOOL: string; 173 | export const GET_SPAM_TEST_DETAILS_TOOL: string; 174 | export const DELETE_SMART_DELIVERY_TESTS_IN_BULK_TOOL: string; 175 | export const STOP_AUTOMATED_SMART_DELIVERY_TEST_TOOL: string; 176 | export const LIST_ALL_TESTS_TOOL: string; 177 | export const PROVIDER_WISE_REPORT_TOOL: string; 178 | export const GEO_WISE_REPORT_TOOL: string; 179 | export const SENDER_ACCOUNT_WISE_REPORT_TOOL: string; 180 | export const SPAM_FILTER_REPORT_TOOL: string; 181 | export const DKIM_DETAILS_TOOL: string; 182 | export const SPF_DETAILS_TOOL: string; 183 | export const RDNS_REPORT_TOOL: string; 184 | export const SENDER_ACCOUNT_LIST_TOOL: string; 185 | export const BLACKLISTS_TOOL: string; 186 | export const SPAM_TEST_EMAIL_CONTENT_TOOL: string; 187 | export const SPAM_TEST_IP_BLACKLIST_COUNT_TOOL: string; 188 | export const EMAIL_REPLY_HEADERS_TOOL: string; 189 | export const SCHEDULE_HISTORY_FOR_AUTOMATED_TESTS_TOOL: string; 190 | export const IP_DETAILS_TOOL: string; 191 | export const MAILBOX_SUMMARY_TOOL: string; 192 | export const MAILBOX_COUNT_API_TOOL: string; 193 | export const GET_ALL_FOLDERS_TOOL: string; 194 | export const CREATE_FOLDERS_TOOL: string; 195 | export const GET_FOLDER_BY_ID_TOOL: string; 196 | export const DELETE_FOLDER_TOOL: string; 197 | 198 | export const smartDeliveryTools: CategoryTool[]; 199 | } ``` -------------------------------------------------------------------------------- /src/tools/lead.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from '../types/common.js'; 2 | 3 | // Lead Management Tools 4 | export const LIST_LEADS_TOOL: CategoryTool = { 5 | name: 'smartlead_list_leads', 6 | description: 'List leads with optional filtering by campaign or status.', 7 | category: ToolCategory.LEAD_MANAGEMENT, 8 | inputSchema: { 9 | type: 'object', 10 | properties: { 11 | campaign_id: { 12 | type: 'number', 13 | description: 'Filter leads by campaign ID', 14 | }, 15 | status: { 16 | type: 'string', 17 | description: 'Filter leads by status (e.g., "active", "unsubscribed", "bounced")', 18 | }, 19 | limit: { 20 | type: 'number', 21 | description: 'Maximum number of leads to return', 22 | }, 23 | offset: { 24 | type: 'number', 25 | description: 'Offset for pagination', 26 | }, 27 | search: { 28 | type: 'string', 29 | description: 'Search term to filter leads', 30 | }, 31 | start_date: { 32 | type: 'string', 33 | description: 'Filter leads created after this date (YYYY-MM-DD format)', 34 | }, 35 | end_date: { 36 | type: 'string', 37 | description: 'Filter leads created before this date (YYYY-MM-DD format)', 38 | }, 39 | }, 40 | }, 41 | }; 42 | 43 | export const GET_LEAD_TOOL: CategoryTool = { 44 | name: 'smartlead_get_lead', 45 | description: 'Get details of a specific lead by ID.', 46 | category: ToolCategory.LEAD_MANAGEMENT, 47 | inputSchema: { 48 | type: 'object', 49 | properties: { 50 | lead_id: { 51 | type: 'number', 52 | description: 'ID of the lead to retrieve', 53 | }, 54 | }, 55 | required: ['lead_id'], 56 | }, 57 | }; 58 | 59 | export const ADD_LEAD_TO_CAMPAIGN_TOOL: CategoryTool = { 60 | name: 'smartlead_add_lead_to_campaign', 61 | description: 'Add a new lead to a campaign.', 62 | category: ToolCategory.LEAD_MANAGEMENT, 63 | inputSchema: { 64 | type: 'object', 65 | properties: { 66 | campaign_id: { 67 | type: 'number', 68 | description: 'ID of the campaign to add the lead to', 69 | }, 70 | email: { 71 | type: 'string', 72 | description: 'Email address of the lead', 73 | }, 74 | first_name: { 75 | type: 'string', 76 | description: 'First name of the lead', 77 | }, 78 | last_name: { 79 | type: 'string', 80 | description: 'Last name of the lead', 81 | }, 82 | company: { 83 | type: 'string', 84 | description: 'Company of the lead', 85 | }, 86 | title: { 87 | type: 'string', 88 | description: 'Job title of the lead', 89 | }, 90 | phone: { 91 | type: 'string', 92 | description: 'Phone number of the lead', 93 | }, 94 | custom_fields: { 95 | type: 'object', 96 | description: 'Custom fields for the lead', 97 | }, 98 | }, 99 | required: ['campaign_id', 'email'], 100 | }, 101 | }; 102 | 103 | export const UPDATE_LEAD_TOOL: CategoryTool = { 104 | name: 'smartlead_update_lead', 105 | description: 'Update an existing lead\'s information.', 106 | category: ToolCategory.LEAD_MANAGEMENT, 107 | inputSchema: { 108 | type: 'object', 109 | properties: { 110 | lead_id: { 111 | type: 'number', 112 | description: 'ID of the lead to update', 113 | }, 114 | email: { 115 | type: 'string', 116 | description: 'New email address for the lead', 117 | }, 118 | first_name: { 119 | type: 'string', 120 | description: 'New first name for the lead', 121 | }, 122 | last_name: { 123 | type: 'string', 124 | description: 'New last name for the lead', 125 | }, 126 | company: { 127 | type: 'string', 128 | description: 'New company for the lead', 129 | }, 130 | title: { 131 | type: 'string', 132 | description: 'New job title for the lead', 133 | }, 134 | phone: { 135 | type: 'string', 136 | description: 'New phone number for the lead', 137 | }, 138 | custom_fields: { 139 | type: 'object', 140 | description: 'Updated custom fields for the lead', 141 | }, 142 | }, 143 | required: ['lead_id'], 144 | }, 145 | }; 146 | 147 | export const UPDATE_LEAD_STATUS_TOOL: CategoryTool = { 148 | name: 'smartlead_update_lead_status', 149 | description: 'Update a lead\'s status.', 150 | category: ToolCategory.LEAD_MANAGEMENT, 151 | inputSchema: { 152 | type: 'object', 153 | properties: { 154 | lead_id: { 155 | type: 'number', 156 | description: 'ID of the lead to update', 157 | }, 158 | status: { 159 | type: 'string', 160 | description: 'New status for the lead', 161 | }, 162 | }, 163 | required: ['lead_id', 'status'], 164 | }, 165 | }; 166 | 167 | export const BULK_IMPORT_LEADS_TOOL: CategoryTool = { 168 | name: 'smartlead_bulk_import_leads', 169 | description: 'Import multiple leads into a campaign at once.', 170 | category: ToolCategory.LEAD_MANAGEMENT, 171 | inputSchema: { 172 | type: 'object', 173 | properties: { 174 | campaign_id: { 175 | type: 'number', 176 | description: 'ID of the campaign to add the leads to', 177 | }, 178 | leads: { 179 | type: 'array', 180 | items: { 181 | type: 'object', 182 | properties: { 183 | email: { 184 | type: 'string', 185 | description: 'Email address of the lead', 186 | }, 187 | first_name: { 188 | type: 'string', 189 | description: 'First name of the lead', 190 | }, 191 | last_name: { 192 | type: 'string', 193 | description: 'Last name of the lead', 194 | }, 195 | company: { 196 | type: 'string', 197 | description: 'Company of the lead', 198 | }, 199 | title: { 200 | type: 'string', 201 | description: 'Job title of the lead', 202 | }, 203 | phone: { 204 | type: 'string', 205 | description: 'Phone number of the lead', 206 | }, 207 | custom_fields: { 208 | type: 'object', 209 | description: 'Custom fields for the lead', 210 | }, 211 | }, 212 | required: ['email'], 213 | }, 214 | description: 'Array of leads to import', 215 | }, 216 | }, 217 | required: ['campaign_id', 'leads'], 218 | }, 219 | }; 220 | 221 | export const DELETE_LEAD_TOOL: CategoryTool = { 222 | name: 'smartlead_delete_lead', 223 | description: 'Delete a lead permanently.', 224 | category: ToolCategory.LEAD_MANAGEMENT, 225 | inputSchema: { 226 | type: 'object', 227 | properties: { 228 | lead_id: { 229 | type: 'number', 230 | description: 'ID of the lead to delete', 231 | }, 232 | }, 233 | required: ['lead_id'], 234 | }, 235 | }; 236 | 237 | // Export an array of all lead management tools for registration 238 | export const leadTools = [ 239 | LIST_LEADS_TOOL, 240 | GET_LEAD_TOOL, 241 | ADD_LEAD_TO_CAMPAIGN_TOOL, 242 | UPDATE_LEAD_TOOL, 243 | UPDATE_LEAD_STATUS_TOOL, 244 | BULK_IMPORT_LEADS_TOOL, 245 | DELETE_LEAD_TOOL, 246 | ]; ``` -------------------------------------------------------------------------------- /src/handlers/smartSenders.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AxiosInstance } from 'axios'; 2 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; 3 | import { 4 | isGetVendorsParams, 5 | isSearchDomainParams, 6 | isAutoGenerateMailboxesParams, 7 | isPlaceOrderParams, 8 | isGetDomainListParams 9 | } from '../types/smartSenders.js'; 10 | 11 | // Smart Senders API base URL - different from the main SmartLead API 12 | const SMART_SENDERS_API_URL = 'https://smart-senders.smartlead.ai/api/v1'; 13 | 14 | // Handler for Smart Senders-related tools 15 | export async function handleSmartSendersTool( 16 | toolName: string, 17 | args: unknown, 18 | apiClient: AxiosInstance, 19 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 20 | ) { 21 | switch (toolName) { 22 | case 'smartlead_get_vendors': { 23 | return handleGetVendors(args, apiClient, withRetry); 24 | } 25 | case 'smartlead_search_domain': { 26 | return handleSearchDomain(args, apiClient, withRetry); 27 | } 28 | case 'smartlead_auto_generate_mailboxes': { 29 | return handleAutoGenerateMailboxes(args, apiClient, withRetry); 30 | } 31 | case 'smartlead_place_order_mailboxes': { 32 | return handlePlaceOrderMailboxes(args, apiClient, withRetry); 33 | } 34 | case 'smartlead_get_domain_list': { 35 | return handleGetDomainList(args, apiClient, withRetry); 36 | } 37 | default: 38 | throw new Error(`Unknown Smart Senders tool: ${toolName}`); 39 | } 40 | } 41 | 42 | // Create a modified client for Smart Senders API with the correct base URL 43 | function createSmartSendersClient(apiClient: AxiosInstance) { 44 | return { 45 | get: (url: string, config?: any) => 46 | apiClient.get(`${SMART_SENDERS_API_URL}${url}`, config), 47 | post: (url: string, data?: any, config?: any) => 48 | apiClient.post(`${SMART_SENDERS_API_URL}${url}`, data, config), 49 | put: (url: string, data?: any, config?: any) => 50 | apiClient.put(`${SMART_SENDERS_API_URL}${url}`, data, config), 51 | delete: (url: string, config?: any) => 52 | apiClient.delete(`${SMART_SENDERS_API_URL}${url}`, config) 53 | }; 54 | } 55 | 56 | // Individual handlers for each tool 57 | async function handleGetVendors( 58 | args: unknown, 59 | apiClient: AxiosInstance, 60 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 61 | ) { 62 | if (!isGetVendorsParams(args)) { 63 | throw new McpError( 64 | ErrorCode.InvalidParams, 65 | 'Invalid arguments for smartlead_get_vendors' 66 | ); 67 | } 68 | 69 | try { 70 | const smartSendersClient = createSmartSendersClient(apiClient); 71 | 72 | const response = await withRetry( 73 | async () => smartSendersClient.get('/smart-senders/get-vendors'), 74 | 'get vendors' 75 | ); 76 | 77 | return { 78 | content: [ 79 | { 80 | type: 'text', 81 | text: JSON.stringify(response.data, null, 2), 82 | }, 83 | ], 84 | isError: false, 85 | }; 86 | } catch (error: any) { 87 | return { 88 | content: [{ 89 | type: 'text', 90 | text: `API Error: ${error.response?.data?.message || error.message}` 91 | }], 92 | isError: true, 93 | }; 94 | } 95 | } 96 | 97 | async function handleSearchDomain( 98 | args: unknown, 99 | apiClient: AxiosInstance, 100 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 101 | ) { 102 | if (!isSearchDomainParams(args)) { 103 | throw new McpError( 104 | ErrorCode.InvalidParams, 105 | 'Invalid arguments for smartlead_search_domain' 106 | ); 107 | } 108 | 109 | try { 110 | const smartSendersClient = createSmartSendersClient(apiClient); 111 | const { domain_name, vendor_id } = args; 112 | 113 | const response = await withRetry( 114 | async () => smartSendersClient.get(`/smart-senders/search-domain?domain_name=${domain_name}&vendor_id=${vendor_id}`), 115 | 'search domain' 116 | ); 117 | 118 | return { 119 | content: [ 120 | { 121 | type: 'text', 122 | text: JSON.stringify(response.data, null, 2), 123 | }, 124 | ], 125 | isError: false, 126 | }; 127 | } catch (error: any) { 128 | return { 129 | content: [{ 130 | type: 'text', 131 | text: `API Error: ${error.response?.data?.message || error.message}` 132 | }], 133 | isError: true, 134 | }; 135 | } 136 | } 137 | 138 | async function handleAutoGenerateMailboxes( 139 | args: unknown, 140 | apiClient: AxiosInstance, 141 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 142 | ) { 143 | if (!isAutoGenerateMailboxesParams(args)) { 144 | throw new McpError( 145 | ErrorCode.InvalidParams, 146 | 'Invalid arguments for smartlead_auto_generate_mailboxes' 147 | ); 148 | } 149 | 150 | try { 151 | const smartSendersClient = createSmartSendersClient(apiClient); 152 | 153 | const response = await withRetry( 154 | async () => smartSendersClient.post('/smart-senders/auto-generate-mailboxes', args), 155 | 'auto-generate mailboxes' 156 | ); 157 | 158 | return { 159 | content: [ 160 | { 161 | type: 'text', 162 | text: JSON.stringify(response.data, null, 2), 163 | }, 164 | ], 165 | isError: false, 166 | }; 167 | } catch (error: any) { 168 | return { 169 | content: [{ 170 | type: 'text', 171 | text: `API Error: ${error.response?.data?.message || error.message}` 172 | }], 173 | isError: true, 174 | }; 175 | } 176 | } 177 | 178 | async function handlePlaceOrderMailboxes( 179 | args: unknown, 180 | apiClient: AxiosInstance, 181 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 182 | ) { 183 | if (!isPlaceOrderParams(args)) { 184 | throw new McpError( 185 | ErrorCode.InvalidParams, 186 | 'Invalid arguments for smartlead_place_order_mailboxes' 187 | ); 188 | } 189 | 190 | try { 191 | const smartSendersClient = createSmartSendersClient(apiClient); 192 | 193 | const response = await withRetry( 194 | async () => smartSendersClient.post('/smart-senders/place-order', args), 195 | 'place order for mailboxes' 196 | ); 197 | 198 | return { 199 | content: [ 200 | { 201 | type: 'text', 202 | text: JSON.stringify(response.data, null, 2), 203 | }, 204 | ], 205 | isError: false, 206 | }; 207 | } catch (error: any) { 208 | return { 209 | content: [{ 210 | type: 'text', 211 | text: `API Error: ${error.response?.data?.message || error.message}` 212 | }], 213 | isError: true, 214 | }; 215 | } 216 | } 217 | 218 | async function handleGetDomainList( 219 | args: unknown, 220 | apiClient: AxiosInstance, 221 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 222 | ) { 223 | if (!isGetDomainListParams(args)) { 224 | throw new McpError( 225 | ErrorCode.InvalidParams, 226 | 'Invalid arguments for smartlead_get_domain_list' 227 | ); 228 | } 229 | 230 | try { 231 | const smartSendersClient = createSmartSendersClient(apiClient); 232 | 233 | const response = await withRetry( 234 | async () => smartSendersClient.get('/smart-senders/get-domain-list'), 235 | 'get domain list' 236 | ); 237 | 238 | return { 239 | content: [ 240 | { 241 | type: 'text', 242 | text: JSON.stringify(response.data, null, 2), 243 | }, 244 | ], 245 | isError: false, 246 | }; 247 | } catch (error: any) { 248 | return { 249 | content: [{ 250 | type: 'text', 251 | text: `API Error: ${error.response?.data?.message || error.message}` 252 | }], 253 | isError: true, 254 | }; 255 | } 256 | } ``` -------------------------------------------------------------------------------- /src/types/email.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AxiosError } from 'axios'; 2 | 3 | // Type guards for Email Account parameters 4 | export function isListEmailAccountsParams(args: unknown): args is ListEmailAccountsParams { 5 | if (!args || typeof args !== 'object') return false; 6 | return true; 7 | } 8 | 9 | export function isAddEmailToCampaignParams(args: unknown): args is AddEmailToCampaignParams { 10 | if (!args || typeof args !== 'object') return false; 11 | const { campaign_id, email_account_id } = args as Partial<AddEmailToCampaignParams>; 12 | return ( 13 | typeof campaign_id === 'number' && 14 | typeof email_account_id === 'number' 15 | ); 16 | } 17 | 18 | export function isRemoveEmailFromCampaignParams(args: unknown): args is RemoveEmailFromCampaignParams { 19 | if (!args || typeof args !== 'object') return false; 20 | const { campaign_id, email_account_id } = args as Partial<RemoveEmailFromCampaignParams>; 21 | return ( 22 | typeof campaign_id === 'number' && 23 | typeof email_account_id === 'number' 24 | ); 25 | } 26 | 27 | export function isFetchEmailAccountsParams(args: unknown): args is FetchEmailAccountsParams { 28 | if (!args || typeof args !== 'object') return false; 29 | return true; 30 | } 31 | 32 | export function isCreateEmailAccountParams(args: unknown): args is CreateEmailAccountParams { 33 | if (!args || typeof args !== 'object') return false; 34 | const { from_name, from_email, user_name, password, smtp_host, smtp_port, imap_host, imap_port } = args as Partial<CreateEmailAccountParams>; 35 | return ( 36 | typeof from_name === 'string' && 37 | typeof from_email === 'string' && 38 | typeof user_name === 'string' && 39 | typeof password === 'string' && 40 | typeof smtp_host === 'string' && 41 | typeof smtp_port === 'number' && 42 | typeof imap_host === 'string' && 43 | typeof imap_port === 'number' 44 | ); 45 | } 46 | 47 | export function isUpdateEmailAccountParams(args: unknown): args is UpdateEmailAccountParams { 48 | if (!args || typeof args !== 'object') return false; 49 | const { email_account_id } = args as Partial<UpdateEmailAccountParams>; 50 | return typeof email_account_id === 'number'; 51 | } 52 | 53 | export function isFetchEmailAccountByIdParams(args: unknown): args is FetchEmailAccountByIdParams { 54 | if (!args || typeof args !== 'object') return false; 55 | const { email_account_id } = args as Partial<FetchEmailAccountByIdParams>; 56 | return typeof email_account_id === 'number'; 57 | } 58 | 59 | export function isUpdateEmailWarmupParams(args: unknown): args is UpdateEmailWarmupParams { 60 | if (!args || typeof args !== 'object') return false; 61 | const { email_account_id, warmup_enabled } = args as Partial<UpdateEmailWarmupParams>; 62 | return ( 63 | typeof email_account_id === 'number' && 64 | typeof warmup_enabled === 'string' 65 | ); 66 | } 67 | 68 | export function isReconnectEmailAccountParams(args: unknown): args is ReconnectEmailAccountParams { 69 | if (!args || typeof args !== 'object') return false; 70 | const { email_account_id } = args as Partial<ReconnectEmailAccountParams>; 71 | return typeof email_account_id === 'number'; 72 | } 73 | 74 | export function isUpdateEmailAccountTagParams(args: unknown): args is UpdateEmailAccountTagParams { 75 | if (!args || typeof args !== 'object') return false; 76 | const { id, name, color } = args as Partial<UpdateEmailAccountTagParams>; 77 | return ( 78 | typeof id === 'number' && 79 | typeof name === 'string' && 80 | typeof color === 'string' 81 | ); 82 | } 83 | 84 | // Interface definitions for Email Account parameters 85 | export interface ListEmailAccountsParams { 86 | campaign_id?: number; 87 | status?: string; 88 | limit?: number; 89 | offset?: number; 90 | } 91 | 92 | export interface AddEmailToCampaignParams { 93 | campaign_id: number; 94 | email_account_id: number; 95 | } 96 | 97 | export interface RemoveEmailFromCampaignParams { 98 | campaign_id: number; 99 | email_account_id: number; 100 | } 101 | 102 | export interface FetchEmailAccountsParams { 103 | status?: string; 104 | limit?: number; 105 | offset?: number; 106 | username?: string; 107 | client_id?: number; // Required Client ID according to the docs 108 | } 109 | 110 | export interface CreateEmailAccountParams { 111 | from_name: string; // User's name 112 | from_email: string; // User email 113 | user_name: string; // Username 114 | password: string; // User's password 115 | smtp_host: string; // Mail SMTP host 116 | smtp_port: number; // Mail SMTP port 117 | imap_host: string; // Imap host URL 118 | imap_port: number; // Imap port 119 | max_email_per_day?: number; // Max number of emails per day 120 | custom_tracking_url?: string; // Custom email tracking url 121 | bcc?: string; // Email BCC 122 | signature?: string; // Email signature 123 | warmup_enabled?: boolean; // Set true to enable warmup 124 | total_warmup_per_day?: number; // Total number of warmups per day 125 | daily_rampup?: number; // Daily rampup number 126 | reply_rate_percentage?: number; // Reply rate in percentage 127 | client_id?: number; // Client ID 128 | } 129 | 130 | export interface UpdateEmailAccountParams { 131 | email_account_id: number; // ID of the email to update 132 | max_email_per_day?: number; // Max number of emails per day 133 | custom_tracking_url?: string; // Custom email tracking URL 134 | bcc?: string; // Email BCC 135 | signature?: string; // Email signature 136 | client_id?: number | null; // Client ID. Set to null if not needed 137 | time_to_wait_in_mins?: number; // Minimum integer time (in minutes) to wait before sending next email 138 | } 139 | 140 | export interface FetchEmailAccountByIdParams { 141 | email_account_id: number; 142 | } 143 | 144 | export interface UpdateEmailWarmupParams { 145 | email_account_id: number; // Email account ID 146 | warmup_enabled: string; // Set false to disable warmup 147 | total_warmup_per_day?: number; // Total number of warmups in a day 148 | daily_rampup?: number; // Set this value to increase or decrease daily ramup in warmup emails 149 | reply_rate_percentage?: string; // Reply rate in percentage 150 | warmup_key_id?: string; // If passed will update the custom warmup-key identifier 151 | } 152 | 153 | export interface ReconnectEmailAccountParams { 154 | email_account_id: number; 155 | connection_details?: { 156 | smtp_host?: string; 157 | smtp_port?: number; 158 | smtp_username?: string; 159 | smtp_password?: string; 160 | imap_host?: string; 161 | imap_port?: number; 162 | imap_username?: string; 163 | imap_password?: string; 164 | oauth_token?: string; 165 | }; 166 | } 167 | 168 | export interface UpdateEmailAccountTagParams { 169 | id: number; // ID of the tag 170 | name: string; // Name of the tag 171 | color: string; // The color of the tag in HEX format 172 | } 173 | 174 | // Email Account response interfaces 175 | export interface EmailAccount { 176 | id: number; 177 | email: string; 178 | name?: string; 179 | provider: string; 180 | status: string; 181 | created_at: string; 182 | updated_at: string; 183 | last_checked_at?: string; 184 | warmup_enabled: boolean; 185 | daily_limit?: number; 186 | tags?: string[]; 187 | } 188 | 189 | export interface EmailAccountResponse { 190 | success: boolean; 191 | data: EmailAccount; 192 | message?: string; 193 | } 194 | 195 | export interface EmailAccountListResponse { 196 | success: boolean; 197 | data: { 198 | accounts: EmailAccount[]; 199 | total: number; 200 | }; 201 | message?: string; 202 | } 203 | 204 | export interface EmailAccountActionResponse { 205 | success: boolean; 206 | message: string; 207 | } ``` -------------------------------------------------------------------------------- /src/types/statistics.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from './common.js'; 2 | 3 | // Interface for fetching campaign statistics 4 | export interface CampaignStatisticsParams { 5 | campaign_id: number; 6 | offset?: number; 7 | limit?: number; 8 | email_sequence_number?: string; 9 | email_status?: string; 10 | sent_time_start_date?: string; 11 | sent_time_end_date?: string; 12 | } 13 | 14 | // Interface for fetching campaign statistics by date range 15 | export interface CampaignStatisticsByDateParams { 16 | campaign_id: number; 17 | start_date: string; 18 | end_date: string; 19 | } 20 | 21 | // Interface for fetching warmup stats by email account 22 | export interface WarmupStatsByEmailParams { 23 | email_account_id: number; 24 | } 25 | 26 | // Interface for fetching campaign top level analytics 27 | export interface CampaignTopLevelAnalyticsParams { 28 | campaign_id: number; 29 | } 30 | 31 | // Interface for fetching campaign top level analytics by date range 32 | export interface CampaignTopLevelAnalyticsByDateParams { 33 | campaign_id: number; 34 | start_date: string; 35 | end_date: string; 36 | } 37 | 38 | // Interface for fetching campaign lead statistics 39 | export interface CampaignLeadStatisticsParams { 40 | campaign_id: number; 41 | limit?: number; 42 | created_at_gt?: string; 43 | event_time_gt?: string; 44 | offset?: number; 45 | } 46 | 47 | // Interface for fetching campaign mailbox statistics 48 | export interface CampaignMailboxStatisticsParams { 49 | campaign_id: number; 50 | client_id?: string; 51 | offset?: number; 52 | limit?: number; 53 | start_date?: string; 54 | end_date?: string; 55 | timezone?: string; 56 | } 57 | 58 | // Interface for downloading campaign data 59 | export interface DownloadCampaignDataParams { 60 | campaign_id: number; 61 | download_type: string; 62 | format: string; 63 | user_id?: string; 64 | } 65 | 66 | // Interface for viewing download statistics 67 | export interface ViewDownloadStatisticsParams { 68 | time_period?: 'all' | 'today' | 'week' | 'month'; 69 | group_by?: 'type' | 'format' | 'campaign' | 'date'; 70 | } 71 | 72 | // Type guards for params validation 73 | 74 | export function isCampaignStatisticsParams(args: unknown): args is CampaignStatisticsParams { 75 | if (typeof args !== 'object' || args === null) { 76 | return false; 77 | } 78 | 79 | const params = args as CampaignStatisticsParams; 80 | 81 | if (typeof params.campaign_id !== 'number') { 82 | return false; 83 | } 84 | 85 | // Optional offset must be a number if present 86 | if (params.offset !== undefined && typeof params.offset !== 'number') { 87 | return false; 88 | } 89 | 90 | // Optional limit must be a number if present 91 | if (params.limit !== undefined && typeof params.limit !== 'number') { 92 | return false; 93 | } 94 | 95 | // Optional email_sequence_number must be a string if present 96 | if (params.email_sequence_number !== undefined && typeof params.email_sequence_number !== 'string') { 97 | return false; 98 | } 99 | 100 | // Optional email_status must be a string if present 101 | if (params.email_status !== undefined && typeof params.email_status !== 'string') { 102 | return false; 103 | } 104 | 105 | // Optional sent_time_start_date must be a string if present 106 | if (params.sent_time_start_date !== undefined && typeof params.sent_time_start_date !== 'string') { 107 | return false; 108 | } 109 | 110 | // Optional sent_time_end_date must be a string if present 111 | if (params.sent_time_end_date !== undefined && typeof params.sent_time_end_date !== 'string') { 112 | return false; 113 | } 114 | 115 | return true; 116 | } 117 | 118 | export function isCampaignStatisticsByDateParams(args: unknown): args is CampaignStatisticsByDateParams { 119 | if (typeof args !== 'object' || args === null) { 120 | return false; 121 | } 122 | 123 | const params = args as CampaignStatisticsByDateParams; 124 | 125 | return ( 126 | typeof params.campaign_id === 'number' && 127 | typeof params.start_date === 'string' && 128 | typeof params.end_date === 'string' 129 | ); 130 | } 131 | 132 | export function isWarmupStatsByEmailParams(args: unknown): args is WarmupStatsByEmailParams { 133 | if (typeof args !== 'object' || args === null) { 134 | return false; 135 | } 136 | 137 | const params = args as WarmupStatsByEmailParams; 138 | 139 | return typeof params.email_account_id === 'number'; 140 | } 141 | 142 | export function isCampaignTopLevelAnalyticsParams(args: unknown): args is CampaignTopLevelAnalyticsParams { 143 | if (typeof args !== 'object' || args === null) { 144 | return false; 145 | } 146 | 147 | const params = args as CampaignTopLevelAnalyticsParams; 148 | 149 | return typeof params.campaign_id === 'number'; 150 | } 151 | 152 | export function isCampaignTopLevelAnalyticsByDateParams(args: unknown): args is CampaignTopLevelAnalyticsByDateParams { 153 | if (typeof args !== 'object' || args === null) { 154 | return false; 155 | } 156 | 157 | const params = args as CampaignTopLevelAnalyticsByDateParams; 158 | 159 | return ( 160 | typeof params.campaign_id === 'number' && 161 | typeof params.start_date === 'string' && 162 | typeof params.end_date === 'string' 163 | ); 164 | } 165 | 166 | export function isCampaignLeadStatisticsParams(args: unknown): args is CampaignLeadStatisticsParams { 167 | if (typeof args !== 'object' || args === null) { 168 | return false; 169 | } 170 | 171 | const params = args as CampaignLeadStatisticsParams; 172 | 173 | if (typeof params.campaign_id !== 'number') { 174 | return false; 175 | } 176 | 177 | // Optional limit must be a string if present (it will be converted to number) 178 | if (params.limit !== undefined && typeof params.limit !== 'number') { 179 | return false; 180 | } 181 | 182 | // Optional created_at_gt must be a string if present 183 | if (params.created_at_gt !== undefined && typeof params.created_at_gt !== 'string') { 184 | return false; 185 | } 186 | 187 | // Optional event_time_gt must be a string if present 188 | if (params.event_time_gt !== undefined && typeof params.event_time_gt !== 'string') { 189 | return false; 190 | } 191 | 192 | // Optional offset must be a string if present (it will be converted to number) 193 | if (params.offset !== undefined && typeof params.offset !== 'number') { 194 | return false; 195 | } 196 | 197 | return true; 198 | } 199 | 200 | export function isCampaignMailboxStatisticsParams(obj: unknown): obj is CampaignMailboxStatisticsParams { 201 | return ( 202 | !!obj && 203 | typeof obj === 'object' && 204 | 'campaign_id' in obj && 205 | typeof (obj as CampaignMailboxStatisticsParams).campaign_id === 'number' 206 | ); 207 | } 208 | 209 | export function isDownloadCampaignDataParams(obj: unknown): obj is DownloadCampaignDataParams { 210 | if (!obj || typeof obj !== 'object') return false; 211 | 212 | const params = obj as Partial<DownloadCampaignDataParams>; 213 | 214 | // Check required fields 215 | if (typeof params.campaign_id !== 'number') return false; 216 | 217 | if (!params.download_type || 218 | !['analytics', 'leads', 'sequence', 'full_export'].includes(params.download_type)) { 219 | return false; 220 | } 221 | 222 | if (!params.format || !['json', 'csv'].includes(params.format)) { 223 | return false; 224 | } 225 | 226 | // Check optional fields 227 | if (params.user_id !== undefined && typeof params.user_id !== 'string') { 228 | return false; 229 | } 230 | 231 | return true; 232 | } 233 | 234 | export function isViewDownloadStatisticsParams(obj: unknown): obj is ViewDownloadStatisticsParams { 235 | if (!obj || typeof obj !== 'object') return false; 236 | 237 | const params = obj as Partial<ViewDownloadStatisticsParams>; 238 | 239 | // Both fields are optional, but need to be validated if present 240 | if (params.time_period !== undefined && 241 | !['all', 'today', 'week', 'month'].includes(params.time_period)) { 242 | return false; 243 | } 244 | 245 | if (params.group_by !== undefined && 246 | !['type', 'format', 'campaign', 'date'].includes(params.group_by)) { 247 | return false; 248 | } 249 | 250 | return true; 251 | } ``` -------------------------------------------------------------------------------- /src/types/lead.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from './common.js'; 2 | 3 | // Interface for listing leads 4 | export interface ListLeadsParams { 5 | campaign_id?: number; 6 | status?: string; 7 | limit?: number; 8 | offset?: number; 9 | search?: string; 10 | start_date?: string; 11 | end_date?: string; 12 | } 13 | 14 | // Interface for getting a single lead 15 | export interface GetLeadParams { 16 | lead_id: number; 17 | } 18 | 19 | // Interface for adding a lead to a campaign 20 | export interface AddLeadToCampaignParams { 21 | campaign_id: number; 22 | email: string; 23 | first_name?: string; 24 | last_name?: string; 25 | company?: string; 26 | title?: string; 27 | phone?: string; 28 | custom_fields?: Record<string, string>; 29 | } 30 | 31 | // Interface for updating a lead 32 | export interface UpdateLeadParams { 33 | lead_id: number; 34 | email?: string; 35 | first_name?: string; 36 | last_name?: string; 37 | company?: string; 38 | title?: string; 39 | phone?: string; 40 | custom_fields?: Record<string, string>; 41 | } 42 | 43 | // Interface for updating lead status 44 | export interface UpdateLeadStatusParams { 45 | lead_id: number; 46 | status: string; 47 | } 48 | 49 | // Interface for bulk importing leads 50 | export interface BulkImportLeadsParams { 51 | campaign_id: number; 52 | leads: Array<{ 53 | email: string; 54 | first_name?: string; 55 | last_name?: string; 56 | company?: string; 57 | title?: string; 58 | phone?: string; 59 | custom_fields?: Record<string, string>; 60 | }>; 61 | } 62 | 63 | // Interface for deleting a lead 64 | export interface DeleteLeadParams { 65 | lead_id: number; 66 | } 67 | 68 | // Type guards for params validation 69 | 70 | export function isListLeadsParams(args: unknown): args is ListLeadsParams { 71 | if (typeof args !== 'object' || args === null) { 72 | return false; 73 | } 74 | 75 | const params = args as ListLeadsParams; 76 | 77 | // Optional campaign_id must be a number if present 78 | if (params.campaign_id !== undefined && typeof params.campaign_id !== 'number') { 79 | return false; 80 | } 81 | 82 | // Optional status must be a string if present 83 | if (params.status !== undefined && typeof params.status !== 'string') { 84 | return false; 85 | } 86 | 87 | // Optional limit must be a number if present 88 | if (params.limit !== undefined && typeof params.limit !== 'number') { 89 | return false; 90 | } 91 | 92 | // Optional offset must be a number if present 93 | if (params.offset !== undefined && typeof params.offset !== 'number') { 94 | return false; 95 | } 96 | 97 | // Optional search must be a string if present 98 | if (params.search !== undefined && typeof params.search !== 'string') { 99 | return false; 100 | } 101 | 102 | // Optional start_date must be a string if present 103 | if (params.start_date !== undefined && typeof params.start_date !== 'string') { 104 | return false; 105 | } 106 | 107 | // Optional end_date must be a string if present 108 | if (params.end_date !== undefined && typeof params.end_date !== 'string') { 109 | return false; 110 | } 111 | 112 | return true; 113 | } 114 | 115 | export function isGetLeadParams(args: unknown): args is GetLeadParams { 116 | return ( 117 | typeof args === 'object' && 118 | args !== null && 119 | 'lead_id' in args && 120 | typeof (args as { lead_id: unknown }).lead_id === 'number' 121 | ); 122 | } 123 | 124 | export function isAddLeadToCampaignParams(args: unknown): args is AddLeadToCampaignParams { 125 | if ( 126 | typeof args !== 'object' || 127 | args === null || 128 | !('campaign_id' in args) || 129 | !('email' in args) || 130 | typeof (args as { campaign_id: unknown }).campaign_id !== 'number' || 131 | typeof (args as { email: unknown }).email !== 'string' 132 | ) { 133 | return false; 134 | } 135 | 136 | const params = args as AddLeadToCampaignParams; 137 | 138 | // Optional fields validation 139 | if (params.first_name !== undefined && typeof params.first_name !== 'string') { 140 | return false; 141 | } 142 | if (params.last_name !== undefined && typeof params.last_name !== 'string') { 143 | return false; 144 | } 145 | if (params.company !== undefined && typeof params.company !== 'string') { 146 | return false; 147 | } 148 | if (params.title !== undefined && typeof params.title !== 'string') { 149 | return false; 150 | } 151 | if (params.phone !== undefined && typeof params.phone !== 'string') { 152 | return false; 153 | } 154 | if ( 155 | params.custom_fields !== undefined && 156 | (typeof params.custom_fields !== 'object' || params.custom_fields === null) 157 | ) { 158 | return false; 159 | } 160 | 161 | return true; 162 | } 163 | 164 | export function isUpdateLeadParams(args: unknown): args is UpdateLeadParams { 165 | if ( 166 | typeof args !== 'object' || 167 | args === null || 168 | !('lead_id' in args) || 169 | typeof (args as { lead_id: unknown }).lead_id !== 'number' 170 | ) { 171 | return false; 172 | } 173 | 174 | const params = args as UpdateLeadParams; 175 | 176 | // Optional fields validation 177 | if (params.email !== undefined && typeof params.email !== 'string') { 178 | return false; 179 | } 180 | if (params.first_name !== undefined && typeof params.first_name !== 'string') { 181 | return false; 182 | } 183 | if (params.last_name !== undefined && typeof params.last_name !== 'string') { 184 | return false; 185 | } 186 | if (params.company !== undefined && typeof params.company !== 'string') { 187 | return false; 188 | } 189 | if (params.title !== undefined && typeof params.title !== 'string') { 190 | return false; 191 | } 192 | if (params.phone !== undefined && typeof params.phone !== 'string') { 193 | return false; 194 | } 195 | if ( 196 | params.custom_fields !== undefined && 197 | (typeof params.custom_fields !== 'object' || params.custom_fields === null) 198 | ) { 199 | return false; 200 | } 201 | 202 | return true; 203 | } 204 | 205 | export function isUpdateLeadStatusParams(args: unknown): args is UpdateLeadStatusParams { 206 | return ( 207 | typeof args === 'object' && 208 | args !== null && 209 | 'lead_id' in args && 210 | 'status' in args && 211 | typeof (args as { lead_id: unknown }).lead_id === 'number' && 212 | typeof (args as { status: unknown }).status === 'string' 213 | ); 214 | } 215 | 216 | export function isBulkImportLeadsParams(args: unknown): args is BulkImportLeadsParams { 217 | if ( 218 | typeof args !== 'object' || 219 | args === null || 220 | !('campaign_id' in args) || 221 | !('leads' in args) || 222 | typeof (args as { campaign_id: unknown }).campaign_id !== 'number' || 223 | !Array.isArray((args as { leads: unknown }).leads) 224 | ) { 225 | return false; 226 | } 227 | 228 | const params = args as BulkImportLeadsParams; 229 | 230 | // Validate each lead in the leads array 231 | for (const lead of params.leads) { 232 | if ( 233 | typeof lead !== 'object' || 234 | lead === null || 235 | !('email' in lead) || 236 | typeof lead.email !== 'string' 237 | ) { 238 | return false; 239 | } 240 | 241 | // Optional fields validation 242 | if (lead.first_name !== undefined && typeof lead.first_name !== 'string') { 243 | return false; 244 | } 245 | if (lead.last_name !== undefined && typeof lead.last_name !== 'string') { 246 | return false; 247 | } 248 | if (lead.company !== undefined && typeof lead.company !== 'string') { 249 | return false; 250 | } 251 | if (lead.title !== undefined && typeof lead.title !== 'string') { 252 | return false; 253 | } 254 | if (lead.phone !== undefined && typeof lead.phone !== 'string') { 255 | return false; 256 | } 257 | if ( 258 | lead.custom_fields !== undefined && 259 | (typeof lead.custom_fields !== 'object' || lead.custom_fields === null) 260 | ) { 261 | return false; 262 | } 263 | } 264 | 265 | return true; 266 | } 267 | 268 | export function isDeleteLeadParams(args: unknown): args is DeleteLeadParams { 269 | return ( 270 | typeof args === 'object' && 271 | args !== null && 272 | 'lead_id' in args && 273 | typeof (args as { lead_id: unknown }).lead_id === 'number' 274 | ); 275 | } ``` -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | import dotenv from 'dotenv'; 5 | import { spawn } from 'child_process'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | import fs from 'fs'; 9 | import readline from 'readline'; 10 | import { createInterface } from 'readline'; 11 | 12 | // Load environment variables from .env file 13 | dotenv.config(); 14 | 15 | // Get the directory name of the current module 16 | const __filename = fileURLToPath(import.meta.url); 17 | const __dirname = path.dirname(__filename); 18 | 19 | // Get version from package.json 20 | const version = '1.0.0'; // This should ideally be imported from package.json 21 | 22 | // License server URL 23 | const LICENSE_SERVER_URL = 'https://sea-turtle-app-64etr.ondigitalocean.app/'; 24 | 25 | // Function to prompt for value interactively 26 | async function promptForValue(question: string, hidden = false): Promise<string> { 27 | const rl = createInterface({ 28 | input: process.stdin, 29 | output: process.stdout, 30 | }); 31 | 32 | return new Promise((resolve) => { 33 | if (hidden) { 34 | process.stdout.write(question); 35 | process.stdin.setRawMode(true); 36 | let password = ''; 37 | 38 | process.stdin.on('data', (chunk) => { 39 | const str = chunk.toString(); 40 | if (str === '\n' || str === '\r' || str === '\u0004') { 41 | process.stdin.setRawMode(false); 42 | process.stdout.write('\n'); 43 | rl.close(); 44 | resolve(password); 45 | } else if (str === '\u0003') { // Ctrl+C 46 | process.exit(0); 47 | } else if (str === '\u007F') { // Backspace 48 | if (password.length > 0) { 49 | password = password.substring(0, password.length - 1); 50 | process.stdout.write('\b \b'); 51 | } 52 | } else { 53 | password += str; 54 | process.stdout.write('*'); 55 | } 56 | }); 57 | } else { 58 | rl.question(question, (answer) => { 59 | rl.close(); 60 | resolve(answer); 61 | }); 62 | } 63 | }); 64 | } 65 | 66 | // Function to ensure required environment variables are set 67 | async function ensureEnvVars(): Promise<void> { 68 | // Check for API key 69 | if (!process.env.SMARTLEAD_API_KEY) { 70 | console.log('\nSmartlead API Key not found.'); 71 | const apiKey = await promptForValue('Enter your Smartlead API Key: ', true); 72 | if (apiKey) { 73 | process.env.SMARTLEAD_API_KEY = apiKey; 74 | } else { 75 | console.log('Smartlead API Key is required to continue.'); 76 | process.exit(1); 77 | } 78 | } 79 | 80 | // Check for license key 81 | if (!process.env.JEAN_LICENSE_KEY) { 82 | console.log('\nJean License Key not found. Defaulting to free tier access.'); 83 | process.env.JEAN_LICENSE_KEY = 'JEANPARTNER'; 84 | } 85 | 86 | // Set license server URL if not set 87 | if (!process.env.LICENSE_SERVER_URL) { 88 | process.env.LICENSE_SERVER_URL = LICENSE_SERVER_URL; 89 | } 90 | 91 | console.log('\nConfiguration complete!\n'); 92 | } 93 | 94 | // Function to save environment variables to .env file 95 | async function saveEnvToFile(): Promise<void> { 96 | if (process.env.SMARTLEAD_API_KEY || process.env.JEAN_LICENSE_KEY) { 97 | const saveEnv = await promptForValue('Do you want to save these settings to a .env file for future use? (y/n): '); 98 | if (saveEnv.toLowerCase() === 'y') { 99 | try { 100 | let envContent = ''; 101 | if (process.env.SMARTLEAD_API_KEY) { 102 | envContent += `SMARTLEAD_API_KEY=${process.env.SMARTLEAD_API_KEY}\n`; 103 | } 104 | if (process.env.JEAN_LICENSE_KEY) { 105 | envContent += `JEAN_LICENSE_KEY=${process.env.JEAN_LICENSE_KEY}\n`; 106 | } 107 | if (process.env.LICENSE_SERVER_URL) { 108 | envContent += `LICENSE_SERVER_URL=${process.env.LICENSE_SERVER_URL}\n`; 109 | } 110 | 111 | fs.writeFileSync('.env', envContent); 112 | console.log('Settings saved to .env file in the current directory.'); 113 | } catch (error) { 114 | console.error('Error saving .env file:', error); 115 | } 116 | } 117 | } 118 | } 119 | 120 | program 121 | .version(version) 122 | .description('Smartlead MCP Server CLI'); 123 | 124 | program 125 | .command('start') 126 | .description('Start the MCP server in standard STDIO mode') 127 | .option('--api-key <key>', 'Your Smartlead API Key') 128 | .option('--license-key <key>', 'Your Jean License Key') 129 | .action(async (options: { apiKey?: string; licenseKey?: string }) => { 130 | // Set env vars from command line options if provided 131 | if (options.apiKey) process.env.SMARTLEAD_API_KEY = options.apiKey; 132 | if (options.licenseKey) process.env.JEAN_LICENSE_KEY = options.licenseKey; 133 | 134 | // Ensure required env vars are set (will prompt if missing) 135 | await ensureEnvVars(); 136 | await saveEnvToFile(); 137 | 138 | console.log('Starting Smartlead MCP Server in STDIO mode...'); 139 | // Run as a separate process instead of trying to import 140 | const indexPath = path.join(__dirname, 'index.js'); 141 | const child = spawn('node', [indexPath], { 142 | stdio: 'inherit', 143 | env: process.env 144 | }); 145 | 146 | process.on('SIGINT', () => { 147 | child.kill(); 148 | process.exit(); 149 | }); 150 | }); 151 | 152 | program 153 | .command('sse') 154 | .description('Start the MCP server in SSE mode for n8n integration') 155 | .option('-p, --port <port>', 'Port to run the server on', '3000') 156 | .option('--api-key <key>', 'Your Smartlead API Key') 157 | .option('--license-key <key>', 'Your Jean License Key') 158 | .action(async (options: { port: string; apiKey?: string; licenseKey?: string }) => { 159 | // Set env vars from command line options if provided 160 | if (options.apiKey) process.env.SMARTLEAD_API_KEY = options.apiKey; 161 | if (options.licenseKey) process.env.JEAN_LICENSE_KEY = options.licenseKey; 162 | 163 | // Ensure required env vars are set (will prompt if missing) 164 | await ensureEnvVars(); 165 | await saveEnvToFile(); 166 | 167 | console.log(`Starting Smartlead MCP Server in SSE mode on port ${options.port}...`); 168 | console.log(`Connect from n8n to http://localhost:${options.port}/sse`); 169 | 170 | // Use supergateway to run in SSE mode 171 | const indexPath = path.join(__dirname, 'index.js'); 172 | const supergateway = spawn('npx', [ 173 | '-y', 174 | 'supergateway', 175 | '--stdio', 176 | `node ${indexPath}`, 177 | '--port', 178 | options.port 179 | ], { 180 | shell: true, 181 | stdio: 'inherit', 182 | env: process.env 183 | }); 184 | 185 | process.on('SIGINT', () => { 186 | supergateway.kill(); 187 | process.exit(); 188 | }); 189 | }); 190 | 191 | program 192 | .command('config') 193 | .description('Show current configuration and set up environment variables') 194 | .option('--api-key <key>', 'Set your Smartlead API Key') 195 | .option('--license-key <key>', 'Set your Jean License Key') 196 | .action(async (options: { apiKey?: string; licenseKey?: string }) => { 197 | if (options.apiKey) process.env.SMARTLEAD_API_KEY = options.apiKey; 198 | if (options.licenseKey) process.env.JEAN_LICENSE_KEY = options.licenseKey; 199 | 200 | await ensureEnvVars(); 201 | await saveEnvToFile(); 202 | 203 | console.log('\nSmartlead MCP Server Configuration:'); 204 | console.log(`API URL: ${process.env.SMARTLEAD_API_URL || 'https://server.smartlead.ai/api/v1'}`); 205 | console.log(`License Server: ${process.env.LICENSE_SERVER_URL || LICENSE_SERVER_URL}`); 206 | console.log(`License Status: ${process.env.JEAN_LICENSE_KEY ? 'Configured' : 'Not Configured'}`); 207 | console.log('\nConfiguration saved and ready to use.'); 208 | }); 209 | 210 | program.parse(process.argv); 211 | 212 | // Default to help if no command is provided 213 | if (!process.argv.slice(2).length) { 214 | program.outputHelp(); 215 | } ``` -------------------------------------------------------------------------------- /src/handlers/webhooks.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AxiosInstance } from 'axios'; 2 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; 3 | import { 4 | isFetchWebhooksByCampaignParams, 5 | isUpsertCampaignWebhookParams, 6 | isDeleteCampaignWebhookParams, 7 | isGetWebhooksPublishSummaryParams, 8 | isRetriggerFailedEventsParams 9 | } from '../types/webhooks.js'; 10 | 11 | // SmartLead API base URL 12 | const SMARTLEAD_API_URL = 'https://server.smartlead.ai/api/v1'; 13 | 14 | // Handler for Webhook-related tools 15 | export async function handleWebhookTool( 16 | toolName: string, 17 | args: unknown, 18 | apiClient: AxiosInstance, 19 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 20 | ) { 21 | switch (toolName) { 22 | case 'smartlead_fetch_webhooks_by_campaign': { 23 | return handleFetchWebhooksByCampaign(args, apiClient, withRetry); 24 | } 25 | case 'smartlead_upsert_campaign_webhook': { 26 | return handleUpsertCampaignWebhook(args, apiClient, withRetry); 27 | } 28 | case 'smartlead_delete_campaign_webhook': { 29 | return handleDeleteCampaignWebhook(args, apiClient, withRetry); 30 | } 31 | case 'smartlead_get_webhooks_publish_summary': { 32 | return handleGetWebhooksPublishSummary(args, apiClient, withRetry); 33 | } 34 | case 'smartlead_retrigger_failed_events': { 35 | return handleRetriggerFailedEvents(args, apiClient, withRetry); 36 | } 37 | default: 38 | throw new Error(`Unknown Webhook tool: ${toolName}`); 39 | } 40 | } 41 | 42 | // Create a modified client for SmartLead API with the correct base URL 43 | function createSmartLeadClient(apiClient: AxiosInstance) { 44 | return { 45 | get: (url: string, config?: any) => 46 | apiClient.get(`${SMARTLEAD_API_URL}${url}`, config), 47 | post: (url: string, data?: any, config?: any) => 48 | apiClient.post(`${SMARTLEAD_API_URL}${url}`, data, config), 49 | put: (url: string, data?: any, config?: any) => 50 | apiClient.put(`${SMARTLEAD_API_URL}${url}`, data, config), 51 | delete: (url: string, config?: any) => 52 | apiClient.delete(`${SMARTLEAD_API_URL}${url}`, config) 53 | }; 54 | } 55 | 56 | // Individual handlers for each tool 57 | async function handleFetchWebhooksByCampaign( 58 | args: unknown, 59 | apiClient: AxiosInstance, 60 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 61 | ) { 62 | if (!isFetchWebhooksByCampaignParams(args)) { 63 | throw new McpError( 64 | ErrorCode.InvalidParams, 65 | 'Invalid arguments for smartlead_fetch_webhooks_by_campaign' 66 | ); 67 | } 68 | 69 | try { 70 | const smartLeadClient = createSmartLeadClient(apiClient); 71 | const { campaign_id } = args; 72 | 73 | const response = await withRetry( 74 | async () => smartLeadClient.get(`/campaigns/${campaign_id}/webhooks`), 75 | 'fetch webhooks by campaign' 76 | ); 77 | 78 | return { 79 | content: [ 80 | { 81 | type: 'text', 82 | text: JSON.stringify(response.data, null, 2), 83 | }, 84 | ], 85 | isError: false, 86 | }; 87 | } catch (error: any) { 88 | return { 89 | content: [{ 90 | type: 'text', 91 | text: `API Error: ${error.response?.data?.message || error.message}` 92 | }], 93 | isError: true, 94 | }; 95 | } 96 | } 97 | 98 | async function handleUpsertCampaignWebhook( 99 | args: unknown, 100 | apiClient: AxiosInstance, 101 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 102 | ) { 103 | if (!isUpsertCampaignWebhookParams(args)) { 104 | throw new McpError( 105 | ErrorCode.InvalidParams, 106 | 'Invalid arguments for smartlead_upsert_campaign_webhook' 107 | ); 108 | } 109 | 110 | try { 111 | const smartLeadClient = createSmartLeadClient(apiClient); 112 | const { campaign_id, ...webhookData } = args; 113 | 114 | const response = await withRetry( 115 | async () => smartLeadClient.post(`/campaigns/${campaign_id}/webhooks`, webhookData), 116 | 'upsert campaign webhook' 117 | ); 118 | 119 | return { 120 | content: [ 121 | { 122 | type: 'text', 123 | text: JSON.stringify(response.data, null, 2), 124 | }, 125 | ], 126 | isError: false, 127 | }; 128 | } catch (error: any) { 129 | return { 130 | content: [{ 131 | type: 'text', 132 | text: `API Error: ${error.response?.data?.message || error.message}` 133 | }], 134 | isError: true, 135 | }; 136 | } 137 | } 138 | 139 | async function handleDeleteCampaignWebhook( 140 | args: unknown, 141 | apiClient: AxiosInstance, 142 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 143 | ) { 144 | if (!isDeleteCampaignWebhookParams(args)) { 145 | throw new McpError( 146 | ErrorCode.InvalidParams, 147 | 'Invalid arguments for smartlead_delete_campaign_webhook' 148 | ); 149 | } 150 | 151 | try { 152 | const smartLeadClient = createSmartLeadClient(apiClient); 153 | const { campaign_id, id } = args; 154 | 155 | // The API documentation suggests a DELETE with a body payload 156 | // Different from typical REST practices but following the API spec 157 | const response = await withRetry( 158 | async () => smartLeadClient.delete(`/campaigns/${campaign_id}/webhooks`, { 159 | data: { id } 160 | }), 161 | 'delete campaign webhook' 162 | ); 163 | 164 | return { 165 | content: [ 166 | { 167 | type: 'text', 168 | text: JSON.stringify(response.data, null, 2), 169 | }, 170 | ], 171 | isError: false, 172 | }; 173 | } catch (error: any) { 174 | return { 175 | content: [{ 176 | type: 'text', 177 | text: `API Error: ${error.response?.data?.message || error.message}` 178 | }], 179 | isError: true, 180 | }; 181 | } 182 | } 183 | 184 | async function handleGetWebhooksPublishSummary( 185 | args: unknown, 186 | apiClient: AxiosInstance, 187 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 188 | ) { 189 | if (!isGetWebhooksPublishSummaryParams(args)) { 190 | throw new McpError( 191 | ErrorCode.InvalidParams, 192 | 'Invalid arguments for smartlead_get_webhooks_publish_summary' 193 | ); 194 | } 195 | 196 | try { 197 | const smartLeadClient = createSmartLeadClient(apiClient); 198 | const { campaign_id, fromTime, toTime } = args; 199 | 200 | let url = `/campaigns/${campaign_id}/webhooks/summary`; 201 | const queryParams = new URLSearchParams(); 202 | 203 | if (fromTime) { 204 | queryParams.append('fromTime', fromTime); 205 | } 206 | 207 | if (toTime) { 208 | queryParams.append('toTime', toTime); 209 | } 210 | 211 | if (queryParams.toString()) { 212 | url += `?${queryParams.toString()}`; 213 | } 214 | 215 | const response = await withRetry( 216 | async () => smartLeadClient.get(url), 217 | 'get webhooks publish summary' 218 | ); 219 | 220 | return { 221 | content: [ 222 | { 223 | type: 'text', 224 | text: JSON.stringify(response.data, null, 2), 225 | }, 226 | ], 227 | isError: false, 228 | }; 229 | } catch (error: any) { 230 | return { 231 | content: [{ 232 | type: 'text', 233 | text: `API Error: ${error.response?.data?.message || error.message}` 234 | }], 235 | isError: true, 236 | }; 237 | } 238 | } 239 | 240 | async function handleRetriggerFailedEvents( 241 | args: unknown, 242 | apiClient: AxiosInstance, 243 | withRetry: <T>(operation: () => Promise<T>, context: string) => Promise<T> 244 | ) { 245 | if (!isRetriggerFailedEventsParams(args)) { 246 | throw new McpError( 247 | ErrorCode.InvalidParams, 248 | 'Invalid arguments for smartlead_retrigger_failed_events' 249 | ); 250 | } 251 | 252 | try { 253 | const smartLeadClient = createSmartLeadClient(apiClient); 254 | const { campaign_id, fromTime, toTime } = args; 255 | 256 | const response = await withRetry( 257 | async () => smartLeadClient.post(`/campaigns/${campaign_id}/webhooks/retrigger-failed-events`, { 258 | fromTime, 259 | toTime 260 | }), 261 | 'retrigger failed events' 262 | ); 263 | 264 | return { 265 | content: [ 266 | { 267 | type: 'text', 268 | text: JSON.stringify(response.data, null, 2), 269 | }, 270 | ], 271 | isError: false, 272 | }; 273 | } catch (error: any) { 274 | return { 275 | content: [{ 276 | type: 'text', 277 | text: `API Error: ${error.response?.data?.message || error.message}` 278 | }], 279 | isError: true, 280 | }; 281 | } 282 | } ``` -------------------------------------------------------------------------------- /src/types/campaign.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Type definitions for campaign management 2 | 3 | export interface CreateCampaignParams { 4 | name: string; 5 | client_id?: number; 6 | } 7 | 8 | export interface UpdateCampaignScheduleParams { 9 | campaign_id: number; 10 | timezone?: string; 11 | days_of_the_week?: number[]; 12 | start_hour?: string; 13 | end_hour?: string; 14 | min_time_btw_emails?: number; 15 | max_new_leads_per_day?: number; 16 | schedule_start_time?: string; 17 | } 18 | 19 | export interface UpdateCampaignSettingsParams { 20 | campaign_id: number; 21 | name?: string; 22 | status?: 'active' | 'paused' | 'completed'; 23 | settings?: Record<string, any>; 24 | } 25 | 26 | export interface UpdateCampaignStatusParams { 27 | campaign_id: number; 28 | status: 'PAUSED' | 'STOPPED' | 'START'; 29 | } 30 | 31 | export interface GetCampaignParams { 32 | campaign_id: number; 33 | } 34 | 35 | export interface GetCampaignSequenceParams { 36 | campaign_id: number; 37 | } 38 | 39 | export interface ListCampaignsParams { 40 | status?: 'active' | 'paused' | 'completed'; 41 | limit?: number; 42 | offset?: number; 43 | } 44 | 45 | export interface SaveCampaignSequenceParams { 46 | campaign_id: number; 47 | sequence: Array<{ 48 | seq_number: number; 49 | seq_delay_details: { 50 | delay_in_days: number; 51 | }; 52 | variant_distribution_type: 'MANUAL_EQUAL' | 'MANUAL_PERCENTAGE' | 'AI_EQUAL'; 53 | lead_distribution_percentage?: number; 54 | winning_metric_property?: 'OPEN_RATE' | 'CLICK_RATE' | 'REPLY_RATE' | 'POSITIVE_REPLY_RATE'; 55 | seq_variants: Array<{ 56 | subject: string; 57 | email_body: string; 58 | variant_label: string; 59 | id?: number; 60 | variant_distribution_percentage?: number; 61 | }>; 62 | }>; 63 | } 64 | 65 | // New interface definitions for remaining campaign management endpoints 66 | export interface GetCampaignsByLeadParams { 67 | lead_id: number; 68 | } 69 | 70 | export interface ExportCampaignLeadsParams { 71 | campaign_id: number; 72 | } 73 | 74 | export interface DeleteCampaignParams { 75 | campaign_id: number; 76 | } 77 | 78 | export interface GetCampaignAnalyticsByDateParams { 79 | campaign_id: number; 80 | start_date: string; 81 | end_date: string; 82 | } 83 | 84 | export interface GetCampaignSequenceAnalyticsParams { 85 | campaign_id: number; 86 | start_date: string; 87 | end_date: string; 88 | time_zone?: string; 89 | } 90 | 91 | // Type guards 92 | export function isCreateCampaignParams(args: unknown): args is CreateCampaignParams { 93 | return ( 94 | typeof args === 'object' && 95 | args !== null && 96 | 'name' in args && 97 | typeof (args as { name: unknown }).name === 'string' 98 | ); 99 | } 100 | 101 | export function isUpdateCampaignScheduleParams(args: unknown): args is UpdateCampaignScheduleParams { 102 | return ( 103 | typeof args === 'object' && 104 | args !== null && 105 | 'campaign_id' in args && 106 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' 107 | ); 108 | } 109 | 110 | export function isUpdateCampaignSettingsParams(args: unknown): args is UpdateCampaignSettingsParams { 111 | return ( 112 | typeof args === 'object' && 113 | args !== null && 114 | 'campaign_id' in args && 115 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' 116 | ); 117 | } 118 | 119 | export function isUpdateCampaignStatusParams(args: unknown): args is UpdateCampaignStatusParams { 120 | return ( 121 | typeof args === 'object' && 122 | args !== null && 123 | 'campaign_id' in args && 124 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' && 125 | 'status' in args && 126 | typeof (args as { status: unknown }).status === 'string' && 127 | ['PAUSED', 'STOPPED', 'START'].includes((args as { status: string }).status) 128 | ); 129 | } 130 | 131 | export function isGetCampaignParams(args: unknown): args is GetCampaignParams { 132 | return ( 133 | typeof args === 'object' && 134 | args !== null && 135 | 'campaign_id' in args && 136 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' 137 | ); 138 | } 139 | 140 | export function isGetCampaignSequenceParams(args: unknown): args is GetCampaignSequenceParams { 141 | return ( 142 | typeof args === 'object' && 143 | args !== null && 144 | 'campaign_id' in args && 145 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' 146 | ); 147 | } 148 | 149 | export function isListCampaignsParams(args: unknown): args is ListCampaignsParams { 150 | return typeof args === 'object' && args !== null; 151 | } 152 | 153 | export function isSaveCampaignSequenceParams(args: unknown): args is SaveCampaignSequenceParams { 154 | if ( 155 | typeof args !== 'object' || 156 | args === null || 157 | !('campaign_id' in args) || 158 | typeof (args as { campaign_id: unknown }).campaign_id !== 'number' || 159 | !('sequence' in args) || 160 | !Array.isArray((args as { sequence: unknown }).sequence) 161 | ) { 162 | return false; 163 | } 164 | 165 | const sequence = (args as { sequence: unknown[] }).sequence; 166 | return sequence.every(isValidSequenceItem); 167 | } 168 | 169 | // New type guards for the remaining campaign management endpoints 170 | export function isGetCampaignsByLeadParams(args: unknown): args is GetCampaignsByLeadParams { 171 | return ( 172 | typeof args === 'object' && 173 | args !== null && 174 | 'lead_id' in args && 175 | typeof (args as { lead_id: unknown }).lead_id === 'number' 176 | ); 177 | } 178 | 179 | export function isExportCampaignLeadsParams(args: unknown): args is ExportCampaignLeadsParams { 180 | return ( 181 | typeof args === 'object' && 182 | args !== null && 183 | 'campaign_id' in args && 184 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' 185 | ); 186 | } 187 | 188 | export function isDeleteCampaignParams(args: unknown): args is DeleteCampaignParams { 189 | return ( 190 | typeof args === 'object' && 191 | args !== null && 192 | 'campaign_id' in args && 193 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' 194 | ); 195 | } 196 | 197 | export function isGetCampaignAnalyticsByDateParams(args: unknown): args is GetCampaignAnalyticsByDateParams { 198 | return ( 199 | typeof args === 'object' && 200 | args !== null && 201 | 'campaign_id' in args && 202 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' && 203 | 'start_date' in args && 204 | typeof (args as { start_date: unknown }).start_date === 'string' && 205 | 'end_date' in args && 206 | typeof (args as { end_date: unknown }).end_date === 'string' 207 | ); 208 | } 209 | 210 | export function isGetCampaignSequenceAnalyticsParams(args: unknown): args is GetCampaignSequenceAnalyticsParams { 211 | return ( 212 | typeof args === 'object' && 213 | args !== null && 214 | 'campaign_id' in args && 215 | typeof (args as { campaign_id: unknown }).campaign_id === 'number' && 216 | 'start_date' in args && 217 | typeof (args as { start_date: unknown }).start_date === 'string' && 218 | 'end_date' in args && 219 | typeof (args as { end_date: unknown }).end_date === 'string' 220 | ); 221 | } 222 | 223 | function isValidSequenceItem(item: unknown): boolean { 224 | return ( 225 | typeof item === 'object' && 226 | item !== null && 227 | 'seq_number' in item && 228 | typeof (item as { seq_number: unknown }).seq_number === 'number' && 229 | 'seq_delay_details' in item && 230 | typeof (item as { seq_delay_details: unknown }).seq_delay_details === 'object' && 231 | (item as { seq_delay_details: unknown }).seq_delay_details !== null && 232 | 'delay_in_days' in (item as { seq_delay_details: { delay_in_days: unknown } }).seq_delay_details && 233 | typeof (item as { seq_delay_details: { delay_in_days: unknown } }).seq_delay_details.delay_in_days === 'number' && 234 | 'variant_distribution_type' in item && 235 | typeof (item as { variant_distribution_type: unknown }).variant_distribution_type === 'string' && 236 | 'seq_variants' in item && 237 | Array.isArray((item as { seq_variants: unknown[] }).seq_variants) && 238 | (item as { seq_variants: unknown[] }).seq_variants.every( 239 | (variant) => 240 | typeof variant === 'object' && 241 | variant !== null && 242 | 'subject' in variant && 243 | typeof (variant as { subject: unknown }).subject === 'string' && 244 | 'email_body' in variant && 245 | typeof (variant as { email_body: unknown }).email_body === 'string' && 246 | 'variant_label' in variant && 247 | typeof (variant as { variant_label: unknown }).variant_label === 'string' 248 | ) 249 | ); 250 | } ``` -------------------------------------------------------------------------------- /src/tools/statistics.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CategoryTool, ToolCategory } from '../types/common.js'; 2 | 3 | // Campaign Statistics Tools 4 | export const CAMPAIGN_STATISTICS_TOOL: CategoryTool = { 5 | name: 'smartlead_get_campaign_statistics', 6 | description: 'Fetch campaign statistics using the campaign\'s ID.', 7 | category: ToolCategory.CAMPAIGN_STATISTICS, 8 | inputSchema: { 9 | type: 'object', 10 | properties: { 11 | campaign_id: { 12 | type: 'number', 13 | description: 'ID of the campaign to fetch statistics for', 14 | }, 15 | offset: { 16 | type: 'number', 17 | description: 'Offset for pagination', 18 | }, 19 | limit: { 20 | type: 'number', 21 | description: 'Maximum number of statistics to return', 22 | }, 23 | email_sequence_number: { 24 | type: 'string', 25 | description: 'Email sequence number to filter by (e.g., "1,2,3,4")', 26 | }, 27 | email_status: { 28 | type: 'string', 29 | description: 'Email status to filter by (e.g., "opened", "clicked", "replied", "unsubscribed", "bounced")', 30 | }, 31 | sent_time_start_date: { 32 | type: 'string', 33 | description: 'Filter by sent time greater than this date (e.g., "2023-10-16 10:33:02.000Z")', 34 | }, 35 | sent_time_end_date: { 36 | type: 'string', 37 | description: 'Filter by sent time less than this date (e.g., "2023-10-16 10:33:02.000Z")', 38 | }, 39 | }, 40 | required: ['campaign_id'], 41 | }, 42 | }; 43 | 44 | export const CAMPAIGN_STATISTICS_BY_DATE_TOOL: CategoryTool = { 45 | name: 'smartlead_get_campaign_statistics_by_date', 46 | description: 'Fetch campaign statistics for a specific date range.', 47 | category: ToolCategory.CAMPAIGN_STATISTICS, 48 | inputSchema: { 49 | type: 'object', 50 | properties: { 51 | campaign_id: { 52 | type: 'number', 53 | description: 'ID of the campaign to fetch statistics for', 54 | }, 55 | start_date: { 56 | type: 'string', 57 | description: 'Start date in YYYY-MM-DD format', 58 | }, 59 | end_date: { 60 | type: 'string', 61 | description: 'End date in YYYY-MM-DD format', 62 | }, 63 | }, 64 | required: ['campaign_id', 'start_date', 'end_date'], 65 | }, 66 | }; 67 | 68 | export const WARMUP_STATS_BY_EMAIL_TOOL: CategoryTool = { 69 | name: 'smartlead_get_warmup_stats_by_email', 70 | description: 'Fetch warmup stats for the last 7 days for a specific email account.', 71 | category: ToolCategory.CAMPAIGN_STATISTICS, 72 | inputSchema: { 73 | type: 'object', 74 | properties: { 75 | email_account_id: { 76 | type: 'number', 77 | description: 'ID of the email account to fetch warmup stats for', 78 | }, 79 | }, 80 | required: ['email_account_id'], 81 | }, 82 | }; 83 | 84 | export const CAMPAIGN_TOP_LEVEL_ANALYTICS_TOOL: CategoryTool = { 85 | name: 'smartlead_get_campaign_top_level_analytics', 86 | description: 'Fetch top level analytics for a campaign.', 87 | category: ToolCategory.CAMPAIGN_STATISTICS, 88 | inputSchema: { 89 | type: 'object', 90 | properties: { 91 | campaign_id: { 92 | type: 'number', 93 | description: 'ID of the campaign to fetch analytics for', 94 | }, 95 | }, 96 | required: ['campaign_id'], 97 | }, 98 | }; 99 | 100 | export const CAMPAIGN_TOP_LEVEL_ANALYTICS_BY_DATE_TOOL: CategoryTool = { 101 | name: 'smartlead_get_campaign_top_level_analytics_by_date', 102 | description: 'Fetch campaign top level analytics for a specific date range.', 103 | category: ToolCategory.CAMPAIGN_STATISTICS, 104 | inputSchema: { 105 | type: 'object', 106 | properties: { 107 | campaign_id: { 108 | type: 'number', 109 | description: 'ID of the campaign to fetch analytics for', 110 | }, 111 | start_date: { 112 | type: 'string', 113 | description: 'Start date in YYYY-MM-DD format', 114 | }, 115 | end_date: { 116 | type: 'string', 117 | description: 'End date in YYYY-MM-DD format', 118 | }, 119 | }, 120 | required: ['campaign_id', 'start_date', 'end_date'], 121 | }, 122 | }; 123 | 124 | export const CAMPAIGN_LEAD_STATISTICS_TOOL: CategoryTool = { 125 | name: 'smartlead_get_campaign_lead_statistics', 126 | description: 'Fetch lead statistics for a campaign.', 127 | category: ToolCategory.CAMPAIGN_STATISTICS, 128 | inputSchema: { 129 | type: 'object', 130 | properties: { 131 | campaign_id: { 132 | type: 'number', 133 | description: 'ID of the campaign to fetch lead statistics for', 134 | }, 135 | limit: { 136 | type: 'number', 137 | description: 'Maximum number of leads to return (max 100)', 138 | }, 139 | created_at_gt: { 140 | type: 'string', 141 | description: 'Filter by leads created after this date (YYYY-MM-DD format)', 142 | }, 143 | event_time_gt: { 144 | type: 'string', 145 | description: 'Filter by events after this date (YYYY-MM-DD format)', 146 | }, 147 | offset: { 148 | type: 'number', 149 | description: 'Offset for pagination', 150 | }, 151 | }, 152 | required: ['campaign_id'], 153 | }, 154 | }; 155 | 156 | export const CAMPAIGN_MAILBOX_STATISTICS_TOOL: CategoryTool = { 157 | name: 'smartlead_get_campaign_mailbox_statistics', 158 | description: 'Fetch mailbox statistics for a campaign.', 159 | category: ToolCategory.CAMPAIGN_STATISTICS, 160 | inputSchema: { 161 | type: 'object', 162 | properties: { 163 | campaign_id: { 164 | type: 'number', 165 | description: 'ID of the campaign to fetch mailbox statistics for', 166 | }, 167 | client_id: { 168 | type: 'string', 169 | description: 'Client ID if the campaign is client-specific', 170 | }, 171 | offset: { 172 | type: 'number', 173 | description: 'Offset for pagination', 174 | }, 175 | limit: { 176 | type: 'number', 177 | description: 'Maximum number of results to return (min 1, max 20)', 178 | }, 179 | start_date: { 180 | type: 'string', 181 | description: 'Start date (must be used with end_date)', 182 | }, 183 | end_date: { 184 | type: 'string', 185 | description: 'End date (must be used with start_date)', 186 | }, 187 | timezone: { 188 | type: 'string', 189 | description: 'Timezone for the data (e.g., "America/Los_Angeles")', 190 | }, 191 | }, 192 | required: ['campaign_id'], 193 | }, 194 | }; 195 | 196 | // Add this new tool for downloading campaign data with tracking 197 | export const DOWNLOAD_CAMPAIGN_DATA_TOOL: CategoryTool = { 198 | name: 'smartlead_download_campaign_data', 199 | description: 'Download campaign data and track the download for analytics.', 200 | category: ToolCategory.CAMPAIGN_STATISTICS, 201 | inputSchema: { 202 | type: 'object', 203 | properties: { 204 | campaign_id: { 205 | type: 'number', 206 | description: 'ID of the campaign to download data for', 207 | }, 208 | download_type: { 209 | type: 'string', 210 | enum: ['analytics', 'leads', 'sequence', 'full_export'], 211 | description: 'Type of data to download', 212 | }, 213 | format: { 214 | type: 'string', 215 | enum: ['json', 'csv'], 216 | description: 'Format of the downloaded data', 217 | }, 218 | user_id: { 219 | type: 'string', 220 | description: 'Optional user identifier for tracking downloads', 221 | }, 222 | }, 223 | required: ['campaign_id', 'download_type', 'format'], 224 | }, 225 | }; 226 | 227 | // Add this new tool for viewing download statistics 228 | export const VIEW_DOWNLOAD_STATISTICS_TOOL: CategoryTool = { 229 | name: 'smartlead_view_download_statistics', 230 | description: 'View statistics about downloaded campaign data.', 231 | category: ToolCategory.CAMPAIGN_STATISTICS, 232 | inputSchema: { 233 | type: 'object', 234 | properties: { 235 | time_period: { 236 | type: 'string', 237 | enum: ['all', 'today', 'week', 'month'], 238 | description: 'Time period to filter statistics by', 239 | }, 240 | group_by: { 241 | type: 'string', 242 | enum: ['type', 'format', 'campaign', 'date'], 243 | description: 'How to group the statistics', 244 | }, 245 | }, 246 | }, 247 | }; 248 | 249 | // Export an array of all campaign statistics tools for registration 250 | export const statisticsTools: CategoryTool[] = [ 251 | CAMPAIGN_STATISTICS_TOOL, 252 | CAMPAIGN_STATISTICS_BY_DATE_TOOL, 253 | WARMUP_STATS_BY_EMAIL_TOOL, 254 | CAMPAIGN_TOP_LEVEL_ANALYTICS_TOOL, 255 | CAMPAIGN_TOP_LEVEL_ANALYTICS_BY_DATE_TOOL, 256 | CAMPAIGN_LEAD_STATISTICS_TOOL, 257 | CAMPAIGN_MAILBOX_STATISTICS_TOOL, 258 | DOWNLOAD_CAMPAIGN_DATA_TOOL, 259 | VIEW_DOWNLOAD_STATISTICS_TOOL, 260 | ]; ```