@iriseller/mcp-server
Version:
Model Context Protocol (MCP) server providing access to IRISeller's AI sales intelligence platform with 7 AI agents, multi-CRM integration, advanced sales workflows, email automation, Rosa demo functionality with action scoring, DNC compliance checking, G
3,930 lines ⢠199 kB
JavaScript
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { WebSearchService } from '../services/web-search-service.js';
import { StockDataService } from '../services/stock-data-service.js';
import { EmailToolHandlers } from './email-handlers.js';
import { CRMQuerySchema, CompanyResearchRequestSchema, PersonalizationRequestSchema, AgentExecutionRequestSchema, WorkflowExecutionRequestSchema } from '../types/index.js';
export class ToolHandlers {
apiService;
webSearchService;
stockDataService;
emailHandlers;
userToken; // Store user token for CRM operations
// Debug logging utilities that respect debug flag
debugLog = (message, ...args) => {
if (this.apiService.config?.debug) {
console.error(message, ...args);
}
};
errorLog = (message, ...args) => {
// Always log errors to stderr (for critical issues only)
console.error(message, ...args);
};
constructor(apiService) {
this.apiService = apiService;
this.webSearchService = new WebSearchService();
this.stockDataService = new StockDataService();
this.emailHandlers = new EmailToolHandlers(apiService);
}
// Method to set user token for authenticated operations
setUserToken(token) {
this.userToken = token;
this.apiService.setUserToken(token);
}
async handleToolCall(request) {
try {
switch (request.params.name) {
case 'qualify_lead':
return await this.handleQualifyLead(request);
case 'query_crm':
return await this.handleQueryCRM(request);
case 'research_company':
return await this.handleResearchCompany(request);
case 'execute_agent':
return await this.handleExecuteAgent(request);
case 'execute_workflow':
return await this.handleExecuteWorkflow(request);
case 'personalize_outreach':
return await this.handlePersonalizeOutreach(request);
case 'get_agent_results':
return await this.handleGetAgentResults(request);
case 'wait_for_agent_completion':
return await this.handleWaitForAgentCompletion(request);
case 'forecast_sales':
return await this.handleForecastSales(request);
case 'list_agents':
return await this.handleListAgents(request);
case 'list_workflows':
return await this.handleListWorkflows(request);
case 'health_check':
return await this.handleHealthCheck(request);
case 'web_search':
return await this.handleWebSearch(request);
case 'get_stock_data':
return await this.handleGetStockData(request);
case 'get_maps_location':
return await this.handleGoogleMapsLookup(request);
case 'check_dnc_compliance':
return await this.handleDNCComplianceCheck(request);
case 'rosa_daily_briefing':
return await this.handleRosaDailyBriefing(request);
case 'prepare_meeting':
return await this.handleMeetingPrep(request);
case 'priority_tasks':
return await this.handlePriorityTasks(request);
// Email Integration Tools
case 'email_auto_respond':
return await this.handleEmailAutoRespond(request);
case 'analyze_email_intent':
return await this.handleEmailIntentAnalysis(request);
case 'compile_sales_info':
return await this.handleCompileSalesInfo(request);
case 'generate_email_response':
return await this.handleGenerateEmailResponse(request);
case 'schedule_email_followup':
return await this.handleScheduleEmailFollowup(request);
case 'get_email_analytics':
return await this.handleEmailAnalytics(request);
// New Email Detection & Sending Tools
case 'detect_emails':
return await this.handleDetectEmails(request);
case 'send_email':
return await this.handleSendEmail(request);
case 'get_email_status':
return await this.handleGetEmailStatus(request);
case 'manage_email_templates':
return await this.handleManageEmailTemplates(request);
case 'manage_email_campaigns':
return await this.handleManageEmailCampaigns(request);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
this.errorLog(`[MCP] Tool execution error for ${request.params.name}:`, error);
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleQualifyLead(request) {
const args = request.params.arguments || {};
// Validate required fields
if (!args.contact_name || !args.company_name) {
throw new McpError(ErrorCode.InvalidParams, 'contact_name and company_name are required');
}
try {
// For now, skip database creation and let CrewAI handle execution tracking
// The email automation system should work with CrewAI execution_ids
this.debugLog(`[MCP] Processing qualify_lead for: ${args.contact_name} at ${args.company_name}`);
const result = await this.apiService.qualifyLead({
contact_name: args.contact_name,
company_name: args.company_name,
title: args.title,
industry: args.industry,
email: args.email,
phone: args.phone,
additional_context: args.additional_context
});
// Handle both success and error cases
if (result.status === 'error') {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'qualify_lead',
execution_id: result.execution_id,
status: 'error',
error: result.error || 'Lead qualification failed',
contact: args.contact_name,
company: args.company_name,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'qualify_lead',
execution_id: result.execution_id, // Use CrewAI execution_id for follow-up tracking
status: result.status,
lead_qualification: {
contact: args.contact_name,
company: args.company_name,
qualification_score: result.results?.enhanced_qualification?.enhanced_score || result.results?.bant_analysis?.overall_score || 0,
qualification_status: result.results?.enhanced_qualification?.qualification_status || result.results?.bant_analysis?.qualification_status || 'Unknown',
bant_breakdown: result.results?.bant_analysis || {},
research_insights: result.results?.research_insights || {},
recommendations: result.results?.recommendations || [],
next_actions: result.results?.next_actions || [],
revenue_potential: result.results?.revenue_potential || {}
},
execution_time: result.execution_time,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Lead qualification error:', error);
throw new McpError(ErrorCode.InternalError, `Lead qualification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleQueryCRM(request) {
const args = request.params.arguments || {};
// Validate and parse the query
const parseResult = CRMQuerySchema.safeParse(args);
if (!parseResult.success) {
throw new McpError(ErrorCode.InvalidParams, `Invalid CRM query parameters: ${parseResult.error.message}`);
}
// Smart sorting logic: For leads queries without explicit sort_by OR when sort_by is 'score', default to value-based sorting
const queryData = { ...parseResult.data };
this.debugLog(`[MCP] DEBUG: Raw args:`, args);
this.debugLog(`[MCP] DEBUG: Parsed queryData:`, queryData);
this.debugLog(`[MCP] DEBUG: entity_type=${queryData.entity_type}, sort_by=${queryData.sort_by}`);
const needsRosaScoring = queryData.entity_type === 'leads' && (!queryData.sort_by || queryData.sort_by === 'score' || queryData.sort_by === 'action_score');
this.debugLog(`[MCP] DEBUG: needsRosaScoring=${needsRosaScoring}`);
let originalLimit = queryData.limit;
if (needsRosaScoring) {
// For Rosa-style scoring, fetch more records to sort client-side with enhanced action scores
// since CRM Connect Gateway may not support advanced action score sorting
queryData.limit = Math.max(100, queryData.limit || 10); // Fetch at least 100 records
this.debugLog(`[MCP] Fetching ${queryData.limit} leads for Rosa-style action scoring (original limit: ${originalLimit})`);
this.debugLog(`[MCP] DEBUG: needsRosaScoring=true, entity_type=${queryData.entity_type}, sort_by=${parseResult.data.sort_by}`);
// Remove sort_by to let CRM return records in default order
const modifiedQuery = { ...queryData };
delete modifiedQuery.sort_by;
delete modifiedQuery.sort_order;
Object.assign(queryData, modifiedQuery);
}
try {
// Pass user token to CRM query for user-specific API key access
const result = await this.apiService.queryCRM(queryData, this.userToken);
let finalData = result.data;
let finalQuery = queryData;
// Check if data was owner-filtered (security update)
const isOwnerFiltered = result.metadata?.ownerFiltered === true;
if (isOwnerFiltered) {
this.debugLog(`[MCP] CRM data is owner-filtered for security compliance`);
}
// Apply client-side sorting if needed
if (needsRosaScoring && result.success && Array.isArray(result.data)) {
this.debugLog(`[MCP] Applying Rosa-style action scoring to ${result.data.length} leads`);
// Calculate Rosa-style action scores for each lead
const scoredData = result.data.map((lead) => ({
...lead,
actionScore: this.calculateRosaActionScore(lead),
rosaCategory: this.determineRosaCategory(lead)
}));
// Sort by action score (descending)
const sortedData = scoredData.sort((a, b) => {
return b.actionScore - a.actionScore; // Descending order
});
// Limit to original requested amount
finalData = sortedData.slice(0, originalLimit || 10);
// Update query info to reflect what we actually did
finalQuery = {
...queryData,
sort_by: 'action_score',
sort_order: 'desc',
limit: originalLimit
};
const topLead = finalData[0];
const leadName = topLead ? `${topLead.FirstName || topLead.firstName || topLead.name || 'Unknown'} ${topLead.LastName || topLead.lastName || ''}`.trim() : 'None';
const actionScore = topLead ? topLead.actionScore : 0;
this.debugLog(`[MCP] Rosa action scoring complete. Top lead: ${leadName} (Action Score: ${actionScore})`);
this.debugLog(`[MCP] DEBUG: Sorted ${finalData.length} leads by action score. Top 3: ${finalData.slice(0, 3).map((l) => `${l.FirstName} ${l.LastName} (${l.actionScore})`).join(', ')}`);
}
// Enhanced response with security metadata
const responseData = {
tool: 'query_crm',
query: finalQuery,
success: result.success,
data: finalData,
count: Array.isArray(finalData) ? finalData.length : 0,
message: result.message,
error: result.error,
metadata: {
...result.metadata,
ownerFiltered: isOwnerFiltered,
securityCompliant: true
},
timestamp: new Date().toISOString()
};
// Add security notice for opportunity queries
let responseText = JSON.stringify(responseData, null, 2);
if (queryData.entity_type === 'opportunities' && isOwnerFiltered) {
responseText += '\n\nš Security Notice: Results are filtered to show only opportunities you own for data security and compliance.';
}
return {
content: [
{
type: 'text',
text: responseText
}
]
};
}
catch (error) {
this.errorLog('[MCP] CRM query error:', error);
// Enhanced error handling for owner filtering scenarios
let errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Check if this is a 404 error for opportunities (likely due to owner filtering)
if (queryData.entity_type === 'opportunities' && errorMessage.includes('404')) {
errorMessage = `No opportunities found that you own. This is due to security filtering that ensures you only see your own opportunities.`;
}
else if (queryData.entity_type === 'opportunities' && errorMessage.includes('403')) {
errorMessage = `Access denied to opportunities. Please check your CRM connection permissions.`;
}
throw new McpError(ErrorCode.InternalError, `CRM query failed: ${errorMessage}`);
}
}
async handleResearchCompany(request) {
const args = request.params.arguments || {};
// Validate and parse the research request
const parseResult = CompanyResearchRequestSchema.safeParse(args);
if (!parseResult.success) {
throw new McpError(ErrorCode.InvalidParams, `Invalid company research parameters: ${parseResult.error.message}`);
}
try {
const result = await this.apiService.researchCompany(parseResult.data);
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'research_company',
company: parseResult.data.company_name,
research_depth: parseResult.data.research_depth,
success: result.success,
research_data: result.data,
message: result.message,
error: result.error,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Company research error:', error);
throw new McpError(ErrorCode.InternalError, `Company research failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleExecuteAgent(request) {
const args = request.params.arguments || {};
// Validate and parse the agent execution request
const parseResult = AgentExecutionRequestSchema.safeParse(args);
if (!parseResult.success) {
throw new McpError(ErrorCode.InvalidParams, `Invalid agent execution parameters: ${parseResult.error.message}`);
}
try {
// First validate that the agent exists
const availableAgents = await this.apiService.getAvailableAgents();
if (!availableAgents.success) {
this.debugLog('[MCP] Could not validate agent availability, proceeding with execution');
}
else {
const validAgents = availableAgents.data?.map((agent) => agent.name) || [];
if (validAgents.length > 0 && !validAgents.includes(parseResult.data.agent_name)) {
throw new McpError(ErrorCode.InvalidParams, `Agent '${parseResult.data.agent_name}' not found. Available agents: ${validAgents.join(', ')}`);
}
}
const result = await this.apiService.executeAgent(parseResult.data);
// If the agent is running/pending, provide guidance on how to get results
if (result.status === 'pending' || result.status === 'running') {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'execute_agent',
agent_name: result.agent_name,
execution_id: result.execution_id,
status: result.status,
results: result.results,
execution_time: result.execution_time,
error: result.error,
next_steps: {
message: "Agent execution is running in the background. Use wait_for_agent_completion to get the final results.",
recommended_action: `Use wait_for_agent_completion with execution_id: "${result.execution_id}" to wait for and receive the final results`,
alternative_action: `Or use get_agent_results with execution_id: "${result.execution_id}" and wait_for_completion: true for shorter waits`,
polling_suggestion: "You can also check periodically without waiting by using get_agent_results with wait_for_completion: false"
},
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'execute_agent',
agent_name: result.agent_name,
execution_id: result.execution_id,
status: result.status,
results: result.results,
execution_time: result.execution_time,
error: result.error,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Agent execution error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Agent execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleExecuteWorkflow(request) {
const args = request.params.arguments || {};
// Validate and parse the workflow execution request
const parseResult = WorkflowExecutionRequestSchema.safeParse(args);
if (!parseResult.success) {
throw new McpError(ErrorCode.InvalidParams, `Invalid workflow execution parameters: ${parseResult.error.message}`);
}
try {
// First validate that the workflow exists
const availableWorkflows = await this.apiService.getAvailableWorkflows();
if (!availableWorkflows.success) {
this.debugLog('[MCP] Could not validate workflow availability, proceeding with execution');
}
else {
const validWorkflows = availableWorkflows.data?.map((workflow) => workflow.name) || [];
if (validWorkflows.length > 0 && !validWorkflows.includes(parseResult.data.workflow_type)) {
throw new McpError(ErrorCode.InvalidParams, `Workflow '${parseResult.data.workflow_type}' not found. Available workflows: ${validWorkflows.join(', ')}`);
}
}
const result = await this.apiService.executeWorkflow(parseResult.data);
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'execute_workflow',
workflow_type: result.workflow_type,
workflow_id: result.workflow_id,
status: result.status,
results: result.results,
agents_executed: result.agents_executed,
execution_time: result.execution_time,
error: result.error,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Workflow execution error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Workflow execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handlePersonalizeOutreach(request) {
const args = request.params.arguments || {};
// Validate and parse the personalization request
const parseResult = PersonalizationRequestSchema.safeParse(args);
if (!parseResult.success) {
throw new McpError(ErrorCode.InvalidParams, `Invalid personalization parameters: ${parseResult.error.message}`);
}
try {
const result = await this.apiService.personalizeOutreach(parseResult.data);
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'personalize_outreach',
prospect: parseResult.data.prospect_data,
message_type: parseResult.data.message_type,
tone: parseResult.data.tone,
success: result.success,
personalized_content: result.data,
message: result.message,
error: result.error,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Personalization error:', error);
throw new McpError(ErrorCode.InternalError, `Personalization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleGetAgentResults(request) {
const args = request.params.arguments || {};
if (!args.execution_id || typeof args.execution_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'execution_id is required and must be a string');
}
const executionId = args.execution_id;
const waitForCompletion = args.wait_for_completion || false;
const timeoutSeconds = Math.min(Math.max(args.timeout_seconds || 30, 5), 300);
try {
let results = await this.apiService.getCompletedExecution(executionId);
// If no results found and wait_for_completion is true, poll for results
if (!results && waitForCompletion) {
this.debugLog(`[MCP] Waiting for agent execution completion: ${executionId}`);
const startTime = Date.now();
const pollInterval = 2000; // 2 seconds
while (!results && (Date.now() - startTime) < (timeoutSeconds * 1000)) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
results = await this.apiService.getCompletedExecution(executionId);
if (results && (results.status === 'completed' || results.status === 'failed')) {
break;
}
}
}
if (!results) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'get_agent_results',
execution_id: executionId,
status: 'not_found',
message: 'Execution not found or still running. Use wait_for_completion=true to wait for results.',
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'get_agent_results',
execution_id: executionId,
status: results.status,
agent_name: results.agent_name || results.agentName,
results: results.results || results.result,
result_summary: results.result_summary || results.resultSummary,
confidence: results.confidence,
execution_time: results.execution_time || results.executionTime,
completed_at: results.completed_at || results.completedAt,
created_at: results.created_at || results.createdAt,
retrieved: results.retrieved,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Get agent results error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Failed to get agent results: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleWaitForAgentCompletion(request) {
const args = request.params.arguments || {};
if (!args.execution_id || typeof args.execution_id !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'execution_id is required and must be a string');
}
const executionId = args.execution_id;
const timeoutSeconds = Math.min(Math.max(args.timeout_seconds || 120, 10), 600);
const checkIntervalSeconds = Math.min(Math.max(args.check_interval_seconds || 5, 2), 30);
try {
this.debugLog(`[MCP] Waiting for agent execution completion: ${executionId} (timeout: ${timeoutSeconds}s, interval: ${checkIntervalSeconds}s)`);
const startTime = Date.now();
let results = null;
let lastStatus = 'unknown';
while ((Date.now() - startTime) < (timeoutSeconds * 1000)) {
results = await this.apiService.getCompletedExecution(executionId);
if (results) {
lastStatus = results.status;
if (results.status === 'completed') {
this.debugLog(`[MCP] Agent execution ${executionId} completed successfully`);
break;
}
else if (results.status === 'failed') {
this.debugLog(`[MCP] Agent execution ${executionId} failed`);
break;
}
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, checkIntervalSeconds * 1000));
}
const waitTime = Math.round((Date.now() - startTime) / 1000);
if (!results) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'wait_for_agent_completion',
execution_id: executionId,
status: 'timeout',
message: `Agent execution not found or still running after ${waitTime} seconds. The execution may still be processing in the background.`,
wait_time_seconds: waitTime,
timeout_seconds: timeoutSeconds,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
if (results.status === 'completed') {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'wait_for_agent_completion',
execution_id: executionId,
status: 'completed',
agent_name: results.agent_name || results.agentName,
results: results.results || results.result,
result_summary: results.result_summary || results.resultSummary,
confidence: results.confidence,
execution_time: results.execution_time || results.executionTime,
wait_time_seconds: waitTime,
completed_at: results.completed_at || results.completedAt,
message: `Agent execution completed successfully after waiting ${waitTime} seconds.`,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
else {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'wait_for_agent_completion',
execution_id: executionId,
status: results.status,
agent_name: results.agent_name || results.agentName,
error: results.error || `Agent execution ${results.status}`,
wait_time_seconds: waitTime,
message: `Agent execution ${results.status} after waiting ${waitTime} seconds.`,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
}
catch (error) {
this.errorLog('[MCP] Wait for agent completion error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Failed to wait for agent completion: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleForecastSales(request) {
const args = request.params.arguments || {};
try {
// Validate time_period format
const timePeriod = args.time_period || '90d';
const validTimePeriodRegex = /^(30d|90d|quarter|quarterly|annual|Q[1-4]|Q[1-4] \d{4}|FY \d{4}|FY\d{2})$/;
if (!validTimePeriodRegex.test(timePeriod)) {
throw new McpError(ErrorCode.InvalidParams, `Invalid time_period format: "${timePeriod}". Supported formats: 30d, 90d, quarter, quarterly, annual, Q1-Q4, Q1 2024, FY 2024, FY25, etc.`);
}
const result = await this.apiService.getForecast({
time_period: timePeriod,
include_scenarios: args.include_scenarios !== false,
filters: args.filters || {}
}, this.userToken);
// Ensure we have a proper response structure
const response = {
tool: 'forecast_sales',
time_period: timePeriod,
success: result?.success ?? false,
forecast_data: result?.data || null,
message: result?.message || 'Forecast completed',
error: result?.error || null,
timestamp: new Date().toISOString()
};
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Sales forecast error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Sales forecast failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleListAgents(request) {
const args = request.params.arguments || {};
try {
const result = await this.apiService.getAvailableAgents();
let healthInfo = null;
if (args.include_status !== false) {
try {
healthInfo = await this.apiService.checkHealth();
}
catch (error) {
this.debugLog('[MCP] Health check failed during list agents:', error);
healthInfo = { backend: false, crewai: false, crmConnect: false };
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'list_agents',
success: result.success,
agents: result.data,
health_status: healthInfo,
message: result.message,
error: result.error,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] List agents error:', error);
throw new McpError(ErrorCode.InternalError, `List agents failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleListWorkflows(request) {
const args = request.params.arguments || {};
try {
const result = await this.apiService.getAvailableWorkflows();
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'list_workflows',
success: result.success,
workflows: result.data,
include_details: args.include_details !== false,
message: result.message,
error: result.error,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] List workflows error:', error);
throw new McpError(ErrorCode.InternalError, `List workflows failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleHealthCheck(request) {
const args = request.params.arguments || {};
try {
const healthStatus = await this.apiService.checkHealth();
const overallHealth = healthStatus.backend && healthStatus.crewai && healthStatus.crmConnect;
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'health_check',
overall_status: overallHealth ? 'healthy' : 'degraded',
services: {
backend: {
status: healthStatus.backend ? 'healthy' : 'unhealthy',
description: 'IRISeller Backend API',
url: 'http://localhost:3001'
},
crewai: {
status: healthStatus.crewai ? 'healthy' : 'unhealthy',
description: 'CrewAI Agent Service',
url: 'http://localhost:8001'
},
crm_connect: {
status: healthStatus.crmConnect ? 'healthy' : 'unhealthy',
description: 'CRM Connect Gateway',
url: 'http://localhost:3001/api/crm-connect'
}
},
detailed: args.detailed === true,
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Health check error:', error);
// Return partial health info even if check fails
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'health_check',
overall_status: 'error',
services: {
backend: { status: 'unknown', description: 'IRISeller Backend API' },
crewai: { status: 'unknown', description: 'CrewAI Agent Service' },
crm_connect: { status: 'unknown', description: 'CRM Connect Gateway' }
},
error: error instanceof Error ? error.message : 'Health check failed',
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
}
async handleWebSearch(request) {
try {
const args = request.params.arguments || {};
// Validate required parameters
if (!args.query || typeof args.query !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'query parameter is required and must be a string');
}
// Check if web search is available
if (!this.webSearchService.isAvailable()) {
throw new McpError(ErrorCode.InternalError, 'Claude web search is not available. Please configure ANTHROPIC_API_KEY environment variable.');
}
// Prepare search request for Claude's web search
const searchRequest = {
query: args.query,
max_uses: typeof args.max_uses === 'number' ? Math.min(Math.max(args.max_uses, 1), 5) : 3,
allowed_domains: Array.isArray(args.allowed_domains) ? args.allowed_domains : undefined,
blocked_domains: Array.isArray(args.blocked_domains) ? args.blocked_domains : undefined,
user_location: args.user_location && typeof args.user_location === 'object' && 'type' in args.user_location && args.user_location.type === 'approximate'
? args.user_location
: undefined
};
// Execute Claude web search
const searchResult = await this.webSearchService.search(searchRequest);
// Format results for MCP response
const formattedResults = {
tool: 'web_search',
query: searchResult.query,
provider: searchResult.provider,
answer: searchResult.answer,
results: searchResult.results,
citations: searchResult.citations,
timestamp: searchResult.timestamp,
configuration: this.webSearchService.getConfiguration()
};
return {
content: [
{
type: 'text',
text: JSON.stringify(formattedResults, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Web search error:', error);
if (error instanceof McpError) {
throw error;
}
// Return error response with configuration information
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'web_search',
error: error instanceof Error ? error.message : 'Web search failed',
configuration: this.webSearchService.getConfiguration(),
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
}
async handleGetStockData(request) {
try {
const args = request.params.arguments || {};
// Validate required parameters
if (!args.symbol || typeof args.symbol !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'symbol parameter is required and must be a string');
}
// Check if stock data service is available
if (!this.stockDataService.isAvailable()) {
throw new McpError(ErrorCode.InternalError, 'Stock data service is not available. Please configure ANTHROPIC_API_KEY environment variable.');
}
// Prepare stock data request
const stockRequest = {
symbol: args.symbol,
company_name: typeof args.company_name === 'string' ? args.company_name : undefined,
include_history: typeof args.include_history === 'boolean' ? args.include_history : true,
history_period: typeof args.history_period === 'string' &&
['1d', '5d', '1m', '3m', '6m', '1y'].includes(args.history_period)
? args.history_period : '3m'
};
// Execute stock data search
const stockResult = await this.stockDataService.getStockData(stockRequest);
// Format results for MCP response
const formattedResults = {
tool: 'get_stock_data',
request: stockRequest,
stock_data: stockResult,
timestamp: new Date().toISOString(),
configuration: this.stockDataService.getConfiguration()
};
return {
content: [
{
type: 'text',
text: JSON.stringify(formattedResults, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Stock data handler error:', error);
// Return error response with configuration information
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'get_stock_data',
error: error instanceof Error ? error.message : 'Stock data fetch failed',
configuration: this.stockDataService.getConfiguration(),
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
}
async handleGoogleMapsLookup(request) {
try {
const args = request.params.arguments || {};
let targetAddress = args.address;
let contactInfo = null;
// If no direct address provided, lookup from CRM
if (!targetAddress && (args.person_name || args.company_name)) {
this.debugLog('[MCP] Looking up address from CRM for:', args.person_name || args.company_name);
// Query CRM to find the contact/lead and get their address
// Use a broader query since backend doesn't support all filters
const crmQuery = {
entity_type: 'leads',
limit: 50, // Increase limit to get more leads
offset: 0,
filters: {}
};
// The backend only supports status, industry, state, and search filters
// We'll get all leads and then filter client-side
const crmResult = await this.apiService.queryCRM(crmQuery, this.userToken);
this.debugLog('[MCP] CRM query result:', {
success: crmResult.success,
dataLength: Array.isArray(crmResult.data) ? crmResult.data.length : 'not array',
error: crmResult.error,
query: crmQuery
});
if (crmResult.success && Array.isArray(crmResult.data) && crmResult.data.length > 0) {
const leads = crmResult.data;
let lead = null;
// Search through the results to find the matching lead
if (args.person_name && typeof args.person_name === 'string') {
const nameParts = args.person_name.trim().toLowerCase().split(' ');
lead = leads.find((l) => {
const firstName = (l.FirstName || l.firstName || '').toLowerCase();
const lastName = (l.LastName || l.lastName || '').toLowerCase();
const fullName = `${firstName} ${lastName}`.trim();
const searchName = args.person_name.toLowerCase().trim();
// Check for exact full name match
if (fullName === searchName) {
return true;
}
// Check if all name parts match
if (nameParts.length >= 2) {
return firstName === nameParts[0] && lastName === nameParts.slice(1).join(' ');
}
else if (nameParts.length === 1) {
// Single name - check both first and last name
return firstName === nameParts[0] || lastName === nameParts[0];
}
return false;
});
}
// If not found by name, try company name
if (!lead && args.company_name && typeof args.company_name === 'string') {
const searchCompany = args.company_name.toLowerCase().trim();
lead = leads.find((l) => (l.Company || l.company || '').toLowerCase().trim() === searchCompany);
}
if (lead) {
this.debugLog('[MCP] Found matching lead:', lead.FirstName || lead.firstName, lead.LastName || lead.lastName, '- Company:', lead.Company || lead.company);
this.debugLog('[MCP] Found lead data:', {
id: lead.id,
company: lead.Company || lead.company || lead.accountName || lead.companyName,
addressFields: {
Street: lead.Street,
street: lead.street,
address: lead.address,
City: lead.City,
city: lead.city,
State: lead.State,
state: lead.state
}
});
contactInfo = this.extractLeadData({ leads: [lead] });
// Build address from CRM fields - try both uppercase (Salesforce) and lowercase (normalized) field names
const addressParts = [
lead.Street || lead.street || lead.address,
lead.City || lead.city,
lead.State || lead.state,
lead.PostalCode || lead.postalCode || lead.zip,
lead.Country || lead.country
].filter(Boolean);
if (addressParts.length > 0) {
targetAddress = addressParts.join(', ');
}
}
else {
this.debugLog('[MCP] No matching lead found among', leads.length, 'results');
}
}
// Also try contacts if no leads found
if (!targetAddress) {
crmQuery.entity_type = 'contacts';
const contactsResult = await this.apiService.queryCRM(crmQuery, this.userToken);
if (contactsResult.success && Array.isArray(contactsResult.data) && contactsResult.data.length > 0) {
const contacts = contactsResult.data;
let contact = null;
// Search through contacts for matching person/company
if (args.person_name && typeof args.person_name === 'string') {
const nameParts = args.person_name.trim().toLowerCase().split(' ');
contact = contacts.find((c) => {
const firstName = (c.FirstName || c.firstName || '').toLowerCase();
const lastName = (c.LastName || c.lastName || '').toLowerCase();
const fullName = `${firstName} ${lastName}`.trim();
const searchName = args.person_name.toLowerCase().trim();
// Check for exact full name match
if (fullName === searchName) {
return true;
}
// Check if all name parts match
if (nameParts.length >= 2) {
return firstName === nameParts[0] && lastName === nameParts.slice(1).join(' ');
}
else if (nameParts.length === 1) {
return firstName === nameParts[0] || lastName === nameParts[0];
}
return false;
});
}
// If not found by name, try company name
if (!contact && args.company_name && typeof args.company_name === 'string') {
const searchCompany = args.company_name.toLowerCase().trim();
contact = contacts.find((c) => (c.AccountName || c.accountName || c.Company || c.company || '').toLowerCase().trim() === searchCompany);
}
if (contact) {
this.debugLog('[MCP] Found matching contact:', contact.FirstName || contact.firstName, contact.LastName || contact.lastName, '- Company:', contact.AccountName || contact.accountName || contact.Company || contact.company);
contactInfo = this.extractLeadData({ contacts: [contact] });
const addressParts = [
contact.MailingStreet || contact.Street || contact.street || contact.address,
contact.MailingCity || contact.City || contact.city,
contact.MailingState || contact.State || contact.state,
contact.MailingPostalCode || contact.PostalCode || contact.postalCode || contact.zip,
contact.MailingCountry || contact.Country || contact.country
].filter(Boolean);
if (addressParts.length > 0) {
targetAddress = addressParts.join(', ');
}
}
else {
this.debugLog('[MCP] No matching contact found among', contacts.length, 'results');
}
}
}
}
if (!targetAddress) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'get_maps_location',
error: 'No address found. Please provide an address directly or ensure the person/company exists in CRM with address data.',
searched_for: {
person_name: args.person_name,
company_name: args.company_name
},
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
// Return simplified result with just the address
const result = {
tool: 'get_maps_location',
success: true,
address: targetAddress,
contact_info: contactInfo,
rosa_response: `Here's the address for ${contactInfo?.company || args.company_name || 'the requested location'}: ${targetAddress}`,
timestamp: new Date().toISOString()
};
this.debugLog('[MCP] Found address:', targetAddress);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Google Maps lookup error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Google Maps lookup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleDNCComplianceCheck(request) {
try {
const args = request.params.arguments || {};
let targetContact = null;
let dncStatus = { cleared: false, reason: '' };
// If no direct contact info provided, lookup from CRM
if (args.person_name || args.company_name || args.phone_number || args.email) {
this.debugLog('[MCP] Looking up contact in CRM for DNC check:', args.person_name || args.company_name);
// Query CRM to find the lead - use a broader query since backend doesn't support all filters
const crmQuery = {
entity_type: 'leads',
limit: 50, // Increase limit to get more leads
offset: 0,
filters: {}
};
// The backend only supports status, industry, state, and search filters
// We'll get all leads and then filter client-side
const crmResult = await this.apiService.queryCRM(crmQuery, this.userToken);
if (crmResult.success && Array.isArray(crmResult.data) && crmResult.data.length > 0) {
// Now search through the results to find the matching lead
const leads = crmResult.data;
// Try to find exact match first
if (args.person_name && typeof args.person_name === 'string') {
const nameParts = args.person_name.trim().toLowerCase().split(' ');
targetContact = leads.find((lead) => {
const firstName = (lead.FirstName || lead.firstName || '').toLowerCase();
const lastName = (lead.LastName || lead.lastName || '').toLowerCase();
const fullName = `${firstName} ${lastName}`.trim();
const searchName = args.person_name.toLowerCase().trim();
// Check for exact full name match
if (fullName === searchName) {
return true;
}
// Check if all name parts match
if (nameParts.length >= 2) {
return firstName === nameParts[0] && lastName === nameParts.slice(1).join(' ');
}
else if (nameParts.length === 1) {
// Single name - check both first and last name
return firstName === nameParts[0] || lastName === nameParts[0];
}
return false;
});
}
// If not found by name, try other criteria
if (!targetContact && args.email && typeof args.email === 'string') {
const searchEmail = args.email.toLowerCase();
targetContact = leads.find((lead) => (lead.Email || lead.email || '').toLowerCase() === searchEmail);
}
if (!targetContact && args.phone_number && typeof args.phone_number === 'string') {
const cleanPhone = args.phone_number.replace(/\D/g, '');
targetContact = leads.find((lead) => {
const leadPhone = (lead.Phone || lead.phone || '').replace(/\D/g, '');
return leadPhone === cleanPhone;
});
}
if (!targetContact && args.company_name && typeof args.company_name === 'string') {
const searchCompany = args.company_name.toLowerCase();
targetContact = leads.find((lead) => (lead.Company || lead.company || '').toLowerCase() === searchCompany);
}
// Log what we found
if (targetContact) {
this.debugLog('[MCP] Found matching lead:', targetContact.FirstName || targetContact.firstName, targetContact.LastName || targetContact.lastName, '- Company:', targetContact.Company || targetContact.company);
}
else {
this.debugLog('[MCP] No matching lead found among', leads.length, 'results');
}
}
}
if (!targetContact) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
tool: 'check_dnc_compliance',
error: 'Contact not found in CRM. Please provide valid person name, company name, phone number, or email.',
searched_for: {
person_name: args.person_name,
company_name: args.company_name,
phone_number: args.phone_number,
email: args.email
},
timestamp: new Date().toISOString()
}, null, 2)
}
]
};
}
// Perform DNC compliance check using Fax field as Y/N flag
dncStatus = this.checkDNCComplianceFromFaxField(targetContact);
const result = {
tool: 'check_dnc_compliance',
success: true,
contact_info: {
name: targetContact.FirstName ? `${targetContact.FirstName} ${targetContact.LastName || ''}`.trim() : targetContact.Name,
company: targetContact.Company || targetContact.AccountName,
phone: targetContact.Phone,
email: targetContact.Email,
title: targetContact.Title,
fax_field_value: targetContact.Fax || targetContact.fax || '(empty)'
},
compliance_status: {
cleared_for_contact: dncStatus.cleared,
dnc_method: 'Fax field Y/N flag',
check_reason: dncStatus.reason,
recommended_action: dncStatus.cleared ? 'Contact is approved' : 'DO NOT CONTACT - DNC restriction active'
},
rosa_response: dncStatus.cleared
? `Checking compliance⦠${targetContact.FirstName || targetContact.Name || 'Contact'} is cleared for contact - no DNC/DNE restrictions found.`
: `ā ļø DNC ALERT: ${targetContact.FirstName || targetContact.Name || 'Contact'} is on the Do Not Call list (Fax field: ${targetContact.Fax}). Please do not contact.`,
timestamp: new Date().toISOString()
};
this.debugLog('[MCP] DNC compliance check completed using Fax field:', targetContact.Fax, 'Status:', dncStatus.cleared);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] DNC compliance check error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `DNC compliance check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleRosaDailyBriefing(request) {
try {
const args = request.params.arguments || {};
const topLeadsCount = typeof args.top_leads_count === 'number' ? args.top_leads_count : 5;
const includeMeetings = args.include_meetings !== false;
const includeTasks = args.include_tasks !== false;
const greetingStyle = args.greeting_style || 'rosa';
this.debugLog(`[MCP] Generating Rosa daily briefing - ${topLeadsCount} leads, meetings: ${includeMeetings}, tasks: ${includeTasks}`);
// Step 1: Get top leads with Rosa action scoring
const leadsResult = await this.apiService.queryCRM({
entity_type: 'leads',
sort_by: 'action_score',
sort_order: 'desc',
limit: Math.max(topLeadsCount, 20), // Get more for better sorting
offset: 0
}, this.userToken);
let topLeads = [];
let upcomingMeetings = [];
let priorityTasks = [];
if (leadsResult.success && Array.isArray(leadsResult.data)) {
// Apply Rosa scoring (already done in queryCRM handler)
topLeads = leadsResult.data.slice(0, topLeadsCount);
// Extract meetings and tasks from all leads (not just top ones)
if (includeMeetings) {
upcomingMeetings = this.extractUpcomingMeetings(leadsResult.data);
this.debugLog(`[MCP] Extracted ${upcomingMeetings.length} meetings from NextStep fields`);
}
if (includeTasks) {
// Try multiple approaches to get tasks
priorityTasks = await this.fetchPriorityTasksFromCRM();
// If CRM tasks API failed, try to get activities using the activities endpoint
if (priorityTasks.length === 0) {
priorityTasks = await this.fetchActivitiesFromCRM();
}
this.debugLog(`[MCP] Fetched ${priorityTasks.length} priority tasks from CRM`);
}
}
// Step 2: Format Rosa-style briefing
const briefing = this.formatRosaDailyBriefing({
topLeads,
upcomingMeetings,
priorityTasks,
greetingStyle,
topLeadsCount
});
const briefingResult = {
tool: 'rosa_daily_briefing',
greeting_style: greetingStyle,
daily_briefing: {
greeting: briefing.greeting,
summary: briefing.summary,
top_leads: briefing.topLeads,
upcoming_meetings: briefing.upcomingMeetings,
priority_tasks: briefing.priorityTasks,
rosa_insights: briefing.rosaInsights
},
data_sources: {
leads_analyzed: leadsResult.data?.length || 0,
meetings_found: upcomingMeetings.length,
tasks_identified: priorityTasks.length,
action_scoring_applied: true
},
timestamp: new Date().toISOString()
};
this.debugLog('[MCP] Rosa daily briefing completed successfully');
return {
content: [
{
type: 'text',
text: JSON.stringify(briefingResult, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Rosa daily briefing error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Rosa daily briefing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleMeetingPrep(request) {
try {
const args = request.params.arguments || {};
// Validate required parameters
if (!args.person_name || typeof args.person_name !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'person_name parameter is required and must be a string');
}
const personName = args.person_name.trim();
const companyName = typeof args.company_name === 'string' ? args.company_name.trim() : undefined;
const meetingContext = typeof args.meeting_context === 'string' ? args.meeting_context : 'business meeting';
const researchDepth = typeof args.research_depth === 'string' &&
['basic', 'comprehensive', 'deep'].includes(args.research_depth)
? args.research_depth : 'comprehensive';
const includeCompanyAnalysis = typeof args.include_company_analysis === 'boolean' ? args.include_company_analysis : true;
const focusAreas = Array.isArray(args.focus_areas) ? args.focus_areas : [];
this.debugLog(`[MCP] Preparing meeting notes for: ${personName}${companyName ? ` at ${companyName}` : ''}`);
// Step 1: Use personalization agent to get enriched CRM data
let crmData = null;
try {
this.debugLog('[MCP] Step 1: Using personalization agent for comprehensive CRM lookup...');
const agentResponse = await this.apiService.executeAgent({
agent_name: 'personalization',
input_data: {
person_name: personName,
company_name: companyName,
meeting_context: meetingContext
},
options: {
timeout: 30,
priority: 'high',
include_research: true
}
});
if (agentResponse?.results?.research_results?.contact_info) {
crmData = agentResponse.results.research_results.contact_info;
this.debugLog(`[MCP] Found enriched CRM data for ${personName}: ${crmData.email}, ${crmData.phone}`);
}
else {
this.debugLog(`[MCP] No CRM data found via personalization agent for ${personName}`);
this.debugLog(`[MCP] Agent response structure:`, JSON.stringify(agentResponse, null, 2).substring(0, 500));
}
}
catch (error) {
this.debugLog('[MCP] CRM lookup via personalization agent failed:', error instanceof Error ? error.message : 'Unknown error');
}
// Step 2: Web Research (if comprehensive or deep)
let webResearchData = null;
if (researchDepth === 'comprehensive' || researchDepth === 'deep') {
try {
this.debugLog('[MCP] Step 2: Conducting web research...');
const searchQueries = [
`"${personName}"${companyName ? ` "${companyName}"` : ''} professional background`,
...(companyName ? [`"${companyName}" recent news developments 2024`] : []),
...(includeCompanyAnalysis && companyName ? [`"${companyName}" industry challenges opportunities`] : []),
...(focusAreas.length > 0 ? focusAreas.map(area => `"${companyName || personName}" ${area}`) : [])
];
const webResults = await Promise.all(searchQueries.slice(0, researchDepth === 'deep' ? 4 : 2).map(async (query) => {
try {
const searchRequest = {
params: {
name: 'web_search',
arguments: {
query,
max_uses: 2,
allowed_domains: ['linkedin.com', 'crunchbase.com', 'bloomberg.com', 'techcrunch.com', 'forbes.com']
}
},
method: 'tools/call'
};
return await this.handleWebSearch(searchRequest);
}
catch (error) {
this.debugLog(`[MCP] Web search failed for query: ${query}`, error);
return null;
}
}));
webResearchData = webResults
.filter(result => result && result.content?.[0]?.text)
.map(result => JSON.parse(result.content[0].text));
this.debugLog(`[MCP] Completed ${webResearchData.length} web searches`);
}
catch (error) {
this.debugLog('[MCP] Web research failed:', error instanceof Error ? error.message : 'Unknown error');
}
}
// Step 3: Advanced AI Synthesis using Agent Execution
let meetingNotes = null;
try {
this.debugLog('[MCP] Step 3: Generating AI-powered meeting notes...');
// Prepare comprehensive context for AI analysis
const aiContext = {
person_name: personName,
company_name: companyName,
meeting_context: meetingContext,
crm_data: crmData,
web_research: webResearchData,
focus_areas: focusAreas
};
// Use the agent execution system for advanced AI synthesis
const agentRequest = {
params: {
name: 'execute_agent',
arguments: {
agent_name: 'rosa_sdr',
input_data: {
contact_name: personName,
company_name: crmData?.leads?.[0]?.company || crmData?.leads?.[0]?.Company || crmData?.contacts?.[0]?.company || companyName || 'Unknown Company',
title: crmData?.leads?.[0]?.title || crmData?.leads?.[0]?.Title || crmData?.contacts?.[0]?.title || 'Unknown Title',
email: crmData?.leads?.[0]?.email || crmData?.leads?.[0]?.Email || crmData?.contacts?.[0]?.email || null,
phone: crmData?.leads?.[0]?.phone || crmData?.leads?.[0]?.Phone || crmData?.contacts?.[0]?.phone || null,
additional_context: `Meeting preparation request for ${meetingContext}. CRM Status: ${crmData?.leads?.[0]?.status || crmData?.leads?.[0]?.Status || 'Active'}. Research focus areas: ${focusAreas.join(', ') || 'General business discussion'}. Web research completed: ${webResearchData?.length || 0} sources.`
},
options: {
timeout: 45,
priority: 'high',
include_research: true
}
}
},
method: 'tools/call'
};
try {
const aiResult = await this.handleExecuteAgent(agentRequest);
if (aiResult.content?.[0]?.text) {
const aiData = JSON.parse(aiResult.content[0].text);
// Extract meeting-specific insights from ROSA's analysis
if (aiData.results || aiData.analysis) {
const analysis = aiData.results || aiData.analysis;
meetingNotes = this.formatMeetingPrepInsights(analysis, personName, meetingContext);
}
else {
meetingNotes = `AI Analysis: ${JSON.stringify(aiData).substring(0, 500)}...`;
}
this.debugLog('[MCP] Advanced AI synthesis completed via ROSA SDR agent');
}
}
catch (agentError) {
this.debugLog('[MCP] Agent-based AI synthesis failed, using fallback:', agentError instanceof Error ? agentError.message : 'Unknown error');
// Fallback to enhanced basic synthesis
meetingNotes = this.generateEnhancedMeetingInsights(crmData, webResearchData, personName, companyName, meetingContext, focusAreas);
}
}
catch (error) {
this.debugLog('[MCP] AI synthesis failed:', error instanceof Error ? error.message : 'Unknown error');
// Final fallback
meetingNotes = this.generateEnhancedMeetingInsights(crmData, webResearchData, personName, companyName, meetingContext, focusAreas);
}
// Step 4: Format comprehensive response
const meetingPrepResults = {
tool: 'prepare_meeting',
request: {
person_name: personName,
company_name: companyName,
meeting_context: meetingContext,
research_depth: researchDepth
},
meeting_preparation: {
executive_summary: this.generateExecutiveSummary(crmData, webResearchData, personName, companyName),
person_profile: this.extractPersonProfile(crmData, webResearchData, personName),
company_overview: companyName && includeCompanyAnalysis ? this.extractCompanyOverview(crmData, webResearchData, companyName) : null,
conversation_starters: this.generateConversationStarters(crmData, webResearchData, meetingContext),
potential_pain_points: this.identifyPainPoints(crmData, webResearchData, companyName),
meeting_objectives: this.suggestMeetingObjectives(meetingContext, crmData),
recommended_actions: this.generateActionItems(crmData, meetingContext),
ai_generated_notes: meetingNotes
},
data_sources: {
crm_data_found: crmData ? 1 : 0,
crm_provider: crmData?.crmProvider || 'Unknown',
web_searches_completed: webResearchData?.length || 0,
research_depth: researchDepth,
ai_synthesis_used: !!meetingNotes,
personalization_agent_used: true
},
timestamp: new Date().toISOString()
};
this.debugLog('[MCP] Meeting preparation completed successfully');
return {
content: [
{
type: 'text',
text: JSON.stringify(meetingPrepResults, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Meeting prep error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Meeting preparation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handlePriorityTasks(request) {
try {
const args = request.params.arguments || {};
// Set defaults
const taskTypes = Array.isArray(args.task_types) ? args.task_types : ['calls', 'emails', 'meetings', 'follow_ups'];
const priorityLevel = typeof args.priority_level === 'string' ? args.priority_level : 'all';
const limit = typeof args.limit === 'number' ? Math.min(Math.max(args.limit, 1), 50) : 10;
const timeFrame = typeof args.time_frame === 'string' ? args.time_frame : 'all';
const sortBy = typeof args.sort_by === 'string' ? args.sort_by : 'action_score';
this.debugLog(`[MCP] Getting priority tasks: types=${taskTypes.join(',')}, priority=${priorityLevel}, limit=${limit}`);
// Use the existing CRM tasks API to get real tasks
const crmTasksQuery = {
limit: Math.min(limit * 2, 100), // Fetch more to filter client-side
offset: 0,
sort_by: 'priority',
sort_order: 'desc'
};
// Add priority filter if not 'all'
if (priorityLevel !== 'all') {
crmTasksQuery.priority = priorityLevel;
}
const crmResult = await this.apiService.queryCRMTasks(crmTasksQuery, this.userToken);
if (!crmResult.success) {
this.debugLog(`[MCP] CRM tasks API failed: ${crmResult.error}, trying fallback approach`);
// Fallback to the existing method that extracts tasks from leads
return await this.handlePriorityTasksFallback(args);
}
if (!Array.isArray(crmResult.data)) {
throw new Error('CRM tasks API returned invalid data format');
}
this.debugLog(`[MCP] Retrieved ${crmResult.data.length} tasks from CRM Connect API`);
// Filter and process the real CRM tasks
let filteredTasks = crmResult.data
.filter(task => this.filterCRMTaskByType(task, taskTypes))
.filter(task => this.filterCRMTaskByTimeFrame(task, timeFrame))
.map(task => this.normalizeCRMTask(task));
// Sort tasks based on the requested sort method
filteredTasks = filteredTasks.sort((a, b) => {
switch (sortBy) {
case 'priority':
return this.comparePriority(b.priority, a.priority);
case 'due_date':
return new Date(a.due_date || '9999-12-31').getTime() - new Date(b.due_date || '9999-12-31').getTime();
case 'created_date':
return new Date(b.created_date || '1900-01-01').getTime() - new Date(a.created_date || '1900-01-01').getTime();
case 'action_score':
default:
return (b.action_score || 0) - (a.action_score || 0);
}
}).slice(0, limit);
const priorityTasksResult = {
tool: 'priority_tasks',
request: {
task_types: taskTypes,
priority_level: priorityLevel,
limit: limit,
time_frame: timeFrame,
sort_by: sortBy
},
priority_tasks: filteredTasks,
summary: {
total_tasks_found: crmResult.data.length,
tasks_returned: filteredTasks.length,
high_priority_count: filteredTasks.filter(t => (t.priority || '').toLowerCase() === 'high').length,
medium_priority_count: filteredTasks.filter(t => (t.priority || '').toLowerCase() === 'medium').length,
low_priority_count: filteredTasks.filter(t => (t.priority || '').toLowerCase() === 'low').length,
task_type_breakdown: this.getCRMTaskTypeBreakdown(filteredTasks)
},
insights: this.generateCRMTaskInsights(filteredTasks),
data_source: 'crm_connect_tasks_api',
timestamp: new Date().toISOString()
};
this.debugLog(`[MCP] Priority tasks completed: ${filteredTasks.length} tasks returned from CRM Connect API`);
return {
content: [
{
type: 'text',
text: JSON.stringify(priorityTasksResult, null, 2)
}
]
};
}
catch (error) {
this.errorLog('[MCP] Priority tasks error:', error);
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Priority tasks failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Fallback method that uses the old approach of parsing lead fields
async handlePriorityTasksFallback(args) {
const taskTypes = Array.isArray(args.task_types) ? args.task_types : ['calls', 'emails', 'meetings', 'follow_ups'];
const priorityLevel = typeof args.priority_level === 'string' ? args.priority_level : 'all';
const limit = typeof args.limit === 'number' ? Math.min(Math.max(args.limit, 1), 50) : 10;
const timeFrame = typeof args.time_frame === 'string' ? args.time_frame : 'all';
const sortBy = typeof args.sort_by === 'string' ? args.sort_by : 'action_score';
this.debugLog(`[MCP] Using fallback task extraction from lead fields`);
// Query leads to extract task information from CRM fields
const crmQuery = {
entity_type: 'leads',
limit: Math.min(limit * 3, 100),
offset: 0,
sort_by: 'created_date',
sort_order: 'desc'
};
const crmResult = await this.apiService.queryCRM(crmQuery, this.userToken);
if (!crmResult.success || !Array.isArray(crmResult.data)) {
throw new Error('Failed to fetch CRM data for fallback priority tasks');
}
// Extract tasks from CRM data using NextStep, Description, and other fields
const extractedTasks = this.extractTasksFromCRMData(crmResult.data, taskTypes, timeFrame);
// Score and filter tasks based on priority and criteria
const scoredTasks = extractedTasks
.map(task => ({
...task,
actionScore: this.calculateTaskActionScore(task),
priorityScore: this.calculateTaskPriorityScore(task)
}))
.filter(task => this.filterTaskByPriority(task, priorityLevel))
.sort((a, b) => {
switch (sortBy) {
case 'priority':
return b.priorityScore - a.priorityScore;
case 'due_date':
return new Date(a.dueDate || '9999-12-31').getTime() - new Date(b.dueDate || '9999-12-31').getTime();
case 'created_date':
return new Date(b.createdDate || '1900-01-01').getTime() - new Date(a.createdDate || '1900-01-01').getTime();
case 'action_score':
default:
return b.actionScore - a.actionScore;
}
})
.slice(0, limit);
const priorityTasksResult = {
tool: 'priority_tasks',
request: {
task_types: taskTypes,
priority_level: priorityLevel,
limit: limit,
time_frame: timeFrame,
sort_by: sortBy
},
priority_tasks: scoredTasks,
summary: {
total_tasks_found: extractedTasks.length,
tasks_returned: scoredTasks.length,
high_priority_count: scoredTasks.filter(t => t.priority === 'high').length,
medium_priority_count: scoredTasks.filter(t => t.priority === 'medium').length,
low_priority_count: scoredTasks.filter(t => t.priority === 'low').length,
task_type_breakdown: this.getTaskTypeBreakdown(scoredTasks)
},
insights: this.generateTaskInsights(scoredTasks),
data_source: 'crm_lead_fields_fallback',
timestamp: new Date().toISOString()
};
return {
content: [
{
type: 'text',
text: JSON.stringify(priorityTasksResult, null, 2)
}
]
};
}
// Helper methods for CRM tasks
filterCRMTaskByType(task, allowedTypes) {
if (allowedTypes.includes('all'))
return true;
const taskType = this.determineCRMTaskType(task);
return allowedTypes.includes(taskType);
}
determineCRMTaskType(task) {
const subject = (task.subject || task.Subject || '').toLowerCase();
const description = (task.description || task.Description || '').toLowerCase();
const type = (task.type || task.Type || task.task_type || '').toLowerCase();
// Check explicit type field first
if (type.includes('call'))
return 'calls';
if (type.includes('email'))
return 'emails';
if (type.includes('meeting'))
return 'meetings';
if (type.includes('follow'))
return 'follow_ups';
if (type.includes('demo'))
return 'demos';
if (type.includes('proposal'))
return 'proposals';
// Check subject and description
const text = `${subject} ${description}`;
if (text.includes('call') || text.includes('phone'))
return 'calls';
if (text.includes('email') || text.includes('send'))
return 'emails';
if (text.includes('meeting') || text.includes('demo'))
return 'meetings';
if (text.includes('follow') || text.includes('check'))
return 'follow_ups';
if (text.includes('proposal') || text.includes('quote'))
return 'proposals';
return 'follow_ups'; // Default fallback
}
filterCRMTaskByTimeFrame(task, timeFrame) {
if (timeFrame === 'all')
return true;
const dueDate = task.due_date || task.Due_Date || task.ActivityDate || task.activity_date;
if (!dueDate)
return timeFrame === 'all';
const taskDate = new Date(dueDate);
const now = new Date();
switch (timeFrame) {
case 'today':
return taskDate.toDateString() === now.toDateString();
case 'tomorrow':
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
return taskDate.toDateString() === tomorrow.toDateString();
case 'this_week':
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - now.getDay());
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
return taskDate >= startOfWeek && taskDate <= endOfWeek;
case 'next_week':
const nextWeekStart = new Date(now);
nextWeekStart.setDate(now.getDate() + (7 - now.getDay()));
const nextWeekEnd = new Date(nextWeekStart);
nextWeekEnd.setDate(nextWeekStart.getDate() + 6);
return taskDate >= nextWeekStart && taskDate <= nextWeekEnd;
case 'this_month':
return taskDate.getMonth() === now.getMonth() && taskDate.getFullYear() === now.getFullYear();
default:
return true;
}
}
normalizeCRMTask(task) {
return {
id: task.id || task.Id || task.task_id,
subject: task.subject || task.Subject || task.name || 'Untitled Task',
description: task.description || task.Description || '',
priority: this.normalizePriority(task.priority || task.Priority || task.Importance || 'Normal'),
status: task.status || task.Status || task.TaskStatus || 'Not Started',
type: this.determineCRMTaskType(task),
due_date: task.due_date || task.Due_Date || task.ActivityDate || task.activity_date,
created_date: task.created_date || task.CreatedDate || task.created_at,
owner: task.owner || task.Owner || task.assigned_to || task.AssignedTo,
contact: {
name: task.contact_name || task.ContactName || task.WhoId || 'Unknown Contact',
company: task.account_name || task.AccountName || task.company || 'Unknown Company',
id: task.contact_id || task.ContactId || task.who_id
},
account: {
name: task.account_name || task.AccountName || task.company,
id: task.account_id || task.AccountId || task.what_id
},
action_score: this.calculateCRMTaskActionScore(task),
crm_source: task.crm_source || task.source || 'unknown',
original_data: task
};
}
normalizePriority(priority) {
const p = priority.toLowerCase();
if (p.includes('high') || p.includes('urgent') || p.includes('critical'))
return 'high';
if (p.includes('low') || p.includes('minor'))
return 'low';
return 'medium';
}
comparePriority(a, b) {
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
return (priorityOrder[a.toLowerCase()] || 2) - (priorityOrder[b.toLowerCase()] || 2);
}
calculateCRMTaskActionScore(task) {
let score = 0;
// Priority scoring
const priority = (task.priority || task.Priority || '').toLowerCase();
if (priority.includes('high') || priority.includes('urgent'))
score += 50;
else if (priority.includes('medium') || priority.includes('normal'))
score += 30;
else if (priority.includes('low'))
score += 10;
else
score += 20; // Default
// Due date urgency
const dueDate = task.due_date || task.Due_Date || task.ActivityDate;
if (dueDate) {
const due = new Date(dueDate);
const now = new Date();
const daysUntilDue = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysUntilDue <= 0)
score += 25; // Overdue
else if (daysUntilDue <= 1)
score += 20; // Due today/tomorrow
else if (daysUntilDue <= 3)
score += 15; // Due this week
else if (daysUntilDue <= 7)
score += 10; // Due next week
}
// Status scoring
const status = (task.status || task.Status || '').toLowerCase();
if (status.includes('in progress') || status.includes('started'))
score += 15;
else if (status.includes('waiting') || status.includes('deferred'))
score += 5;
else if (status.includes('completed'))
score -= 10; // Lower priority for completed
return Math.max(0, score);
}
getCRMTaskTypeBreakdown(tasks) {
const breakdown = {};
tasks.forEach(task => {
const type = task.type || 'unknown';
breakdown[type] = (breakdown[type] || 0) + 1;
});
return breakdown;
}
generateCRMTaskInsights(tasks) {
const insights = [];
if (tasks.length === 0) {
insights.push("No priority tasks found matching your criteria.");
return insights;
}
const highPriorityCount = tasks.filter(t => (t.priority || '').toLowerCase() === 'high').length;
const overdueCount = tasks.filter(t => {
if (!t.due_date)
return false;
return new Date(t.due_date) < new Date();
}).length;
if (highPriorityCount > 0) {
insights.push(`You have ${highPriorityCount} high-priority tasks requiring immediate attention.`);
}
if (overdueCount > 0) {
insights.push(`${overdueCount} tasks are overdue and need urgent action.`);
}
const taskTypes = [...new Set(tasks.map(t => t.type))];
if (taskTypes.length > 1) {
insights.push(`Your tasks span ${taskTypes.length} different activity types: ${taskTypes.join(', ')}.`);
}
const topTask = tasks[0];
if (topTask) {
insights.push(`Your highest priority task is: "${topTask.subject}" for ${topTask.contact.company}.`);
}
// Add CRM source insights
const crmSources = [...new Set(tasks.map(t => t.crm_source).filter(Boolean))];
if (crmSources.length > 1) {
insights.push(`Tasks are coming from ${crmSources.length} CRM systems: ${crmSources.join(', ')}.`);
}
return insights;
}
// Helper methods for priority tasks (fallback)
extractTasksFromCRMData(crmData, taskTypes, timeFrame) {
const tasks = [];
const now = new Date();
crmData.forEach((record) => {
// Extract tasks from NextStep field (common task field)
const nextStep = record.NextStep || record.next_step || record.Description || record.description || '';
const company = record.Company || record.company_name || record.company || 'Unknown Company';
const contactName = `${record.FirstName || record.first_name || record.firstName || ''} ${record.LastName || record.last_name || record.lastName || ''}`.trim();
const email = record.Email || record.email || '';
const phone = record.Phone || record.phone || '';
const status = record.Status || record.status || 'Active';
const createdDate = record.CreatedDate || record.created_date || record.createdAt || now.toISOString();
if (nextStep && nextStep.trim()) {
// Parse task information from NextStep field
const taskInfo = this.parseTaskFromText(nextStep, taskTypes);
if (taskInfo.type) {
const task = {
id: `task_${record.Id || record.id || Math.random().toString(36).substr(2, 9)}`,
type: taskInfo.type,
title: taskInfo.title || nextStep.substring(0, 50) + (nextStep.length > 50 ? '...' : ''),
description: nextStep,
priority: taskInfo.priority || this.inferTaskPriority(nextStep, record),
contact: {
name: contactName || 'Unknown Contact',
company: company,
email: email,
phone: phone
},
leadInfo: {
status: status,
id: record.Id || record.id,
revenue: record.AnnualRevenue || record.annual_revenue || 0
},
dueDate: taskInfo.dueDate,
createdDate: createdDate,
source: 'crm_nextstep'
};
// Filter by time frame
if (this.matchesTimeFrame(task, timeFrame)) {
tasks.push(task);
}
}
}
// Also check for meeting-related tasks in other fields
if (taskTypes.includes('meetings')) {
const meetingFields = [record.MeetingNotes, record.meeting_notes, record.LastActivityDate, record.last_activity_date];
meetingFields.forEach(field => {
if (field && typeof field === 'string' && field.toLowerCase().includes('meeting')) {
const meetingTask = {
id: `meeting_${record.Id || record.id || Math.random().toString(36).substr(2, 9)}`,
type: 'meetings',
title: `Follow up on meeting with ${contactName}`,
description: field,
priority: 'medium',
contact: {
name: contactName || 'Unknown Contact',
company: company,
email: email,
phone: phone
},
leadInfo: {
status: status,
id: record.Id || record.id,
revenue: record.AnnualRevenue || record.annual_revenue || 0
},
createdDate: createdDate,
source: 'crm_meeting_notes'
};
if (this.matchesTimeFrame(meetingTask, timeFrame)) {
tasks.push(meetingTask);
}
}
});
}
});
return tasks;
}
parseTaskFromText(text, allowedTypes) {
const lowerText = text.toLowerCase();
let type = null;
let priority = null;
let dueDate = null;
let title = text.substring(0, 100);
// Determine task type based on keywords
if (allowedTypes.includes('calls') && (lowerText.includes('call') || lowerText.includes('phone'))) {
type = 'calls';
}
else if (allowedTypes.includes('emails') && (lowerText.includes('email') || lowerText.includes('send') || lowerText.includes('follow up'))) {
type = 'emails';
}
else if (allowedTypes.includes('meetings') && (lowerText.includes('meeting') || lowerText.includes('schedule') || lowerText.includes('demo'))) {
type = 'meetings';
}
else if (allowedTypes.includes('follow_ups') && (lowerText.includes('follow') || lowerText.includes('check'))) {
type = 'follow_ups';
}
else if (allowedTypes.includes('demos') && (lowerText.includes('demo') || lowerText.includes('presentation'))) {
type = 'demos';
}
else if (allowedTypes.includes('proposals') && (lowerText.includes('proposal') || lowerText.includes('quote'))) {
type = 'proposals';
}
else if (allowedTypes.includes('research') && (lowerText.includes('research') || lowerText.includes('investigate'))) {
type = 'research';
}
// Determine priority based on keywords
if (lowerText.includes('urgent') || lowerText.includes('asap') || lowerText.includes('immediate')) {
priority = 'high';
}
else if (lowerText.includes('important') || lowerText.includes('priority')) {
priority = 'high';
}
else if (lowerText.includes('when possible') || lowerText.includes('low priority')) {
priority = 'low';
}
else {
priority = 'medium';
}
// Try to extract due dates
const datePatterns = [
/(?:by|due|before)\s+(\d{1,2}\/\d{1,2}\/\d{4})/i,
/(?:by|due|before)\s+(\d{4}-\d{2}-\d{2})/i,
/(?:today|tomorrow|this week|next week)/i
];
for (const pattern of datePatterns) {
const match = text.match(pattern);
if (match) {
if (match[1]) {
dueDate = match[1];
}
else if (match[0]) {
const timeRef = match[0].toLowerCase();
const now = new Date();
if (timeRef.includes('today')) {
dueDate = now.toISOString().split('T')[0];
}
else if (timeRef.includes('tomorrow')) {
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
dueDate = tomorrow.toISOString().split('T')[0];
}
else if (timeRef.includes('this week')) {
const endOfWeek = new Date(now);
endOfWeek.setDate(endOfWeek.getDate() + (7 - endOfWeek.getDay()));
dueDate = endOfWeek.toISOString().split('T')[0];
}
else if (timeRef.includes('next week')) {
const nextWeek = new Date(now);
nextWeek.setDate(nextWeek.getDate() + 7);
dueDate = nextWeek.toISOString().split('T')[0];
}
}
break;
}
}
return { type, priority, dueDate, title };
}
inferTaskPriority(text, record) {
const lowerText = text.toLowerCase();
const revenue = record.AnnualRevenue || record.annual_revenue || 0;
const status = (record.Status || record.status || '').toLowerCase();
// High priority indicators
if (lowerText.includes('urgent') || lowerText.includes('asap') || lowerText.includes('immediate')) {
return 'high';
}
if (revenue > 1000000 || status.includes('hot') || status.includes('qualified')) {
return 'high';
}
// Low priority indicators
if (lowerText.includes('when possible') || lowerText.includes('low priority') || status.includes('cold')) {
return 'low';
}
return 'medium';
}
matchesTimeFrame(task, timeFrame) {
if (timeFrame === 'all')
return true;
const now = new Date();
const taskDate = task.dueDate ? new Date(task.dueDate) : new Date(task.createdDate);
switch (timeFrame) {
case 'today':
return taskDate.toDateString() === now.toDateString();
case 'tomorrow':
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
return taskDate.toDateString() === tomorrow.toDateString();
case 'this_week':
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - now.getDay());
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
return taskDate >= startOfWeek && taskDate <= endOfWeek;
case 'next_week':
const nextWeekStart = new Date(now);
nextWeekStart.setDate(now.getDate() + (7 - now.getDay()));
const nextWeekEnd = new Date(nextWeekStart);
nextWeekEnd.setDate(nextWeekStart.getDate() + 6);
return taskDate >= nextWeekStart && taskDate <= nextWeekEnd;
case 'this_month':
return taskDate.getMonth() === now.getMonth() && taskDate.getFullYear() === now.getFullYear();
default:
return true;
}
}
calculateTaskActionScore(task) {
let score = 0;
// Priority scoring
switch (task.priority) {
case 'high':
score += 50;
break;
case 'medium':
score += 30;
break;
case 'low':
score += 10;
break;
}
// Task type scoring
switch (task.type) {
case 'meetings':
score += 40;
break;
case 'calls':
score += 35;
break;
case 'demos':
score += 35;
break;
case 'proposals':
score += 30;
break;
case 'emails':
score += 25;
break;
case 'follow_ups':
score += 20;
break;
case 'research':
score += 15;
break;
}
// Revenue potential scoring
const revenue = task.leadInfo?.revenue || 0;
if (revenue > 1000000)
score += 20;
else if (revenue > 500000)
score += 15;
else if (revenue > 100000)
score += 10;
else if (revenue > 50000)
score += 5;
// Lead status scoring
const status = (task.leadInfo?.status || '').toLowerCase();
if (status.includes('hot') || status.includes('qualified'))
score += 15;
else if (status.includes('warm') || status.includes('interested'))
score += 10;
else if (status.includes('cold') || status.includes('unqualified'))
score -= 5;
// Due date urgency scoring
if (task.dueDate) {
const dueDate = new Date(task.dueDate);
const now = new Date();
const daysUntilDue = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysUntilDue <= 0)
score += 25; // Overdue
else if (daysUntilDue <= 1)
score += 20; // Due today/tomorrow
else if (daysUntilDue <= 3)
score += 15; // Due this week
else if (daysUntilDue <= 7)
score += 10; // Due next week
}
return Math.max(0, score);
}
calculateTaskPriorityScore(task) {
switch (task.priority) {
case 'high': return 100;
case 'medium': return 50;
case 'low': return 25;
default: return 30;
}
}
filterTaskByPriority(task, priorityLevel) {
if (priorityLevel === 'all')
return true;
return task.priority === priorityLevel;
}
getTaskTypeBreakdown(tasks) {
const breakdown = {};
tasks.forEach(task => {
breakdown[task.type] = (breakdown[task.type] || 0) + 1;
});
return breakdown;
}
generateTaskInsights(tasks) {
const insights = [];
if (tasks.length === 0) {
insights.push("No priority tasks found matching your criteria.");
return insights;
}
const highPriorityCount = tasks.filter(t => t.priority === 'high').length;
const overdueCount = tasks.filter(t => {
if (!t.dueDate)
return false;
return new Date(t.dueDate) < new Date();
}).length;
if (highPriorityCount > 0) {
insights.push(`You have ${highPriorityCount} high-priority tasks requiring immediate attention.`);
}
if (overdueCount > 0) {
insights.push(`${overdueCount} tasks are overdue and need urgent action.`);
}
const taskTypes = [...new Set(tasks.map(t => t.type))];
if (taskTypes.length > 1) {
insights.push(`Your tasks span ${taskTypes.length} different activity types: ${taskTypes.join(', ')}.`);
}
const topTask = tasks[0];
if (topTask) {
insights.push(`Your highest priority task is: "${topTask.title}" for ${topTask.contact.company}.`);
}
return insights;
}
// Helper methods for meeting preparation
buildMeetingPrepPrompt(data) {
return `Generate comprehensive meeting preparation notes for a ${data.meetingContext} with ${data.personName}${data.companyName ? ` from ${data.companyName}` : ''}.
CRM Data Available: ${data.crmData ? 'Yes' : 'No'}
Web Research Data: ${data.webResearchData ? 'Yes' : 'No'}
Focus Areas: ${data.focusAreas.join(', ') || 'General business discussion'}
Please provide:
1. Key talking points and conversation starters
2. Potential challenges and pain points to address
3. Strategic opportunities to explore
4. Recommended meeting outcomes and next steps
Make it actionable and specific to this person and company.`;
}
generateExecutiveSummary(crmData, webData, personName, companyName) {
const leadData = this.extractLeadData(crmData);
const hasData = crmData?.total_found > 0 || webData?.length > 0 || Object.keys(leadData).length > 0;
let summary = '';
// Person and company information
const title = leadData.title || 'Unknown role';
const actualCompany = leadData.company || companyName;
const industry = leadData.industry;
// Enhanced summary with qualification status
if (leadData.qualificationStatus === 'Sales Qualified' || leadData.qualificationStatus === 'Highly Qualified') {
summary += `š„ HIGH PRIORITY: Meeting with ${personName}, ${title} at ${actualCompany}. `;
summary += `This is a SALES QUALIFIED prospect with strong potential. `;
}
else if (leadData.qualificationStatus === 'Marketing Qualified') {
summary += `š Meeting with ${personName}, ${title} at ${actualCompany}. `;
summary += `Marketing qualified prospect - focus on needs qualification and solution fit. `;
}
else {
summary += `Meeting with ${personName}, ${title}${actualCompany ? ` at ${actualCompany}` : ''}. `;
}
// Industry context
if (industry) {
summary += `Operating in ${industry} sector. `;
}
// Company size and revenue context
if (leadData.numberOfEmployees || leadData.annualRevenue) {
const sizeCategory = this.categorizeCompanySize(leadData.numberOfEmployees, leadData.annualRevenue);
summary += `${sizeCategory} organization. `;
}
// Qualification scores if available
if (leadData.qualificationScore) {
summary += `Qualification score: ${leadData.qualificationScore}/100. `;
}
// Key pain points from description
if (leadData.description && leadData.description.length > 20) {
const painPoints = this.extractPainPointsFromText(leadData.description);
if (painPoints.length > 0) {
summary += `Key documented needs: ${painPoints.slice(0, 2).join(', ')}. `;
}
}
// Next step information
if (leadData.nextStep) {
summary += `Documented next step: ${leadData.nextStep}. `;
}
// Research context
if (webData?.length > 0) {
summary += `Recent web research completed across ${webData.length} sources providing market context. `;
}
// Recommendation based on qualification
if (leadData.qualificationStatus === 'Sales Qualified') {
summary += `RECOMMENDATION: Focus on solution demonstration, ROI discussion, and closing next steps.`;
}
else if (leadData.qualificationStatus === 'Marketing Qualified') {
summary += `RECOMMENDATION: Complete BANT qualification and identify specific use cases.`;
}
else if (!hasData) {
summary += `Limited data available - recommend focusing on discovery questions and relationship building.`;
}
else {
summary += `RECOMMENDATION: Leverage documented challenges to demonstrate relevant solutions and build credibility.`;
}
return summary;
}
extractPersonProfile(crmData, webData, personName) {
const leadData = this.extractLeadData(crmData);
// Enhanced person profile with comprehensive data
const profile = {
name: personName,
title: leadData.title || 'Unknown',
company: leadData.company || 'Unknown',
email: leadData.email || null,
phone: leadData.phone || null,
linkedin_url: leadData.linkedin_url || null,
industry: leadData.industry || null,
website: leadData.website || null,
address: leadData.address || null,
crm_provider: leadData.crmProvider || null,
crm_id: leadData.crmId || null,
source: leadData.source || null,
record_type: leadData.recordType || 'Unknown',
web_insights: webData?.length ? `Research completed from ${webData.length} sources` : null,
// Enhanced qualification information
qualification_status: leadData.qualificationStatus,
qualification_score: leadData.qualificationScore,
bant_scores: {
budget: leadData.bantBudgetScore,
authority: leadData.bantAuthorityScore,
need: leadData.bantNeedScore,
timeline: leadData.bantTimelineScore
},
// Lead-specific information
lead_status: leadData.status,
lead_rating: leadData.rating,
annual_revenue: leadData.annualRevenue,
employee_count: leadData.numberOfEmployees,
description: leadData.description,
next_step: leadData.nextStep,
// Engagement insights
last_activity: leadData.lastActivity,
created_date: leadData.createdDate,
modified_date: leadData.modifiedDate
};
// Add web research insights
if (webData?.length > 0) {
profile.web_research_summary = this.summarizeWebResearch(webData);
}
return profile;
}
extractCompanyOverview(crmData, webData, companyName) {
const leadData = this.extractLeadData(crmData);
const overview = {
name: companyName,
industry: leadData.industry || 'Unknown Industry',
annual_revenue: leadData.annualRevenue || 'Unknown',
employee_count: leadData.numberOfEmployees || 'Unknown',
website: leadData.website,
location: this.formatAddress(leadData),
// Enhanced company intelligence
business_model: this.inferBusinessModel(leadData),
company_size_category: this.categorizeCompanySize(leadData.numberOfEmployees, leadData.annualRevenue),
// Research-based insights
recent_developments: webData?.length ? this.extractDevelopments(webData) : 'No recent data available',
market_position: this.assessMarketPosition(leadData, webData),
technology_profile: this.assessTechnologyProfile(leadData, webData),
// Pain points and opportunities specific to this company
potential_challenges: this.identifyCompanySpecificChallenges(leadData, webData),
growth_opportunities: this.identifyGrowthOpportunities(leadData, webData),
competitive_landscape: webData?.length ? 'Analysis based on recent research' : 'To be researched',
// Decision-making insights
buying_signals: this.identifyBuyingSignals(leadData),
decision_timeline: this.estimateDecisionTimeline(leadData),
budget_indicators: this.assessBudgetIndicators(leadData)
};
// Add industry-specific insights
if (leadData.industry) {
overview.industry_trends = this.getIndustryTrends(leadData.industry);
overview.industry_specific_pain_points = this.getIndustrySpecificPainPoints(leadData.industry);
}
return overview;
}
generateConversationStarters(crmData, webData, context) {
const starters = [];
// Get lead/contact data
const leadData = this.extractLeadData(crmData);
const personName = leadData.name || 'Unknown';
const companyName = leadData.company;
const title = leadData.title;
const industry = leadData.industry;
const description = leadData.description;
const qualificationStatus = leadData.qualificationStatus;
// Personalized opening based on lead information
if (leadData.name && leadData.title && leadData.company) {
starters.push(`Hi ${personName}, thank you for taking the time to meet with me today. I understand you're the ${title} at ${companyName} - I'm excited to learn more about your current initiatives.`);
}
else {
starters.push(`Thank you for taking the time to meet with me about ${context}`);
}
// Industry-specific conversation starters
if (industry) {
starters.push(`I've been working with several companies in the ${industry} space recently - I'd love to hear your perspective on the current market challenges.`);
}
// Lead-specific challenges from description
if (description && description.length > 20) {
const challengeKeywords = ['challenge', 'problem', 'issue', 'struggle', 'difficulty', 'pain', 'bottleneck'];
const hasChallenge = challengeKeywords.some(keyword => description.toLowerCase().includes(keyword));
if (hasChallenge) {
starters.push(`I noticed in our previous discussions some of the challenges you mentioned - I'd like to dive deeper into those areas and see how we might help.`);
}
}
// Qualification-based starters
if (qualificationStatus === 'Sales Qualified' || qualificationStatus === 'Highly Qualified') {
starters.push(`Based on our previous conversations, it sounds like you're actively looking for solutions in this area - I'd love to understand your evaluation process and timeline.`);
}
// Web research insights
if (webData?.length > 0) {
if (companyName) {
starters.push(`I did some research on ${companyName} and saw some interesting developments - I'd love to get your take on how the market dynamics are affecting your business.`);
}
else {
starters.push('I did some research on your industry and would love to get your perspective on the current trends.');
}
}
// Fallback if no data available
if (starters.length === 0) {
starters.push(`Thank you for taking the time to meet with me about ${context}`);
starters.push('I\'d love to learn more about your current challenges and priorities');
}
// Ensure we have at least 2 starters but no more than 4
return starters.slice(0, Math.max(2, Math.min(4, starters.length)));
}
identifyPainPoints(crmData, webData, companyName) {
const painPoints = [];
const leadData = this.extractLeadData(crmData);
// Extract pain points from lead description
const description = leadData.description || '';
if (description.length > 20) {
const extractedPains = this.extractPainPointsFromText(description);
painPoints.push(...extractedPains);
}
// Add qualification-based pain points
if (leadData.qualificationAnalysis) {
const analysisText = leadData.qualificationAnalysis.toString();
const analysisPains = this.extractPainPointsFromText(analysisText);
painPoints.push(...analysisPains);
}
// Industry-specific pain points based on actual industry data
if (leadData.industry) {
const industryPains = this.getIndustrySpecificPainPoints(leadData.industry);
painPoints.push(...industryPains);
}
// Company size-based challenges
if (leadData.numberOfEmployees || leadData.annualRevenue) {
const sizePains = this.getCompanySizeBasedPainPoints(leadData.numberOfEmployees, leadData.annualRevenue);
painPoints.push(...sizePains);
}
// Web research insights about company challenges
if (webData?.length > 0) {
webData.forEach((research) => {
if (research.insights) {
const webPains = this.extractPainPointsFromText(research.insights);
painPoints.push(...webPains);
}
});
}
// Remove duplicates and ensure uniqueness
const uniquePainPoints = [...new Set(painPoints)].filter(point => point.length > 10);
// Add fallback generic points if no specific ones found
if (uniquePainPoints.length < 2) {
const genericPains = [
`Operational efficiency challenges${companyName ? ` at ${companyName}` : ''}`,
'Digital transformation and technology integration needs',
'Process optimization and cost reduction opportunities'
];
uniquePainPoints.push(...genericPains.filter(p => !uniquePainPoints.includes(p)));
}
return uniquePainPoints.slice(0, 6); // Maximum 6 pain points
}
suggestMeetingObjectives(context, crmData) {
const objectives = [];
const leadData = this.extractLeadData(crmData);
// Lead-specific objectives based on qualification status
if (leadData.qualificationStatus === 'Sales Qualified' || leadData.qualificationStatus === 'Highly Qualified') {
objectives.push('Present tailored solution recommendations based on identified needs');
objectives.push('Discuss implementation timeline and next steps');
objectives.push('Address any remaining concerns or objections');
}
else if (leadData.qualificationStatus === 'Marketing Qualified') {
objectives.push('Qualify budget, authority, need, and timeline (BANT)');
objectives.push('Understand decision-making process and key stakeholders');
objectives.push('Build credibility and demonstrate expertise');
}
else {
objectives.push('Discover specific business challenges and pain points');
objectives.push('Understand current processes and technology stack');
objectives.push('Assess fit and qualification criteria');
}
// Context-specific objectives
if (context.includes('demo')) {
if (leadData.industry) {
objectives.push(`Demonstrate ${leadData.industry}-specific use cases and ROI`);
}
else {
objectives.push('Demonstrate relevant product capabilities');
}
objectives.push('Gather feedback on proposed solution approach');
}
if (context.includes('follow-up')) {
objectives.push('Address questions and concerns from previous interactions');
if (leadData.description?.includes('proposal') || leadData.description?.includes('quote')) {
objectives.push('Review and finalize proposal details');
}
}
if (context.includes('discovery')) {
objectives.push('Map current technology landscape and integration requirements');
objectives.push('Identify key success metrics and ROI expectations');
}
// NextStep-based objectives from CRM
if (leadData.nextStep) {
const nextStepObjective = `Execute planned next step: ${leadData.nextStep}`;
if (!objectives.some(obj => obj.includes(leadData.nextStep))) {
objectives.push(nextStepObjective);
}
}
// Ensure we have core objectives if nothing specific
if (objectives.length < 3) {
const coreObjectives = [
'Build rapport and establish trust',
'Understand current business challenges and priorities',
'Determine next steps and timeline'
];
objectives.push(...coreObjectives.filter(obj => !objectives.includes(obj)));
}
return objectives.slice(0, 6); // Maximum 6 objectives
}
generateActionItems(crmData, context) {
const actions = [];
const leadData = this.extractLeadData(crmData);
// Standard post-meeting actions
actions.push('Send personalized follow-up email with meeting summary and key takeaways');
actions.push('Update CRM with detailed meeting notes, outcomes, and next steps');
// Qualification-specific actions
if (leadData.qualificationStatus === 'Sales Qualified' || leadData.qualificationStatus === 'Highly Qualified') {
actions.push('Prepare detailed proposal with ROI analysis and implementation timeline');
actions.push('Schedule technical discovery call with solution architects');
actions.push('Provide relevant case studies from similar companies in their industry');
}
else if (leadData.qualificationStatus === 'Marketing Qualified') {
actions.push('Send educational content about our solutions and success stories');
actions.push('Schedule follow-up call to continue qualification process');
}
else {
actions.push('Research their company and industry challenges further');
actions.push('Prepare nurturing sequence with valuable industry insights');
}
// Industry-specific actions
if (leadData.industry) {
actions.push(`Compile industry-specific resources and insights for ${leadData.industry} sector`);
}
// Context-specific actions
if (context.includes('demo')) {
actions.push('Follow up on demo feedback and address any technical questions');
actions.push('Provide demo recording and additional technical documentation');
}
if (context.includes('proposal')) {
actions.push('Refine proposal based on meeting feedback and requirements');
actions.push('Schedule proposal review meeting with decision makers');
}
if (context.includes('discovery')) {
actions.push('Create detailed requirements document based on discovery findings');
actions.push('Prepare customized solution architecture and integration plan');
}
// Lead-specific actions from description or next steps
if (leadData.nextStep && !actions.some(action => action.includes(leadData.nextStep))) {
actions.push(`Execute documented next step: ${leadData.nextStep}`);
}
if (leadData.description) {
const urgentKeywords = ['urgent', 'asap', 'immediate', 'quickly', 'rush'];
const isUrgent = urgentKeywords.some(keyword => leadData.description.toLowerCase().includes(keyword));
if (isUrgent) {
actions.push('Prioritize rapid response due to urgency indicators');
}
}
// Stakeholder engagement actions
if (leadData.title?.toLowerCase().includes('manager') || leadData.title?.toLowerCase().includes('director')) {
actions.push('Identify and engage with additional decision makers and influencers');
}
return actions.slice(0, 8); // Maximum 8 action items
}
// Advanced AI synthesis helper methods
formatMeetingPrepInsights(analysis, personName, meetingContext) {
try {
// Extract key insights from ROSA's analysis
let insights = `AI-Powered Meeting Preparation for ${personName}:\n\n`;
if (analysis.qualification_score) {
insights += `šÆ Lead Qualification Score: ${analysis.qualification_score}/100\n`;
}
if (analysis.pain_points && Array.isArray(analysis.pain_points)) {
insights += `\nā ļø Identified Pain Points:\n${analysis.pain_points.map((point) => `⢠${point}`).join('\n')}\n`;
}
if (analysis.recommendations && Array.isArray(analysis.recommendations)) {
insights += `\nš” AI Recommendations:\n${analysis.recommendations.map((rec) => `⢠${rec}`).join('\n')}\n`;
}
if (analysis.next_best_actions && Array.isArray(analysis.next_best_actions)) {
insights += `\nšÆ Next Best Actions:\n${analysis.next_best_actions.map((action) => `⢠${action}`).join('\n')}\n`;
}
if (analysis.talking_points && Array.isArray(analysis.talking_points)) {
insights += `\nš£ļø AI-Generated Talking Points:\n${analysis.talking_points.map((point) => `⢠${point}`).join('\n')}\n`;
}
// Add context-specific insights
insights += `\nš¤ Meeting Context: Optimized for ${meetingContext} with personalized approach based on CRM and web research data.`;
return insights;
}
catch (error) {
return `AI Analysis completed for ${personName}. Raw data: ${JSON.stringify(analysis).substring(0, 300)}...`;
}
}
generateEnhancedMeetingInsights(crmData, webResearchData, personName, companyName, meetingContext, focusAreas) {
let insights = `Enhanced Meeting Preparation Insights for ${personName}:\n\n`;
// CRM-based insights
if (crmData && crmData.total_found > 0) {
const crmRecord = crmData.contacts?.[0] || crmData.leads?.[0];
const actualCompany = crmRecord?.company || crmRecord?.Company || companyName;
const title = crmRecord?.title || crmRecord?.Title;
const status = crmRecord?.status || crmRecord?.Status;
insights += `š¢ CRM Profile: ${title} at ${actualCompany}\n`;
insights += `š Status: ${status} (${crmData.contacts?.length > 0 ? 'Contact' : 'Lead'})\n`;
if (crmRecord?.email) {
insights += `š§ Contact: ${crmRecord.email}\n`;
}
// Role-specific insights
if (title) {
if (title.toLowerCase().includes('vp') || title.toLowerCase().includes('vice president')) {
insights += `\nš¼ VP-Level Approach: Focus on strategic initiatives, ROI, and organizational impact.\n`;
}
else if (title.toLowerCase().includes('director')) {
insights += `\nš¼ Director-Level Approach: Emphasize operational efficiency and team productivity.\n`;
}
else if (title.toLowerCase().includes('manager')) {
insights += `\nš¼ Manager-Level Approach: Discuss day-to-day challenges and process improvements.\n`;
}
}
}
// Web research insights
if (webResearchData && webResearchData.length > 0) {
insights += `\nš Research Completed: ${webResearchData.length} web sources analyzed for current market context.\n`;
}
// Focus area insights
if (focusAreas && focusAreas.length > 0) {
insights += `\nšÆ Key Focus Areas to Discuss:\n${focusAreas.map(area => `⢠${area}`).join('\n')}\n`;
}
// Meeting context insights
if (meetingContext) {
insights += `\nš Meeting Type: ${meetingContext}\n`;
if (meetingContext.includes('demo')) {
insights += `⢠Prepare interactive demonstration tailored to their use case\n⢠Focus on specific features that address their pain points\n`;
}
else if (meetingContext.includes('sales')) {
insights += `⢠Emphasize value proposition and ROI\n⢠Prepare pricing discussion and next steps\n`;
}
else if (meetingContext.includes('discovery')) {
insights += `⢠Ask open-ended questions about current processes\n⢠Identify decision-making criteria and timeline\n`;
}
}
insights += `\nš¤ AI Recommendation: This enhanced analysis combines CRM data, web research, and contextual intelligence to optimize your meeting approach.`;
return insights;
}
// Email Integration Handler Methods
async handleEmailAutoRespond(request) {
try {
const result = await this.emailHandlers.handleEmailAutoRespond(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email auto-respond handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email auto-respond failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleEmailIntentAnalysis(request) {
try {
const result = await this.emailHandlers.handleEmailIntentAnalysis(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email intent analysis handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email intent analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleCompileSalesInfo(request) {
try {
const result = await this.emailHandlers.handleCompileSalesInfo(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Compile sales info handler error:', error);
throw new McpError(ErrorCode.InternalError, `Compile sales info failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleGenerateEmailResponse(request) {
try {
const result = await this.emailHandlers.handleGenerateEmailResponse(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Generate email response handler error:', error);
throw new McpError(ErrorCode.InternalError, `Generate email response failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleScheduleEmailFollowup(request) {
try {
const result = await this.emailHandlers.handleScheduleEmailFollowup(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Schedule email followup handler error:', error);
throw new McpError(ErrorCode.InternalError, `Schedule email followup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleEmailAnalytics(request) {
try {
const result = await this.emailHandlers.handleEmailAnalytics(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email analytics handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email analytics failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// New Email Detection & Sending Tool Handlers
async handleDetectEmails(request) {
try {
const result = await this.emailHandlers.handleEmailDetection(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email detection handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email detection failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleSendEmail(request) {
try {
const result = await this.emailHandlers.handleEmailSending(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email sending handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email sending failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleGetEmailStatus(request) {
try {
const result = await this.emailHandlers.handleEmailStatus(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email status handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email status check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleManageEmailTemplates(request) {
try {
const result = await this.emailHandlers.handleEmailTemplates(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email template management handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email template management failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async handleManageEmailCampaigns(request) {
try {
const result = await this.emailHandlers.handleEmailCampaigns(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
catch (error) {
this.errorLog('Email campaign management handler error:', error);
throw new McpError(ErrorCode.InternalError, `Email campaign management failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Helper methods for enhanced meeting preparation
extractLeadData(crmData) {
// Extract lead data from various CRM structures
if (!crmData)
return {};
// Handle different CRM data structures
let leadRecord = null;
if (crmData.leads && Array.isArray(crmData.leads) && crmData.leads.length > 0) {
leadRecord = crmData.leads[0];
}
else if (crmData.contacts && Array.isArray(crmData.contacts) && crmData.contacts.length > 0) {
leadRecord = crmData.contacts[0];
}
else if (crmData.contact_info) {
leadRecord = crmData.contact_info;
}
else if (typeof crmData === 'object') {
leadRecord = crmData;
}
if (!leadRecord)
return {};
return {
name: leadRecord.name || leadRecord.Name || `${leadRecord.FirstName || ''} ${leadRecord.LastName || ''}`.trim(),
title: leadRecord.title || leadRecord.Title,
company: leadRecord.company || leadRecord.Company,
email: leadRecord.email || leadRecord.Email,
phone: leadRecord.phone || leadRecord.Phone,
linkedin_url: leadRecord.linkedin_url || leadRecord.LinkedInUrl,
industry: leadRecord.industry || leadRecord.Industry,
website: leadRecord.website || leadRecord.Website,
address: leadRecord.address || leadRecord.Street,
city: leadRecord.city || leadRecord.City,
state: leadRecord.state || leadRecord.State,
country: leadRecord.country || leadRecord.Country,
crmProvider: leadRecord.crmProvider || leadRecord.CrmProvider,
crmId: leadRecord.crmId || leadRecord.CrmId || leadRecord.Id,
source: leadRecord.source || leadRecord.LeadSource,
status: leadRecord.status || leadRecord.Status,
rating: leadRecord.rating || leadRecord.Rating,
description: leadRecord.description || leadRecord.Description,
nextStep: leadRecord.nextStep || leadRecord.NextStep,
qualificationStatus: leadRecord.qualificationStatus || leadRecord.QualificationStatus,
qualificationScore: leadRecord.qualificationScore || leadRecord.QualificationScore,
qualificationAnalysis: leadRecord.qualificationAnalysis || leadRecord.QualificationAnalysis,
qualificationRecommendations: leadRecord.qualificationRecommendations || leadRecord.QualificationRecommendations,
bantBudgetScore: leadRecord.bantBudgetScore || leadRecord.BantBudgetScore,
bantAuthorityScore: leadRecord.bantAuthorityScore || leadRecord.BantAuthorityScore,
bantNeedScore: leadRecord.bantNeedScore || leadRecord.BantNeedScore,
bantTimelineScore: leadRecord.bantTimelineScore || leadRecord.BantTimelineScore,
annualRevenue: leadRecord.annualRevenue || leadRecord.AnnualRevenue,
numberOfEmployees: leadRecord.numberOfEmployees || leadRecord.NumberOfEmployees,
createdDate: leadRecord.createdDate || leadRecord.CreatedDate,
modifiedDate: leadRecord.modifiedDate || leadRecord.LastModifiedDate,
lastActivity: leadRecord.lastActivity || leadRecord.LastActivity,
recordType: this.determineRecordType(crmData)
};
}
determineRecordType(crmData) {
if (crmData.leads && crmData.leads.length > 0)
return 'Lead';
if (crmData.contacts && crmData.contacts.length > 0)
return 'Contact';
return 'Unknown';
}
extractPainPointsFromText(text) {
if (!text || text.length < 10)
return [];
const painPoints = [];
const lowerText = text.toLowerCase();
// Keywords that indicate pain points
const painKeywords = [
'challenge', 'problem', 'issue', 'struggle', 'difficulty', 'pain', 'bottleneck',
'inefficient', 'slow', 'manual', 'outdated', 'lacking', 'need', 'require',
'improve', 'optimize', 'automate', 'streamline', 'eliminate', 'reduce'
];
// Common business pain point patterns
const painPatterns = [
/(?:manual|time-consuming|inefficient)\s+(?:process|workflow|system)/gi,
/(?:lack|need|require)\s+(?:automation|integration|visibility)/gi,
/(?:struggling|difficulty)\s+with\s+(\w+(?:\s+\w+)*)/gi,
/(?:challenge|problem|issue)\s+(?:with|around|regarding)\s+(\w+(?:\s+\w+)*)/gi,
/(?:improve|optimize|streamline)\s+(\w+(?:\s+\w+)*)/gi
];
// Extract sentences containing pain indicators
const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 10);
sentences.forEach(sentence => {
const lowerSentence = sentence.toLowerCase();
// Check if sentence contains pain keywords
const containsPainKeyword = painKeywords.some(keyword => lowerSentence.includes(keyword));
if (containsPainKeyword) {
// Extract meaningful pain points from the sentence
painPatterns.forEach(pattern => {
const matches = sentence.match(pattern);
if (matches) {
matches.forEach(match => {
if (match.length > 15 && match.length < 100) {
painPoints.push(this.capitalizeFirstLetter(match.trim()));
}
});
}
});
// If no pattern matches, use the whole sentence if it's not too long
if (sentence.length > 20 && sentence.length < 150 && containsPainKeyword) {
painPoints.push(this.capitalizeFirstLetter(sentence));
}
}
});
return [...new Set(painPoints)]; // Remove duplicates
}
getIndustrySpecificPainPoints(industry) {
const industryPains = {
'technology': [
'Rapid technology obsolescence and need for continuous upgrades',
'Scaling infrastructure to handle growing user base',
'Data security and privacy compliance challenges'
],
'healthcare': [
'Regulatory compliance and documentation requirements',
'Patient data management and interoperability issues',
'Cost containment while maintaining quality of care'
],
'financial services': [
'Regulatory compliance and reporting complexity',
'Legacy system modernization challenges',
'Cybersecurity and fraud prevention concerns'
],
'manufacturing': [
'Supply chain disruptions and inventory management',
'Equipment maintenance and downtime reduction',
'Quality control and regulatory compliance'
],
'retail': [
'Omnichannel customer experience challenges',
'Inventory management across multiple channels',
'Customer data integration and personalization'
],
'education': [
'Student engagement and learning outcome measurement',
'Technology integration in curriculum delivery',
'Administrative efficiency and resource optimization'
]
};
const normalizedIndustry = industry.toLowerCase();
// Try exact match first
if (industryPains[normalizedIndustry]) {
return industryPains[normalizedIndustry];
}
// Try partial matches
for (const [key, pains] of Object.entries(industryPains)) {
if (normalizedIndustry.includes(key) || key.includes(normalizedIndustry)) {
return pains;
}
}
// Generic business pain points
return [
`${industry} industry digital transformation challenges`,
'Process automation and operational efficiency needs',
'Customer experience optimization requirements'
];
}
getCompanySizeBasedPainPoints(employees, revenue) {
const empCount = parseInt(employees?.toString() || '0');
const annualRev = parseInt(revenue?.toString() || '0');
if (empCount < 50 || annualRev < 5000000) {
// Small business pain points
return [
'Limited resources for technology implementation',
'Need for scalable solutions that grow with the business',
'Manual processes due to budget constraints'
];
}
else if (empCount < 500 || annualRev < 100000000) {
// Mid-market pain points
return [
'Outgrowing current systems and processes',
'Need for better integration between departments',
'Scalability challenges as business grows'
];
}
else {
// Enterprise pain points
return [
'Complex integration requirements across multiple systems',
'Compliance and governance at scale',
'Managing change across large, distributed teams'
];
}
}
summarizeWebResearch(webData) {
if (!webData || webData.length === 0)
return 'No web research available';
const summaryPoints = [];
webData.forEach((research, index) => {
if (research.insights) {
summaryPoints.push(`Source ${index + 1}: ${research.insights.substring(0, 100)}...`);
}
else if (research.content) {
summaryPoints.push(`Source ${index + 1}: ${research.content.substring(0, 100)}...`);
}
});
return summaryPoints.join(' | ');
}
formatAddress(leadData) {
const parts = [leadData.city, leadData.state, leadData.country].filter(Boolean);
return parts.length > 0 ? parts.join(', ') : 'Unknown Location';
}
inferBusinessModel(leadData) {
const revenue = parseInt(leadData.annualRevenue?.toString() || '0');
const employees = parseInt(leadData.numberOfEmployees?.toString() || '0');
const industry = leadData.industry?.toLowerCase() || '';
if (industry.includes('saas') || industry.includes('software')) {
return 'Software as a Service (SaaS)';
}
else if (industry.includes('retail') || industry.includes('e-commerce')) {
return 'Retail/E-commerce';
}
else if (industry.includes('consulting') || industry.includes('services')) {
return 'Professional Services';
}
else if (industry.includes('manufacturing')) {
return 'Manufacturing/Production';
}
else if (revenue > 100000000) {
return 'Large Enterprise';
}
else if (revenue > 10000000) {
return 'Mid-Market';
}
else {
return 'Small to Medium Business';
}
}
categorizeCompanySize(employees, revenue) {
const empCount = parseInt(employees?.toString() || '0');
const annualRev = parseInt(revenue?.toString() || '0');
if (empCount > 1000 || annualRev > 100000000) {
return 'Enterprise (1000+ employees)';
}
else if (empCount > 250 || annualRev > 25000000) {
return 'Large Company (250-1000 employees)';
}
else if (empCount > 50 || annualRev > 5000000) {
return 'Mid-Market (50-250 employees)';
}
else {
return 'Small Business (<50 employees)';
}
}
extractDevelopments(webData) {
const developments = [];
webData.forEach((research) => {
if (research.insights) {
// Look for news, announcements, changes
const newsKeywords = ['announced', 'launched', 'acquired', 'partnership', 'funding', 'expansion'];
const insights = research.insights.toLowerCase();
if (newsKeywords.some(keyword => insights.includes(keyword))) {
developments.push(research.insights.substring(0, 150));
}
}
});
return developments.length > 0 ? developments.join('. ') : 'No recent developments identified from research';
}
assessMarketPosition(leadData, webData) {
const revenue = parseInt(leadData.annualRevenue?.toString() || '0');
const employees = parseInt(leadData.numberOfEmployees?.toString() || '0');
if (revenue > 1000000000) {
return 'Market Leader - Large enterprise with significant market presence';
}
else if (revenue > 100000000) {
return 'Established Player - Strong market position in their segment';
}
else if (revenue > 10000000) {
return 'Growing Company - Expanding market presence';
}
else {
return 'Emerging Player - Building market presence';
}
}
assessTechnologyProfile(leadData, webData) {
const industry = leadData.industry?.toLowerCase() || '';
const employees = parseInt(leadData.numberOfEmployees?.toString() || '0');
if (industry.includes('technology') || industry.includes('software')) {
return 'Technology-forward organization with likely modern tech stack';
}
else if (employees > 500) {
return 'Large organization likely using enterprise-grade technology solutions';
}
else if (employees > 50) {
return 'Mid-size organization with mixed technology maturity';
}
else {
return 'Small organization likely using basic technology solutions';
}
}
identifyCompanySpecificChallenges(leadData, webData) {
const challenges = [];
// Based on company size
const sizeCategory = this.categorizeCompanySize(leadData.numberOfEmployees, leadData.annualRevenue);
if (sizeCategory.includes('Small')) {
challenges.push('Resource constraints and need for cost-effective solutions');
}
else if (sizeCategory.includes('Enterprise')) {
challenges.push('Complex integration requirements and enterprise-scale challenges');
}
// Based on industry
if (leadData.industry) {
const industryPains = this.getIndustrySpecificPainPoints(leadData.industry);
challenges.push(...industryPains.slice(0, 2)); // Take first 2
}
// Based on description
if (leadData.description) {
const extractedChallenges = this.extractPainPointsFromText(leadData.description);
challenges.push(...extractedChallenges.slice(0, 2)); // Take first 2
}
return challenges.slice(0, 4); // Maximum 4 challenges
}
identifyGrowthOpportunities(leadData, webData) {
const opportunities = [];
// Based on qualification status
if (leadData.qualificationStatus === 'Sales Qualified') {
opportunities.push('High potential for immediate solution implementation');
}
// Based on company growth indicators
if (leadData.annualRevenue && parseInt(leadData.annualRevenue.toString()) > 50000000) {
opportunities.push('Established company with budget for technology investments');
}
// Based on industry trends
if (leadData.industry) {
const industry = leadData.industry.toLowerCase();
if (industry.includes('technology')) {
opportunities.push('Technology sector growth and digital transformation initiatives');
}
else if (industry.includes('healthcare')) {
opportunities.push('Healthcare digital transformation and regulatory modernization');
}
else {
opportunities.push('Industry digitization and process optimization opportunities');
}
}
return opportunities.slice(0, 3); // Maximum 3 opportunities
}
identifyBuyingSignals(leadData) {
const signals = [];
if (leadData.qualificationStatus === 'Sales Qualified') {
signals.push('Qualified as sales-ready prospect');
}
if (leadData.description) {
const urgentKeywords = ['urgent', 'asap', 'immediate', 'quickly', 'timeline', 'deadline'];
if (urgentKeywords.some(keyword => leadData.description.toLowerCase().includes(keyword))) {
signals.push('Urgency indicators in communication');
}
const budgetKeywords = ['budget', 'funding', 'approved', 'allocated', 'investment'];
if (budgetKeywords.some(keyword => leadData.description.toLowerCase().includes(keyword))) {
signals.push('Budget discussion indicators');
}
}
if (leadData.nextStep && leadData.nextStep.toLowerCase().includes('demo')) {
signals.push('Active engagement - demo scheduled');
}
return signals;
}
estimateDecisionTimeline(leadData) {
if (leadData.qualificationStatus === 'Sales Qualified') {
return '30-60 days - Active evaluation phase';
}
else if (leadData.qualificationStatus === 'Marketing Qualified') {
return '60-90 days - Early consideration phase';
}
else if (leadData.description?.toLowerCase().includes('urgent')) {
return '2-4 weeks - Urgent requirement';
}
else {
return '90+ days - Long-term evaluation';
}
}
assessBudgetIndicators(leadData) {
const revenue = parseInt(leadData.annualRevenue?.toString() || '0');
if (revenue > 100000000) {
return 'High budget potential - Enterprise-scale revenue';
}
else if (revenue > 25000000) {
return 'Moderate to high budget potential - Mid-market revenue';
}
else if (revenue > 5000000) {
return 'Limited to moderate budget potential - SMB revenue';
}
else {
return 'Budget constraints likely - Small business revenue';
}
}
getIndustryTrends(industry) {
const industryTrends = {
'technology': [
'AI and automation adoption acceleration',
'Cloud-first infrastructure strategies',
'Cybersecurity investment prioritization'
],
'healthcare': [
'Telemedicine and digital health expansion',
'Electronic health records modernization',
'Patient experience digitization'
],
'financial services': [
'Digital banking transformation',
'RegTech adoption for compliance',
'Open banking and API ecosystem development'
]
};
const normalizedIndustry = industry.toLowerCase();
return industryTrends[normalizedIndustry] || [
'Digital transformation initiatives',
'Process automation adoption',
'Customer experience enhancement focus'
];
}
capitalizeFirstLetter(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
// Rosa-style Action Score Calculation
calculateRosaActionScore(lead) {
let score = 0;
// Base qualification score (0-40 points)
const qualScore = parseFloat(lead.qualificationScore || lead.QualificationScore || 0);
score += Math.min(qualScore * 0.4, 40); // Cap at 40 points
// Revenue/Company size (0-25 points)
const revenue = parseFloat(lead.AnnualRevenue || lead.annualRevenue || 0);
if (revenue > 100000000)
score += 25; // $100M+ = 25 pts
else if (revenue > 50000000)
score += 20; // $50M+ = 20 pts
else if (revenue > 10000000)
score += 15; // $10M+ = 15 pts
else if (revenue > 1000000)
score += 10; // $1M+ = 10 pts
else if (revenue > 0)
score += 5; // Any revenue = 5 pts
// Lead status/rating (0-20 points)
const status = (lead.Status || lead.status || '').toLowerCase();
const rating = (lead.Rating || lead.rating || '').toLowerCase();
if (status === 'qualified' || rating === 'hot')
score += 20;
else if (status === 'working' || rating === 'warm')
score += 15;
else if (status === 'new' || rating === 'medium')
score += 10;
else if (status === 'nurture' || rating === 'cold')
score += 5;
// Activity recency (0-10 points)
const lastActivity = lead.LastActivity || lead.lastActivity;
if (lastActivity) {
const daysSinceActivity = Math.floor((Date.now() - new Date(lastActivity).getTime()) / (1000 * 60 * 60 * 24));
if (daysSinceActivity <= 1)
score += 10; // Today/yesterday = 10 pts
else if (daysSinceActivity <= 7)
score += 7; // This week = 7 pts
else if (daysSinceActivity <= 30)
score += 4; // This month = 4 pts
}
// Next step urgency (0-5 points)
const nextStep = (lead.NextStep || lead.nextStep || '').toLowerCase();
if (nextStep.includes('demo') || nextStep.includes('meeting') || nextStep.includes('call'))
score += 5;
else if (nextStep.includes('follow-up') || nextStep.includes('proposal'))
score += 3;
// Description engagement indicators (bonus points)
const description = (lead.Description || lead.description || '').toLowerCase();
if (description.includes('browsing') || description.includes('interested'))
score += 2;
if (description.includes('urgent') || description.includes('hot'))
score += 3;
return Math.min(Math.round(score), 100); // Cap at 100
}
// Rosa Category Determination
determineRosaCategory(lead) {
const status = (lead.Status || lead.status || '').toLowerCase();
const rating = (lead.Rating || lead.rating || '').toLowerCase();
const nextStep = (lead.NextStep || lead.nextStep || '').toLowerCase();
const description = (lead.Description || lead.description || '').toLowerCase();
// High engagement categories
if (status === 'qualified' || rating === 'hot' || description.includes('hot')) {
return 'Hot Lead, Qualified';
}
if (nextStep.includes('meeting') || nextStep.includes('demo')) {
return 'Meeting, Existing Product Expansion';
}
if (status === 'new' || description.includes('new')) {
return 'First Product, New Product Expansion';
}
// Lower engagement categories
if (status === 'unqualified' || !lead.LastActivity) {
return 'Not Contacted, Expiring Opportunities';
}
if (status === 'nurture') {
return 'Not Contacted, Expiring Triggers';
}
// Default
return 'Active Prospect, Standard Priority';
}
// Extract upcoming "meetings" from NextStep fields and descriptions
extractUpcomingMeetings(leads) {
const meetings = [];
leads.forEach(lead => {
const nextStep = (lead.NextStep || lead.nextStep || '').toString();
const description = (lead.Description || lead.description || '').toString();
const leadName = lead.FirstName ? `${lead.FirstName} ${lead.LastName || ''}`.trim() : lead.name;
const company = lead.Company || lead.company;
// Check NextStep field for meetings
if (nextStep && nextStep.length > 0) {
const meetingKeywords = ['meeting', 'call', 'demo', 'appointment', 'discussion', 'presentation'];
const hasMeetingKeyword = meetingKeywords.some(keyword => nextStep.toLowerCase().includes(keyword));
if (hasMeetingKeyword) {
meetings.push({
person: leadName,
company: company,
context: nextStep,
priority: this.extractPriorityFromNextStep(nextStep),
estimated_time: this.extractTimeFromNextStep(nextStep),
source: 'NextStep Field'
});
}
}
// Check description field for meeting references
if (description && description.length > 20) {
const meetingPatterns = [
/meeting.*(?:scheduled|planned|set)/i,
/(?:call|demo).*(?:scheduled|planned|set)/i,
/(?:scheduled|planning).*(?:meeting|call|demo)/i
];
meetingPatterns.forEach(pattern => {
const match = description.match(pattern);
if (match) {
meetings.push({
person: leadName,
company: company,
context: match[0],
priority: this.extractPriorityFromNextStep(match[0]),
estimated_time: this.extractTimeFromNextStep(match[0]),
source: 'Description Field'
});
}
});
}
});
// Remove duplicates and return
const uniqueMeetings = meetings.filter((meeting, index, self) => index === self.findIndex(m => m.person === meeting.person && m.company === meeting.company));
return uniqueMeetings;
}
// Fetch priority tasks from CRM Connect API
async fetchPriorityTasksFromCRM() {
try {
this.debugLog('[MCP] Fetching priority tasks from CRM Connect API');
// Try to query CRM Connect tasks API
const tasksResult = await this.apiService.queryCRMTasks({
limit: 50,
offset: 0,
sort_by: 'priority',
sort_order: 'desc'
}, this.userToken);
if (!tasksResult.success) {
this.debugLog(`[MCP] CRM tasks API failed: ${tasksResult.error}, using fallback approach`);
// Fallback: Extract tasks from lead descriptions and next steps
return this.extractTasksFromLeadData();
}
if (!Array.isArray(tasksResult.data) || tasksResult.data.length === 0) {
this.debugLog('[MCP] No tasks found in CRM, checking lead data for task information');
return this.extractTasksFromLeadData();
}
const tasks = tasksResult.data.map((task) => ({
id: task.id,
task: task.subject || task.Subject || 'Unnamed Task',
description: task.description || task.Description || '',
priority: this.normalizePriority(task.priority || task.Priority),
status: task.status || task.Status || 'Unknown',
due: this.formatTaskDueDate(task.dueDate || task.ActivityDate),
created: task.createdDate || task.CreatedDate,
lead_id: task.relatedToId || task.whoId,
lead_name: task.relatedToName || 'Unknown Contact',
company: task.relatedToCompany || 'Unknown Company',
crm_provider: task.crmProvider || 'Unknown CRM',
rosa_summary: `${this.normalizePriority(task.priority || task.Priority).toUpperCase()}: ${task.subject || 'Task'}`
}));
// Sort by Rosa priority scoring
return tasks
.sort((a, b) => this.calculateTaskPriorityScore(b) - this.calculateTaskPriorityScore(a))
.slice(0, 20); // Limit to top 20 priority tasks
}
catch (error) {
this.debugLog('[MCP] Error fetching CRM tasks:', error);
// Fallback to extracting from NextStep if tasks API fails
return [];
}
}
// Extract priority tasks from Description fields (FALLBACK METHOD)
extractPriorityTasks(leads) {
const tasks = [];
leads.forEach(lead => {
const description = lead.Description || lead.description || '';
const nextStep = lead.NextStep || lead.nextStep || '';
// Parse structured tasks from description
if (description.includes('TASK:')) {
const taskMatches = description.match(/TASK:([^|]+)\|PRIORITY:([^|]+)(?:\|DUE:([^|]+))?/g);
if (taskMatches) {
taskMatches.forEach((match) => {
const parts = match.split('|');
tasks.push({
task: parts[0].replace('TASK:', '').trim(),
priority: parts[1].replace('PRIORITY:', '').trim(),
due: parts[2] ? parts[2].replace('DUE:', '').trim() : 'No deadline',
lead: lead.FirstName ? `${lead.FirstName} ${lead.LastName || ''}`.trim() : lead.name,
company: lead.Company || lead.company
});
});
}
}
// Generate tasks from nextStep if no structured tasks
if (nextStep && !description.includes('TASK:')) {
tasks.push({
task: nextStep,
priority: this.inferPriorityFromContent(nextStep + ' ' + description),
due: 'Soon',
lead: lead.FirstName ? `${lead.FirstName} ${lead.LastName || ''}`.trim() : lead.name,
company: lead.Company || lead.company
});
}
});
return tasks.sort((a, b) => {
const priorityOrder = { 'urgent': 4, 'high': 3, 'medium': 2, 'low': 1 };
const aPriority = priorityOrder[a.priority.toLowerCase()] || 1;
const bPriority = priorityOrder[b.priority.toLowerCase()] || 1;
return bPriority - aPriority;
});
}
extractTimeFromNextStep(nextStep) {
const timeRegex = /(\d{1,2}:\d{2}\s*(?:AM|PM)?|\d{1,2}\s*(?:AM|PM))/i;
const match = nextStep.match(timeRegex);
return match ? match[0] : 'Time TBD';
}
extractPriorityFromNextStep(nextStep) {
if (!nextStep)
return 'Medium';
const lower = nextStep.toString().toLowerCase();
if (lower.includes('urgent') || lower.includes('asap'))
return 'Urgent';
if (lower.includes('important') || lower.includes('priority'))
return 'High';
return 'Medium';
}
inferPriorityFromContent(content) {
if (!content)
return 'low';
const lower = content.toString().toLowerCase();
if (lower.includes('urgent') || lower.includes('asap') || lower.includes('hot'))
return 'urgent';
if (lower.includes('important') || lower.includes('qualified') || lower.includes('demo'))
return 'high';
if (lower.includes('follow') || lower.includes('check'))
return 'medium';
return 'low';
}
// Rosa Daily Briefing Formatter
formatRosaDailyBriefing(data) {
const { topLeads, upcomingMeetings, priorityTasks, greetingStyle, topLeadsCount } = data;
// Generate greeting based on style
let greeting = '';
const currentTime = new Date().toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
switch (greetingStyle) {
case 'rosa':
greeting = `Hello - Good morning! Here's your day at a glance:\n⢠Top ${topLeadsCount} leads ranked by action score\n⢠Upcoming meetings\n⢠Key priority tasks\nWould you like details on your top leads?`;
break;
case 'casual':
greeting = `Hey! Good morning! Ready to crush today? Here's what's on deck:`;
break;
case 'formal':
greeting = `Good morning. Your daily sales briefing is ready for review.`;
break;
default:
greeting = `Good morning! Here's your day at a glance:`;
}
// Format top leads in Rosa style
const formattedTopLeads = topLeads.map((lead, index) => {
const name = lead.FirstName ? `${lead.FirstName} ${lead.LastName || ''}`.trim() : lead.name || 'Unknown';
const company = lead.Company || lead.company || 'Unknown Company';
const actionScore = lead.actionScore || this.calculateRosaActionScore(lead);
const category = lead.rosaCategory || this.determineRosaCategory(lead);
return {
rank: index + 1,
name,
company,
action_score: actionScore,
category,
email: lead.Email || lead.email,
phone: lead.Phone || lead.phone,
title: lead.Title || lead.title,
revenue: lead.AnnualRevenue || lead.annualRevenue,
last_activity: lead.LastActivity || lead.lastActivity,
next_step: lead.NextStep || lead.nextStep,
rosa_summary: `${name} ā ${company} (Action Score: ${actionScore}, ${category})`
};
});
// Format upcoming meetings
const formattedMeetings = upcomingMeetings.map((meeting) => ({
person: meeting.person,
company: meeting.company,
context: meeting.context,
priority: meeting.priority,
estimated_time: meeting.estimated_time,
summary: `${meeting.person} (${meeting.company}) - ${meeting.context}`
}));
// Format priority tasks
const formattedTasks = priorityTasks.slice(0, 8).map((task) => ({
task: task.task,
priority: task.priority,
due: task.due,
lead: task.lead,
company: task.company,
summary: `${task.priority.toUpperCase()}: ${task.task} (${task.lead})`
}));
// Generate Rosa insights and recommendations
const rosaInsights = this.generateRosaInsights(topLeads, upcomingMeetings, priorityTasks);
// Create summary
const summary = {
total_leads_analyzed: topLeads.length,
highest_action_score: topLeads.length > 0 ? Math.max(...topLeads.map((l) => l.actionScore || 0)) : 0,
meetings_scheduled: formattedMeetings.length,
urgent_tasks: formattedTasks.filter((t) => t.priority.toLowerCase() === 'urgent').length,
top_recommendation: this.getTopRecommendation(topLeads, priorityTasks)
};
return {
greeting,
summary,
topLeads: formattedTopLeads,
upcomingMeetings: formattedMeetings,
priorityTasks: formattedTasks,
rosaInsights
};
}
generateRosaInsights(topLeads, meetings, tasks) {
const insights = {
focus_recommendations: [],
urgency_alerts: [],
opportunity_highlights: [],
productivity_tips: []
};
// Focus recommendations based on top leads
if (topLeads.length > 0) {
const topLead = topLeads[0];
if (topLead.actionScore >= 90) {
insights.focus_recommendations.push(`š„ PRIORITY: ${topLead.name} at ${topLead.company} has an action score of ${topLead.actionScore} - extremely high potential!`);
}
const hotLeads = topLeads.filter((l) => l.actionScore >= 85);
if (hotLeads.length > 1) {
insights.focus_recommendations.push(`šÆ You have ${hotLeads.length} leads with 85+ action scores - consider parallel outreach strategies.`);
}
}
// Urgency alerts
const urgentTasks = tasks.filter(t => t.priority.toLowerCase() === 'urgent');
if (urgentTasks.length > 0) {
insights.urgency_alerts.push(`ā ļø URGENT: ${urgentTasks.length} high-priority tasks need immediate attention.`);
}
const todaysMeetings = meetings.filter(m => (m.context && m.context.toLowerCase().includes('today')) ||
(m.estimated_time && m.estimated_time !== 'Time TBD'));
if (todaysMeetings.length > 0) {
insights.urgency_alerts.push(`š
TODAY: ${todaysMeetings.length} meetings scheduled - prepare talking points.`);
}
// Opportunity highlights
const qualifiedLeads = topLeads.filter((l) => (l.category && l.category.includes('Qualified')) ||
(l.category && l.category.includes('Hot')));
if (qualifiedLeads.length > 0) {
insights.opportunity_highlights.push(`š° ${qualifiedLeads.length} qualified leads ready for closing activities.`);
}
// Productivity tips
insights.productivity_tips.push('š Start with your highest action score lead for maximum impact.');
if (meetings.length > 0) {
insights.productivity_tips.push('š Use meeting prep tool for detailed conversation strategies.');
}
return insights;
}
getTopRecommendation(topLeads, tasks) {
if (topLeads.length === 0)
return 'Focus on lead generation and qualification activities.';
const topLead = topLeads[0];
const urgentTasks = tasks.filter(t => t.priority.toLowerCase() === 'urgent');
if (urgentTasks.length > 0) {
return `Handle urgent task: ${urgentTasks[0].task}, then focus on ${topLead.name} (Action Score: ${topLead.actionScore})`;
}
return `Focus on ${topLead.name} at ${topLead.company} - highest action score (${topLead.actionScore}) with strong potential.`;
}
// Helper methods for CRM tasks processing
formatTaskDueDate(dueDate) {
if (!dueDate)
return 'No due date';
try {
const due = new Date(dueDate);
const now = new Date();
const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0)
return 'Today';
if (diffDays === 1)
return 'Tomorrow';
if (diffDays === -1)
return 'Yesterday (Overdue)';
if (diffDays < 0)
return `${Math.abs(diffDays)} days overdue`;
if (diffDays <= 7)
return `${diffDays} days`;
return due.toLocaleDateString();
}
catch (error) {
return dueDate;
}
}
// Google Maps Link Generation (using existing CRM address data)
generateGoogleMapsLinks(address, options = {}) {
const encodedAddress = encodeURIComponent(address.trim());
const mapType = options.map_type || 'standard';
const includeDirections = options.include_directions || false;
const fromAddress = options.from_address;
// Generate different Google Maps links
const links = {
// Standard Google Maps view
standard_view: `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`,
// Direct location link (opens in Google Maps app if available)
location_link: `https://maps.google.com/?q=${encodedAddress}`,
// Satellite view
satellite_view: `https://www.google.com/maps/search/?api=1&query=${encodedAddress}&map_action=map&basemap=satellite`,
// Street view (if available)
street_view: `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${encodedAddress}`,
// Embeddable map link
embed_link: `https://www.google.com/maps/embed/v1/place?key=YOUR_API_KEY&q=${encodedAddress}`,
// Mobile-friendly link
mobile_link: `https://maps.google.com/maps?q=${encodedAddress}&hl=en&gl=us`
};
// Add directions link if requested
if (includeDirections && fromAddress) {
const encodedFromAddress = encodeURIComponent(fromAddress.trim());
links.directions = `https://www.google.com/maps/dir/?api=1&origin=${encodedFromAddress}&destination=${encodedAddress}&travelmode=driving`;
links.transit_directions = `https://www.google.com/maps/dir/?api=1&origin=${encodedFromAddress}&destination=${encodedAddress}&travelmode=transit`;
links.walking_directions = `https://www.google.com/maps/dir/?api=1&origin=${encodedFromAddress}&destination=${encodedAddress}&travelmode=walking`;
}
// Generate additional information
const info = {
formatted_address: address,
maps_search_query: encodedAddress,
suggested_actions: [
'Click "Standard View" to see the location on Google Maps',
'Use "Mobile Link" to share with team members',
includeDirections ? 'Get driving directions with the directions link' : 'Add from_address parameter for driving directions',
'Use "Street View" to see the actual building (if available)'
],
rosa_usage: {
demo_scenario: 'Rosa can now say: "Here\'s the headquarters for Urban MartRetail Group on Google Maps. You can grab directions, check traffic, or share the location with your team."',
integration_method: 'Uses existing CRM address field data - no external API required',
supported_crm_fields: ['Street/address', 'City/city', 'State/state', 'PostalCode/zip', 'Country/country']
}
};
return { links, info };
}
// Enhanced Meeting Prep with Location Integration
enhanceMeetingPrepWithLocation(leadData, args) {
const addressParts = [
leadData.address,
leadData.city,
leadData.state,
leadData.country
].filter(Boolean);
if (addressParts.length > 0) {
const fullAddress = addressParts.join(', ');
const mapsData = this.generateGoogleMapsLinks(fullAddress);
return {
meeting_location: {
address: fullAddress,
google_maps_link: mapsData.links.standard_view,
mobile_maps_link: mapsData.links.mobile_link,
rosa_context: `Meeting location: ${fullAddress}. Google Maps link available for directions and traffic updates.`
},
location_talking_points: [
`I see you're located at ${fullAddress} - I can provide directions if needed`,
'I can share the Google Maps link with my team for easy reference',
'Let me know if you need any help with directions to your office'
]
};
}
return null;
}
// DNC Compliance Checking using Fax field as Y/N flag
checkDNCComplianceFromFaxField(contact) {
// Get the Fax field value (case insensitive)
const faxField = (contact.Fax || contact.fax || '').toString().trim().toLowerCase();
this.debugLog('[MCP] Checking DNC via Fax field:', faxField);
// If Fax field is empty, assume contact is allowed (default behavior)
if (!faxField || faxField === '') {
return {
cleared: true,
reason: 'Fax field is empty - no DNC restriction found (default: cleared)'
};
}
// Check for DNC indicators (Y/Yes/True/1 = DO NOT CALL)
const dncIndicators = ['y', 'yes', 'true', '1', 'dnc', 'do not call'];
const clearIndicators = ['n', 'no', 'false', '0'];
if (faxField && dncIndicators.includes(faxField)) {
return {
cleared: false,
reason: `Fax field set to "${contact.Fax}" - Contact is on Do Not Call list`
};
}
if (faxField && clearIndicators.includes(faxField)) {
return {
cleared: true,
reason: `Fax field set to "${contact.Fax}" - Contact explicitly cleared for calling`
};
}
// If Fax field has some other value, treat as cleared but note the unusual value
return {
cleared: true,
reason: `Fax field contains "${contact.Fax}" - treating as cleared (non-standard value)`
};
}
// Extract tasks from lead data when CRM tasks API is unavailable
async extractTasksFromLeadData() {
try {
// Get leads data to extract task information from descriptions and next steps
const leadsResult = await this.apiService.queryCRM({
entity_type: 'leads',
limit: 50,
offset: 0,
sort_order: 'desc'
}, this.userToken);
if (!leadsResult.success || !Array.isArray(leadsResult.data)) {
return [];
}
const tasks = [];
leadsResult.data.forEach((lead) => {
const nextStep = (lead.NextStep || lead.nextStep || '').toString();
const description = (lead.Description || lead.description || '').toString();
const leadName = `${lead.FirstName || ''} ${lead.LastName || ''}`.trim();
const company = lead.Company || lead.company;
// Extract tasks from NextStep field
if (nextStep && nextStep.length > 5) {
tasks.push({
id: `task_${lead.Id || lead.id}_nextstep`,
task: nextStep,
description: `Next step for ${leadName}`,
priority: this.inferPriorityFromContent(nextStep + ' ' + description),
status: 'Not Started',
due: 'Soon',
created: lead.CreatedDate || lead.createdDate || new Date().toISOString(),
lead_id: lead.Id || lead.id,
lead_name: leadName,
company: company,
crm_provider: 'NextStep Field',
rosa_summary: `${this.inferPriorityFromContent(nextStep).toUpperCase()}: ${nextStep} (${leadName})`
});
}
// Extract tasks from description if it contains action items
if (description && description.length > 20) {
const actionKeywords = ['follow up', 'send', 'call', 'email', 'schedule', 'prepare', 'review'];
const hasActionKeyword = actionKeywords.some(keyword => description.toLowerCase().includes(keyword));
if (hasActionKeyword) {
// Extract first sentence that contains action keywords
const sentences = description.split(/[.!?]+/).filter((s) => s.trim().length > 10);
const actionSentence = sentences.find((sentence) => actionKeywords.some(keyword => sentence.toLowerCase().includes(keyword)));
if (actionSentence && actionSentence.length > 10) {
tasks.push({
id: `task_${lead.Id || lead.id}_description`,
task: actionSentence.trim(),
description: `Action item from lead description`,
priority: this.inferPriorityFromContent(actionSentence + ' ' + nextStep),
status: 'Identified',
due: 'To be scheduled',
created: lead.CreatedDate || lead.createdDate || new Date().toISOString(),
lead_id: lead.Id || lead.id,
lead_name: leadName,
company: company,
crm_provider: 'Description Field',
rosa_summary: `${this.inferPriorityFromContent(actionSentence).toUpperCase()}: ${actionSentence.substring(0, 50)}... (${leadName})`
});
}
}
}
});
// Sort by priority and return top tasks
return tasks
.sort((a, b) => this.calculateTaskPriorityScore(b) - this.calculateTaskPriorityScore(a))
.slice(0, 20);
}
catch (error) {
this.debugLog('[MCP] Error extracting tasks from lead data:', error);
return [];
}
}
// Fetch activities for specific leads using related-objects endpoint
async fetchActivitiesForLeads(leads) {
try {
this.debugLog('[MCP] Fetching activities for top leads using related-objects endpoint');
const tasks = [];
// Get activities for each top lead
for (const lead of leads.slice(0, 3)) { // Limit to top 3 leads to avoid too many API calls
try {
const leadId = lead.Id || lead.id;
if (!leadId)
continue;
// Call the related-objects endpoint for this specific lead
const response = await fetch(`${this.apiService.config.iriseller_api_url}/api/crm-connect/related-objects?entityType=Lead&entityId=${leadId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.userToken}`,
'Content-Type': 'application/json',
'X-MCP-User-Email': this.extractEmailFromToken(this.userToken)
},
signal: AbortSignal.timeout(15000)
});
if (response.ok) {
const data = await response.json();
if (data.success && data.data && Array.isArray(data.data)) {
// Filter for tasks and activities
const leadActivities = data.data.filter((item) => item.type === 'Task' || item.type === 'Activity' || item.Subject);
leadActivities.forEach((activity) => {
tasks.push({
id: activity.Id || activity.id,
task: activity.Subject || activity.subject || 'Activity',
description: activity.Description || activity.description || '',
priority: this.normalizePriority(activity.Priority || 'Medium'),
status: activity.Status || activity.status || 'Open',
due: this.formatTaskDueDate(activity.ActivityDate || activity.DueDate),
created: activity.CreatedDate || activity.createdDate,
lead_id: leadId,
lead_name: `${lead.FirstName || lead.firstName || ''} ${lead.LastName || lead.lastName || ''}`.trim(),
company: lead.Company || lead.company,
crm_provider: 'CRM Related Objects',
rosa_summary: `${this.normalizePriority(activity.Priority || 'Medium').toUpperCase()}: ${activity.Subject || 'Activity'}`
});
});
}
}
}
catch (leadError) {
this.debugLog(`[MCP] Error fetching activities for lead ${lead.FirstName} ${lead.LastName}:`, leadError);
}
}
return tasks.sort((a, b) => this.calculateTaskPriorityScore(b) - this.calculateTaskPriorityScore(a));
}
catch (error) {
this.debugLog('[MCP] Error fetching activities for leads:', error);
return [];
}
}
// Helper to extract email from JWT token
extractEmailFromToken(token) {
if (!token)
return '';
try {
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
return payload.email || '';
}
catch (error) {
return '';
}
}
// Fetch activities from CRM using the same pattern as other CRM Connect calls
async fetchActivitiesFromCRM() {
try {
this.debugLog('[MCP] Fetching priority tasks using same auth pattern as leads API');
// Use the exact same approach as the working queryCRMTasks method
const tasksResult = await this.apiService.queryCRMTasks({
limit: 50,
offset: 0,
sort_by: 'priority',
sort_order: 'desc'
}, this.userToken);
if (!tasksResult.success) {
this.debugLog(`[MCP] CRM tasks API failed: ${tasksResult.error}`);
return [];
}
if (!Array.isArray(tasksResult.data) || tasksResult.data.length === 0) {
this.debugLog('[MCP] No tasks found in CRM tasks API response');
return [];
}
this.debugLog(`[MCP] Found ${tasksResult.data.length} tasks from CRM using same auth as leads`);
const tasks = tasksResult.data.map((task) => ({
id: task.id,
task: task.subject || task.Subject || 'CRM Task',
description: task.description || task.Description || '',
priority: this.normalizePriority(task.priority || task.Priority || 'Medium'),
status: task.status || task.Status || 'Open',
due: this.formatTaskDueDate(task.dueDate || task.ActivityDate),
created: task.createdDate || task.CreatedDate,
lead_id: task.relatedToId || task.whoId,
lead_name: task.relatedToName || 'Unknown Contact',
company: task.relatedToCompany || 'Unknown Company',
crm_provider: 'CRM Tasks API (Same Auth as Leads)',
rosa_summary: `${this.normalizePriority(task.priority || task.Priority || 'Medium').toUpperCase()}: ${task.subject || task.Subject || 'Task'}`
}));
return tasks
.sort((a, b) => this.calculateTaskPriorityScore(b) - this.calculateTaskPriorityScore(a))
.slice(0, 20);
}
catch (error) {
this.debugLog('[MCP] Error fetching tasks from CRM:', error);
return [];
}
}
}