heyreach-mcp-server
Version:
Modern MCP server for HeyReach LinkedIn automation with dual transport support (stdio + HTTP streaming) and header authentication
1,004 lines (909 loc) • 36.9 kB
JavaScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { HeyReachClient } from './heyreach-client.js';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// Get the directory of the current module and read version from package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
export class HeyReachMcpServer {
server;
heyReachClient;
constructor(config) {
this.server = new McpServer({
name: 'heyreach-mcp-server',
version: packageJson.version,
});
this.heyReachClient = new HeyReachClient(config);
this.setupTools();
}
setupTools() {
// Campaign Management Tools
this.setupCampaignTools();
// Lead Management Tools
this.setupLeadTools();
// Messaging Tools
this.setupMessagingTools();
// Social Action Tools
this.setupSocialTools();
// Analytics Tools
this.setupAnalyticsTools();
}
setupCampaignTools() {
// Get all campaigns
this.server.tool('get-all-campaigns', {
description: 'Retrieve all campaigns from your HeyReach account.'
}, async () => {
const result = await this.heyReachClient.getAllCampaigns();
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: JSON.stringify(result.data, null, 2)
}]
};
});
// Get campaign details
this.server.tool('get-campaign-details', {
description: 'Get detailed information about a specific campaign.',
campaignId: z.string().describe('Campaign ID to retrieve details for. Use get-all-campaigns to find valid IDs.')
}, async ({ campaignId }) => {
const result = await this.heyReachClient.getCampaignDetails(campaignId);
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: JSON.stringify(result.data, null, 2)
}]
};
});
// Create campaign
this.server.tool('create-campaign', {
name: z.string().describe(`The name of the new campaign to create.
**Prerequisites:**
- Valid HeyReach API key must be configured
- API key must have campaign creation permissions
- No other API calls required before using this tool
**API Endpoint:** POST /campaign/Create
**Request Payload Structure:**
\`\`\`json
{
"name": "Q1 LinkedIn Outreach",
"description": "Quarterly outreach campaign targeting tech executives",
"settings": {
"dailyLimit": 50,
"delayBetweenActions": 30,
"workingHours": {
"start": "09:00",
"end": "17:00"
}
},
"messageTemplates": ["template_123", "template_456"]
}
\`\`\`
**Parameter Details:**
- name (string, required): Campaign name (1-100 characters)
- Example: "Q1 LinkedIn Outreach", "Tech Executive Campaign"
- Must be unique within your account`),
description: z.string().optional().describe(`Optional description of the campaign's purpose and target audience.
- description (string, optional): Detailed campaign description (max 500 characters)
- Example: "Targeting CTOs and VPs of Engineering at Series B+ startups"
- Used for internal organization and reporting`),
dailyLimit: z.number().optional().describe(`Maximum number of actions (messages, connection requests) to perform per day.
- dailyLimit (number, optional): Daily action limit (1-200, default: 50)
- Example: 25 for conservative approach, 100 for aggressive outreach
- Helps maintain LinkedIn compliance and avoid account restrictions`),
delayBetweenActions: z.number().optional().describe(`Time delay between individual actions in minutes to appear more human-like.
- delayBetweenActions (number, optional): Delay in minutes (5-120, default: 30)
- Example: 15 for faster campaigns, 60 for more natural pacing
- Reduces risk of LinkedIn detection and account limitations`)
}, async ({ name, description, dailyLimit, delayBetweenActions }) => {
const params = {
name,
description,
settings: {
dailyLimit,
delayBetweenActions
}
};
const result = await this.heyReachClient.createCampaign(params);
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Campaign created successfully: ${JSON.stringify(result.data, null, 2)}`
}]
};
});
// Pause/Resume campaign
this.server.tool('toggle-campaign-status', {
campaignId: z.string().describe(`The unique identifier of the campaign to pause or resume.
**Prerequisites:**
- Valid HeyReach API key must be configured
- Campaign must exist in your HeyReach account
- Recommended: Use 'get-all-campaigns' or 'get-campaign-details' to verify campaign exists and current status
**API Endpoint:** POST /campaign/{action}
**Request Payload:**
\`\`\`json
{
"campaignId": "camp_123456"
}
\`\`\`
**Parameter Details:**
- campaignId (string, required): The unique campaign identifier
- Format: Alphanumeric string, typically prefixed with "camp_"
- Example: "camp_123456", "campaign_abc789"
- Must be an existing campaign in your account`),
action: z.enum(['pause', 'resume']).describe(`The action to perform on the campaign.
**Parameter Details:**
- action (enum, required): Must be either "pause" or "resume"
- "pause": Stops all automated actions for this campaign
- Use when: Temporarily stopping outreach, reviewing campaign performance, or making adjustments
- Effect: No new messages or connection requests will be sent
- "resume": Restarts automated actions for a paused campaign
- Use when: Ready to continue outreach after pausing
- Effect: Campaign will resume sending messages according to its schedule and settings
**Response Structure:**
\`\`\`json
{
"success": true,
"data": {
"id": "camp_123456",
"name": "Q1 LinkedIn Outreach",
"status": "paused",
"updatedAt": "2024-01-20T15:30:00Z"
}
}
\`\`\`
**Common Error Scenarios:**
- 404 Not Found: Campaign ID does not exist
- 400 Bad Request: Invalid action or campaign already in requested state
- 403 Forbidden: API key lacks campaign management permissions
**Usage Example:**
Input: { "campaignId": "camp_123456", "action": "pause" }
Use this to control campaign execution without deleting the campaign or its data.`)
}, async ({ campaignId, action }) => {
const result = await this.heyReachClient.toggleCampaignStatus(campaignId, action);
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Campaign ${action}d successfully: ${JSON.stringify(result.data, null, 2)}`
}]
};
});
}
setupLeadTools() {
// Add leads to campaign
this.server.tool('add-leads-to-campaign', {
campaignId: z.string().describe(`The unique identifier of the campaign to add leads to.
**Prerequisites:**
- Valid HeyReach API key must be configured
- Campaign must exist and be accessible with your API key
- Recommended: Use 'get-all-campaigns' to verify campaign exists before adding leads
**API Endpoint:** POST /campaign/AddLeadsToListV2
**Request Payload Structure:**
\`\`\`json
{
"campaignId": "camp_123456",
"leads": [
{
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@techcorp.com",
"linkedinUrl": "https://linkedin.com/in/johnsmith",
"company": "TechCorp Inc",
"position": "VP of Engineering"
}
]
}
\`\`\`
**Parameter Details:**
- campaignId (string, required): The target campaign identifier
- Format: Alphanumeric string, typically prefixed with "camp_"
- Example: "camp_123456", "campaign_abc789"
- Must be an existing, active campaign`),
leads: z.array(z.object({
firstName: z.string().optional().describe(`First name of the lead contact.
- firstName (string, optional): Lead's first name (1-50 characters)
- Example: "John", "Sarah", "Michael"
- Used for message personalization and contact identification`),
lastName: z.string().optional().describe(`Last name of the lead contact.
- lastName (string, optional): Lead's last name (1-50 characters)
- Example: "Smith", "Johnson", "Williams"
- Combined with firstName for full name display and personalization`),
email: z.string().optional().describe(`Email address of the lead for contact and tracking.
- email (string, optional): Valid email address format
- Example: "john.smith@techcorp.com", "sarah.j@startup.io"
- Used for email outreach and lead identification
- Must be valid email format if provided`),
linkedinUrl: z.string().optional().describe(`LinkedIn profile URL for social outreach and verification.
- linkedinUrl (string, optional): Full LinkedIn profile URL
- Example: "https://linkedin.com/in/johnsmith", "https://www.linkedin.com/in/sarah-johnson-123456"
- Used for LinkedIn connection requests and profile verification
- Must be valid LinkedIn URL format if provided`),
company: z.string().optional().describe(`Company name where the lead currently works.
- company (string, optional): Current employer name (1-100 characters)
- Example: "TechCorp Inc", "Startup Solutions LLC", "Global Enterprises"
- Used for targeting and message personalization`),
position: z.string().optional().describe(`Job title or position of the lead at their company.
- position (string, optional): Current job title (1-100 characters)
- Example: "VP of Engineering", "Senior Software Developer", "Chief Technology Officer"
- Used for targeting and message personalization`)
})).describe(`Array of lead objects to add to the campaign.
**Lead Array Details:**
- Minimum 1 lead, maximum 1000 leads per request
- At least one field (firstName, lastName, email, linkedinUrl, company, or position) must be provided per lead
- LinkedIn URL is highly recommended for LinkedIn-based campaigns
- Duplicate leads (same email or LinkedIn URL) will be automatically filtered
**Response Structure:**
\`\`\`json
{
"success": true,
"data": {
"addedCount": 5
},
"message": "Successfully added 5 leads to campaign"
}
\`\`\`
**Common Error Scenarios:**
- 404 Not Found: Campaign ID does not exist
- 400 Bad Request: Invalid lead data format or empty leads array
- 403 Forbidden: API key lacks campaign modification permissions
- 422 Unprocessable Entity: Duplicate leads or invalid email/LinkedIn URL formats
**Usage Example:**
Input: {
"campaignId": "camp_123456",
"leads": [
{
"firstName": "John",
"lastName": "Smith",
"email": "john@techcorp.com",
"linkedinUrl": "https://linkedin.com/in/johnsmith",
"company": "TechCorp",
"position": "CTO"
}
]
}`)
}, async ({ campaignId, leads }) => {
const result = await this.heyReachClient.addLeadsToCampaign({ campaignId, leads });
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Successfully added ${result.data?.addedCount} leads to campaign ${campaignId}`
}]
};
});
// Get campaign leads
this.server.tool('get-campaign-leads', {
campaignId: z.string().describe(`The unique identifier of the campaign to retrieve leads from.
**Prerequisites:**
- Valid HeyReach API key must be configured
- Campaign must exist and be accessible with your API key
- Recommended: Use 'get-all-campaigns' to verify campaign exists before retrieving leads
**API Endpoint:** POST /campaign/GetLeads
**Request Payload:**
\`\`\`json
{
"campaignId": "camp_123456",
"page": 1,
"limit": 50
}
\`\`\`
**Parameter Details:**
- campaignId (string, required): The campaign identifier to retrieve leads from
- Format: Alphanumeric string, typically prefixed with "camp_"
- Example: "camp_123456", "campaign_abc789"
- Must be an existing campaign with leads`),
page: z.number().optional().default(1).describe(`Page number for pagination to navigate through large lead lists.
**Parameter Details:**
- page (number, optional, default: 1): Page number for pagination (minimum: 1)
- Example: 1 for first page, 2 for second page, etc.
- Used to navigate through large lead lists
- Each page contains up to 'limit' number of leads`),
limit: z.number().optional().default(50).describe(`Number of leads to return per page.
**Parameter Details:**
- limit (number, optional, default: 50): Number of leads per page (1-100)
- Example: 25 for smaller pages, 100 for maximum per page
- Larger limits reduce API calls but increase response size
- Maximum allowed: 100 leads per request
**Response Structure:**
\`\`\`json
{
"success": true,
"data": [
{
"id": "lead_123456",
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@techcorp.com",
"linkedinUrl": "https://linkedin.com/in/johnsmith",
"company": "TechCorp Inc",
"position": "VP of Engineering",
"status": "pending",
"campaignId": "camp_123456",
"addedAt": "2024-01-15T10:00:00Z",
"lastActivity": "2024-01-20T14:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 150,
"hasMore": true
}
}
\`\`\`
**Lead Status Values:**
- "pending": Lead added but no actions taken yet
- "contacted": Initial message or connection request sent
- "replied": Lead has responded to outreach
- "connected": LinkedIn connection established
- "not_interested": Lead indicated no interest
- "bounced": Email bounced or LinkedIn request rejected
**Common Error Scenarios:**
- 404 Not Found: Campaign ID does not exist
- 400 Bad Request: Invalid page number or limit value
- 403 Forbidden: API key lacks campaign read permissions
**Usage Example:**
Input: { "campaignId": "camp_123456", "page": 1, "limit": 25 }
Use this to review leads in a campaign, monitor their status, and track outreach progress.`)
}, async ({ campaignId, page, limit }) => {
const result = await this.heyReachClient.getCampaignLeads(campaignId, page, limit);
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: JSON.stringify({
leads: result.data,
pagination: result.pagination
}, null, 2)
}]
};
});
// Update lead status
this.server.tool('update-lead-status', {
leadId: z.string().describe(`The unique identifier of the lead to update status for.
**Prerequisites:**
- Valid HeyReach API key must be configured
- Lead must exist in your HeyReach account
- Recommended: Use 'get-campaign-leads' to obtain valid lead IDs and current status
**API Endpoint:** POST /lead/UpdateStatus
**Request Payload:**
\`\`\`json
{
"leadId": "lead_123456",
"status": "contacted"
}
\`\`\`
**Parameter Details:**
- leadId (string, required): The unique lead identifier
- Format: Alphanumeric string, typically prefixed with "lead_"
- Example: "lead_123456", "lead_abc789"
- Must be an existing lead in your campaigns`),
status: z.enum(['pending', 'contacted', 'replied', 'connected', 'not_interested', 'bounced'])
.describe(`The new status to assign to the lead.
**Status Options and Usage:**
- "pending": Lead added but no actions taken yet
- Use when: Resetting a lead back to initial state
- Next actions: Ready for automated outreach to begin
- "contacted": Initial message or connection request sent
- Use when: First outreach attempt has been made
- Next actions: Wait for response or schedule follow-up
- "replied": Lead has responded to outreach
- Use when: Lead has sent a message back
- Next actions: Continue conversation or schedule meeting
- "connected": LinkedIn connection established
- Use when: LinkedIn connection request was accepted
- Next actions: Send follow-up message or direct outreach
- "not_interested": Lead indicated no interest
- Use when: Lead explicitly declined or showed no interest
- Next actions: Remove from active outreach, add to exclusion list
- "bounced": Email bounced or LinkedIn request rejected
- Use when: Technical delivery failure occurred
- Next actions: Verify contact information or try alternative channels
**Response Structure:**
\`\`\`json
{
"success": true,
"data": {
"id": "lead_123456",
"firstName": "John",
"lastName": "Smith",
"status": "contacted",
"lastActivity": "2024-01-20T15:30:00Z",
"campaignId": "camp_123456"
}
}
\`\`\`
**Common Error Scenarios:**
- 404 Not Found: Lead ID does not exist
- 400 Bad Request: Invalid status value
- 403 Forbidden: API key lacks lead modification permissions
- 422 Unprocessable Entity: Status transition not allowed (e.g., bounced to replied)
**Usage Example:**
Input: { "leadId": "lead_123456", "status": "contacted" }
Use this to manually update lead status or correct automated status tracking.`)
}, async ({ leadId, status }) => {
const result = await this.heyReachClient.updateLeadStatus(leadId, status);
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Lead status updated successfully: ${JSON.stringify(result.data, null, 2)}`
}]
};
});
}
setupMessagingTools() {
// Send message
this.server.tool('send-message', {
leadId: z.string().describe(`The unique identifier of the lead to send a message to.
**Prerequisites:**
- Valid HeyReach API key must be configured
- Lead must exist in your HeyReach account
- Lead must have a valid LinkedIn URL or email address
- Recommended: Use 'get-campaign-leads' to obtain valid lead IDs and verify contact information
**API Endpoint:** POST /message/Send
**Request Payload:**
\`\`\`json
{
"leadId": "lead_123456",
"message": "Hi John, I noticed your work at TechCorp and would love to connect...",
"templateId": "template_789"
}
\`\`\`
**Parameter Details:**
- leadId (string, required): The unique lead identifier
- Format: Alphanumeric string, typically prefixed with "lead_"
- Example: "lead_123456", "lead_abc789"
- Must be an existing lead with valid contact information`),
message: z.string().describe(`The message content to send to the lead.
**Parameter Details:**
- message (string, required): The message text to send (1-2000 characters)
- Example: "Hi John, I noticed your work at TechCorp and would love to connect to discuss our mutual interests in AI technology."
- Should be personalized and relevant to the lead
- Avoid spam-like language or overly promotional content
- Can include variables that will be replaced with lead information
**Message Best Practices:**
- Keep messages concise and personalized
- Reference the lead's company, position, or recent activity
- Include a clear call-to-action
- Avoid excessive links or promotional language
- Respect LinkedIn's messaging guidelines`),
templateId: z.string().optional().describe(`Optional template ID to use for message formatting and variable replacement.
**Parameter Details:**
- templateId (string, optional): The unique template identifier
- Format: Alphanumeric string, typically prefixed with "template_"
- Example: "template_123", "template_connection_request"
- Use 'get-message-templates' to obtain valid template IDs
**Template Usage:**
- Templates provide pre-written message structures with variables
- Variables like {{firstName}}, {{company}}, {{position}} are automatically replaced
- Helps maintain consistent messaging across campaigns
- If templateId is provided, the 'message' parameter may be ignored in favor of template content
**Response Structure:**
\`\`\`json
{
"success": true,
"data": {
"messageId": "msg_123456"
},
"message": "Message sent successfully"
}
\`\`\`
**Common Error Scenarios:**
- 404 Not Found: Lead ID does not exist
- 400 Bad Request: Invalid message content or missing contact information
- 403 Forbidden: API key lacks messaging permissions
- 422 Unprocessable Entity: Lead has no valid contact method (LinkedIn/email)
- 429 Too Many Requests: Daily messaging limit exceeded
**Usage Example:**
Input: {
"leadId": "lead_123456",
"message": "Hi John, I saw your recent post about AI in healthcare. Would love to connect!",
"templateId": "template_connection_request"
}`)
}, async ({ leadId, message, templateId }) => {
const result = await this.heyReachClient.sendMessage({ leadId, message, templateId });
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Message sent successfully. Message ID: ${result.data?.messageId}`
}]
};
});
// Get message templates
this.server.tool('get-message-templates', {
description: `Retrieve all available message templates from your HeyReach account.
**Prerequisites:**
- Valid HeyReach API key must be configured
- No other API calls required before using this tool
**API Endpoint:** GET /templates/GetAll
**Request Payload:** None
**Response Structure:**
\`\`\`json
{
"success": true,
"data": [
{
"id": "template_123456",
"name": "Connection Request - Tech Executives",
"content": "Hi {{firstName}}, I noticed your work at {{company}} and would love to connect to discuss {{industry}} trends.",
"type": "connection_request",
"variables": ["firstName", "company", "industry"]
},
{
"id": "template_789012",
"name": "Follow-up Message",
"content": "Thanks for connecting, {{firstName}}! I'd love to learn more about your role as {{position}} at {{company}}.",
"type": "follow_up",
"variables": ["firstName", "position", "company"]
}
]
}
\`\`\`
**Template Types:**
- "connection_request": Initial LinkedIn connection request messages
- "follow_up": Follow-up messages after connection acceptance
- "direct_message": Direct messages for existing connections
**Template Variables:**
Common variables that can be used in templates:
- {{firstName}}: Lead's first name
- {{lastName}}: Lead's last name
- {{company}}: Lead's company name
- {{position}}: Lead's job title
- {{industry}}: Lead's industry (if available)
- {{customField}}: Any custom field defined for the lead
**Common Error Scenarios:**
- 401 Unauthorized: Invalid or expired API key
- 403 Forbidden: API key lacks template read permissions
- 500 Internal Server Error: HeyReach service temporarily unavailable
**Usage Example:**
This tool requires no parameters and returns all message templates available in your account.
Use the returned template IDs with the 'send-message' tool for consistent messaging.`
}, async () => {
const result = await this.heyReachClient.getMessageTemplates();
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: JSON.stringify(result.data, null, 2)
}]
};
});
}
setupSocialTools() {
// Perform social action
this.server.tool('perform-social-action', {
action: z.enum(['like', 'follow', 'view']).describe(`The type of social action to perform on LinkedIn.
**Prerequisites:**
- Valid HeyReach API key must be configured
- LinkedIn account must be connected to HeyReach
- Target URL must be accessible and valid
- Recommended: Verify lead exists if associating action with a lead
**API Endpoint:** POST /social/Action
**Request Payload:**
\`\`\`json
{
"action": "like",
"targetUrl": "https://linkedin.com/posts/johnsmith_ai-technology-innovation-activity-1234567890",
"leadId": "lead_123456"
}
\`\`\`
**Action Types and Usage:**
- "like": Like a LinkedIn post or update
- Use when: Engaging with lead's content to build rapport
- Target: LinkedIn post URLs, company updates, or shared content
- Effect: Increases visibility and shows interest in lead's content
- "follow": Follow a LinkedIn profile or company page
- Use when: Building long-term relationship with lead or their company
- Target: LinkedIn profile URLs or company page URLs
- Effect: Establishes ongoing connection and content visibility
- "view": View a LinkedIn profile
- Use when: Researching lead or showing interest without direct engagement
- Target: LinkedIn profile URLs
- Effect: Appears in "Who viewed your profile" notifications`),
targetUrl: z.string().describe(`The LinkedIn URL to perform the action on.
**Parameter Details:**
- targetUrl (string, required): Valid LinkedIn URL (posts, profiles, company pages)
- Post URL example: "https://linkedin.com/posts/johnsmith_ai-technology-innovation-activity-1234567890"
- Profile URL example: "https://linkedin.com/in/johnsmith"
- Company URL example: "https://linkedin.com/company/techcorp"
- Must be a publicly accessible LinkedIn URL
- URL must match the action type (e.g., post URLs for likes, profile URLs for follows/views)
**URL Validation:**
- Must start with "https://linkedin.com/" or "https://www.linkedin.com/"
- Must be a valid, active LinkedIn page
- Private profiles or restricted content may cause action failures`),
leadId: z.string().optional().describe(`Optional lead ID to associate this social action with for tracking and campaign management.
**Parameter Details:**
- leadId (string, optional): The unique lead identifier
- Format: Alphanumeric string, typically prefixed with "lead_"
- Example: "lead_123456", "lead_abc789"
- Use 'get-campaign-leads' to obtain valid lead IDs
**Association Benefits:**
- Links social actions to specific leads for tracking
- Enables campaign performance analysis
- Helps maintain engagement history per lead
- Useful for automated follow-up sequences
**Response Structure:**
\`\`\`json
{
"success": true,
"data": {
"id": "action_123456",
"type": "like",
"targetUrl": "https://linkedin.com/posts/johnsmith_ai-technology-innovation-activity-1234567890",
"status": "pending",
"leadId": "lead_123456",
"scheduledAt": "2024-01-20T16:00:00Z"
}
}
\`\`\`
**Action Status Values:**
- "pending": Action queued for execution
- "completed": Action successfully performed
- "failed": Action failed due to error or restriction
**Common Error Scenarios:**
- 400 Bad Request: Invalid URL format or unsupported action type
- 403 Forbidden: LinkedIn account not connected or insufficient permissions
- 404 Not Found: Target URL does not exist or is no longer accessible
- 429 Too Many Requests: Daily social action limit exceeded
- 422 Unprocessable Entity: Action not allowed (e.g., already liked, already following)
**Usage Example:**
Input: {
"action": "like",
"targetUrl": "https://linkedin.com/posts/johnsmith_ai-technology-innovation-activity-1234567890",
"leadId": "lead_123456"
}`)
}, async ({ action, targetUrl, leadId }) => {
const result = await this.heyReachClient.performSocialAction({ action, targetUrl, leadId });
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `Social action (${action}) queued successfully: ${JSON.stringify(result.data, null, 2)}`
}]
};
});
}
setupAnalyticsTools() {
// Get campaign metrics
this.server.tool('get-campaign-metrics', {
campaignId: z.string().describe(`The unique identifier of the campaign to retrieve performance metrics for.
**Prerequisites:**
- Valid HeyReach API key must be configured
- Campaign must exist and be accessible with your API key
- Recommended: Use 'get-all-campaigns' to verify campaign exists and has been running
**API Endpoint:** GET /analytics/campaign/{campaignId}
**Request Payload:** None (campaign ID passed in URL path)
**Parameter Details:**
- campaignId (string, required): The unique campaign identifier
- Format: Alphanumeric string, typically prefixed with "camp_"
- Example: "camp_123456", "campaign_abc789"
- Must be an existing campaign with some activity for meaningful metrics
**Response Structure:**
\`\`\`json
{
"success": true,
"data": {
"campaignId": "camp_123456",
"totalLeads": 150,
"contacted": 120,
"replied": 25,
"connected": 45,
"responseRate": 20.8,
"connectionRate": 37.5,
"metrics": {
"messagesPerDay": 15.5,
"averageResponseTime": "2.3 days",
"topPerformingTemplate": "template_123",
"conversionFunnel": {
"leads": 150,
"contacted": 120,
"replied": 25,
"meetings": 8,
"deals": 2
}
},
"timeRange": {
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-01-31T23:59:59Z"
}
}
}
\`\`\`
**Metric Definitions:**
- totalLeads: Total number of leads in the campaign
- contacted: Number of leads that have been reached out to
- replied: Number of leads that responded to outreach
- connected: Number of LinkedIn connections established
- responseRate: Percentage of contacted leads who replied (replied/contacted * 100)
- connectionRate: Percentage of contacted leads who connected (connected/contacted * 100)
**Performance Indicators:**
- Response Rate > 15%: Good performance
- Response Rate 10-15%: Average performance
- Response Rate < 10%: Needs optimization
- Connection Rate > 30%: Excellent LinkedIn engagement
- Connection Rate 20-30%: Good LinkedIn engagement
- Connection Rate < 20%: Consider improving connection requests
**Common Error Scenarios:**
- 404 Not Found: Campaign ID does not exist
- 403 Forbidden: API key lacks analytics read permissions
- 400 Bad Request: Invalid campaign ID format
- 204 No Content: Campaign exists but has no activity data yet
**Usage Example:**
Input: { "campaignId": "camp_123456" }
Use this to monitor campaign performance, identify optimization opportunities, and report on outreach effectiveness.`)
}, async ({ campaignId }) => {
const result = await this.heyReachClient.getCampaignMetrics(campaignId);
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: JSON.stringify(result.data, null, 2)
}]
};
});
// Check API key
this.server.tool('check-api-key', {
description: `Verify that your HeyReach API key is valid and has proper permissions.
**Prerequisites:**
- HeyReach API key must be configured in the MCP server
- No other API calls required before using this tool
**API Endpoint:** GET /auth/CheckApiKey
**Request Payload:** None
**Response Structure:**
\`\`\`json
{
"success": true,
"data": true,
"message": "API key is valid (Status: 200)"
}
\`\`\`
**Validation Results:**
- Valid API Key: Returns success: true, data: true
- Invalid API Key: Returns success: false with error message
- Expired API Key: Returns 401 Unauthorized error
- Malformed API Key: Returns 400 Bad Request error
**API Key Requirements:**
- Must be a valid HeyReach API key from your account settings
- Format: Base64-encoded string (typically 40+ characters)
- Example format: "QGUYbd7rkBqswN0otgk8KvzCVRZ+h7Tiz0onFETzF6M="
- Must have appropriate permissions for intended operations
**Permission Levels:**
- Read-only: Can view campaigns, leads, and metrics
- Campaign Management: Can create, modify, and control campaigns
- Lead Management: Can add, update, and manage leads
- Messaging: Can send messages and access templates
- Social Actions: Can perform LinkedIn actions
- Full Access: All permissions enabled
**Common Error Scenarios:**
- 401 Unauthorized: API key is invalid, expired, or revoked
- 403 Forbidden: API key is valid but lacks required permissions
- 400 Bad Request: API key format is incorrect
- 500 Internal Server Error: HeyReach authentication service unavailable
**Troubleshooting:**
- If invalid: Generate a new API key in HeyReach account settings
- If forbidden: Check API key permissions in HeyReach dashboard
- If service unavailable: Retry after a few minutes
**Usage Example:**
This tool requires no parameters and validates the currently configured API key.
Use this as a first step to ensure your HeyReach integration is properly configured before using other tools.`
}, async () => {
const result = await this.heyReachClient.checkApiKey();
if (!result.success) {
return {
content: [{
type: 'text',
text: `Error: ${result.error}`
}],
isError: true
};
}
return {
content: [{
type: 'text',
text: `API Key is ${result.data ? 'valid' : 'invalid'}`
}]
};
});
}
getServer() {
return this.server;
}
}