@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
1,076 lines (1,075 loc) • 49.5 kB
JavaScript
import axios from 'axios';
import jwt from 'jsonwebtoken';
import { FallbackService } from './fallback-service.js';
export class IRISellerAPIService {
backendApi;
crewaiApi;
crmConnectApi;
config;
userToken;
tokenExpiry;
tokenRefreshInProgress = false;
// Debug logging utility that respects debug flag
debugLog = (message, ...args) => {
if (this.config.debug) {
console.error(message, ...args);
}
};
errorLog = (message, ...args) => {
// Always log errors to stderr (for critical issues only)
console.error(message, ...args);
};
constructor(config) {
this.config = config;
// Initialize API clients
this.backendApi = axios.create({
baseURL: config.iriseller_api_url,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'IRISeller-MCP-Server/1.0.0'
}
});
this.crewaiApi = axios.create({
baseURL: config.crewai_api_url,
timeout: 60000, // Longer timeout for AI operations
headers: {
'Content-Type': 'application/json',
'User-Agent': 'IRISeller-MCP-Server/1.0.0'
}
});
this.crmConnectApi = axios.create({
baseURL: config.crm_connect_api_url,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'IRISeller-MCP-Server/1.0.0'
}
});
// Add authentication if provided
if (config.api_key) {
const authHeader = `Bearer ${config.api_key}`;
this.backendApi.defaults.headers.common['Authorization'] = authHeader;
this.crewaiApi.defaults.headers.common['Authorization'] = authHeader;
this.crmConnectApi.defaults.headers.common['Authorization'] = authHeader;
}
// Add request/response interceptors for logging
this.setupInterceptors();
}
/**
* Set user token for authenticated operations
*/
setUserToken(token) {
this.userToken = token;
// Extract expiry from JWT token
try {
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
this.tokenExpiry = payload.exp ? payload.exp * 1000 : undefined; // Convert to milliseconds
this.debugLog(`[MCP] User token set, expires at: ${this.tokenExpiry ? new Date(this.tokenExpiry).toISOString() : 'unknown'}`);
}
catch (error) {
this.errorLog('[MCP] Failed to extract token expiry:', error);
this.tokenExpiry = undefined;
}
}
/**
* Generic method to make HTTP requests to the backend API
*/
async makeRequest(method, endpoint, options = {}) {
try {
const { params, body, headers } = options;
// Add user token to headers if available
const requestHeaders = {
...headers
};
if (this.userToken) {
requestHeaders['Authorization'] = `Bearer ${this.userToken}`;
}
const response = await this.backendApi.request({
method: method.toUpperCase(),
url: endpoint,
params,
data: body,
headers: requestHeaders
});
return {
success: true,
data: response.data,
message: 'Request completed successfully'
};
}
catch (error) {
this.errorLog(`[API] ${method.toUpperCase()} ${endpoint} failed:`, error.response?.data || error.message);
return {
success: false,
error: error.response?.data?.message || error.message || 'Request failed',
data: null
};
}
}
/**
* Generate system JWT token for internal API calls
*/
generateSystemJwtToken() {
try {
if (!this.config.jwt_secret) {
this.debugLog('[MCP] No JWT secret configured, using fallback');
// Use environment variable or fallback
const jwtSecret = process.env.JWT_SECRET || 'fallback_secret_not_for_production';
const token = jwt.sign({
userId: 'user_jchen_001', // Use real user ID from database
id: 'user_jchen_001',
email: 'jchen@iriseller.com', // Use real user email
role: 'user',
isSystemToken: true // Mark as system token for MCP operations
}, jwtSecret, { expiresIn: '4h' });
return token;
}
const token = jwt.sign({
userId: 'user_jchen_001', // Use real user ID from database
id: 'user_jchen_001',
email: 'jchen@iriseller.com', // Use real user email
role: 'user',
isSystemToken: true // Mark as system token for MCP operations
}, this.config.jwt_secret, { expiresIn: '1h' });
// Validate token format before returning
if (!token || typeof token !== 'string' || token.split('.').length !== 3) {
this.errorLog('[MCP] Generated invalid JWT token format');
return null;
}
return token;
}
catch (error) {
this.errorLog('[MCP] Error generating system JWT token:', error);
return null;
}
}
/**
* Check if token is expired or will expire soon (within 5 minutes)
*/
isTokenExpired() {
if (!this.tokenExpiry) {
return true; // Assume expired if we can't determine expiry
}
const fiveMinutesFromNow = Date.now() + (5 * 60 * 1000);
return this.tokenExpiry <= fiveMinutesFromNow;
}
/**
* Refresh the user token using system token generation
*/
async refreshUserToken() {
if (this.tokenRefreshInProgress) {
this.debugLog('[MCP] Token refresh already in progress, waiting...');
// Wait for the ongoing refresh to complete
while (this.tokenRefreshInProgress) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return !this.isTokenExpired();
}
this.tokenRefreshInProgress = true;
try {
this.debugLog('[MCP] Refreshing user token...');
// Generate new system token with proper expiration
const newToken = await this.generateUserTokenForSystem();
if (!newToken) {
this.errorLog('[MCP] Failed to generate new user token');
return false;
}
// Set the new token
this.setUserToken(newToken);
this.debugLog('[MCP] User token refreshed successfully');
return true;
}
catch (error) {
this.errorLog('[MCP] Error refreshing user token:', error);
return false;
}
finally {
this.tokenRefreshInProgress = false;
}
}
/**
* Generate a user token for system operations using default credentials
*/
async generateUserTokenForSystem() {
if (!this.config.jwt_secret) {
return null;
}
try {
// Generate token with proper expiration and user context
const defaultEmail = process.env.MCP_DEFAULT_USER_EMAIL || 'system@iriseller.com';
const payload = {
email: defaultEmail,
userId: `mcp-user-${defaultEmail}`,
id: `mcp-user-${defaultEmail}`,
role: 'user',
isSystemToken: true,
sub: defaultEmail,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (4 * 60 * 60) // 4 hours for system tokens
};
return jwt.sign(payload, this.config.jwt_secret);
}
catch (error) {
this.errorLog('[MCP] Error generating user token:', error);
return null;
}
}
/**
* Ensure we have a valid token before making requests
*/
async ensureValidToken() {
if (!this.userToken) {
this.debugLog('[MCP] No user token available, generating new one...');
return await this.refreshUserToken();
}
if (this.isTokenExpired()) {
this.debugLog('[MCP] Token is expired or will expire soon, refreshing...');
return await this.refreshUserToken();
}
return true;
}
setupInterceptors() {
const requestInterceptor = async (config) => {
// Ensure we have a valid token before making the request
await this.ensureValidToken();
if (this.config.debug) {
this.debugLog(`[MCP] API Request: ${config.method?.toUpperCase()} ${config.url}`);
}
return config;
};
const responseInterceptor = (response) => {
if (this.config.debug) {
this.debugLog(`[MCP] API Response: ${response.status} ${response.config.url}`);
}
return response;
};
const errorInterceptor = async (error) => {
const originalRequest = error.config;
// Don't log 404 errors for agent results polling - these are expected during completion waiting
const isAgentResultsPolling = error.config?.url?.includes('/agents/results/');
const is404Error = error.response?.status === 404;
if (!(isAgentResultsPolling && is404Error)) {
this.errorLog(`[MCP] API Error: ${error.message}`, {
url: error.config?.url,
status: error.response?.status,
data: error.response?.data
});
}
// Handle 401 errors with automatic token refresh and retry
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
this.debugLog('[MCP] Got 401 error, attempting token refresh and retry...');
const refreshSuccess = await this.refreshUserToken();
if (refreshSuccess && this.userToken) {
// Update the authorization header with new token
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers['Authorization'] = `Bearer ${this.userToken}`;
this.debugLog('[MCP] Retrying request with new token');
// Retry the original request with the new token
if (originalRequest.baseURL?.includes(this.config.iriseller_api_url)) {
return this.backendApi.request(originalRequest);
}
else if (originalRequest.baseURL?.includes(this.config.crewai_api_url)) {
return this.crewaiApi.request(originalRequest);
}
else if (originalRequest.baseURL?.includes(this.config.crm_connect_api_url)) {
return this.crmConnectApi.request(originalRequest);
}
}
this.errorLog('[MCP] Token refresh failed or no valid token available');
}
return Promise.reject(error);
};
[this.backendApi, this.crewaiApi, this.crmConnectApi].forEach(api => {
api.interceptors.request.use(requestInterceptor, (error) => Promise.reject(error));
api.interceptors.response.use(responseInterceptor, errorInterceptor);
});
}
// Health check methods
async checkHealth() {
const results = {
backend: false,
crewai: false,
crmConnect: false
};
try {
await this.backendApi.get('/api/health');
results.backend = true;
}
catch (error) {
this.debugLog('[MCP] Backend health check failed:', error);
}
try {
await this.crewaiApi.get('/health');
results.crewai = true;
}
catch (error) {
this.debugLog('[MCP] CrewAI health check failed:', error);
}
try {
// CRM Connect health check requires authentication
const systemToken = this.generateSystemJwtToken();
if (!systemToken) {
this.debugLog('[MCP] Failed to generate system token for CRM Connect health check');
results.crmConnect = false;
}
else {
const response = await this.backendApi.get('/api/crm-connect/health', {
headers: {
'Authorization': `Bearer ${systemToken}`
}
});
// Check if the response indicates success
if (response.data && response.data.success) {
results.crmConnect = true;
}
}
}
catch (error) {
this.debugLog('[MCP] CRM Connect health check failed:', error);
}
return results;
}
// CRM Operations
async queryCRMTasks(query, userToken) {
try {
// Use the same authentication approach as queryCRM (which works)
const authToken = userToken || this.generateSystemJwtToken();
if (!authToken) {
return {
success: false,
error: 'Failed to generate authentication token for CRM tasks query',
data: []
};
}
// Extract email from JWT token if available
let userEmail = null;
if (userToken) {
try {
const payload = JSON.parse(Buffer.from(userToken.split('.')[1], 'base64').toString());
userEmail = payload.email;
}
catch (error) {
this.debugLog('[MCP] Failed to extract email from JWT token:', error);
}
}
const params = new URLSearchParams();
// Add query parameters for tasks API
if (query.limit)
params.append('limit', query.limit.toString());
if (query.offset)
params.append('offset', query.offset.toString());
if (query.status) {
if (Array.isArray(query.status)) {
query.status.forEach((status) => params.append('status', status));
}
else {
params.append('status', query.status);
}
}
if (query.priority)
params.append('priority', query.priority);
if (query.sort_by)
params.append('sortBy', query.sort_by);
if (query.sort_order)
params.append('sortOrder', query.sort_order);
// Set up headers with authentication
const headers = {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
'User-Agent': 'IRISeller-MCP-Server/1.0'
};
// Add user email header for user-specific CRM API key lookup
if (userEmail) {
headers['X-MCP-User-Email'] = userEmail;
}
this.debugLog('[MCP] Calling CRM Connect tasks API with same auth as leads');
this.debugLog(`[MCP] Tasks API URL: ${this.config.iriseller_api_url}/api/tasks-v2/high-priority?${params.toString()}`);
this.debugLog('[MCP] Tasks API headers:', headers);
// Use the tasks-v2/high-priority endpoint (same as frontend)
const response = await fetch(`${this.config.iriseller_api_url}/api/tasks-v2/high-priority?${params.toString()}`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30000)
});
if (!response.ok) {
const errorData = await response.text();
this.debugLog('[MCP] CRM tasks API error response:', errorData);
return {
success: false,
error: `CRM tasks API error: ${response.status} ${response.statusText}`,
data: []
};
}
const data = await response.json();
this.debugLog('[MCP] CRM tasks API response received, data type:', typeof data);
this.debugLog('[MCP] CRM tasks API response preview:', JSON.stringify(data).substring(0, 500));
if (data.success && Array.isArray(data.data)) {
this.debugLog(`[MCP] Successfully retrieved ${data.data.length} tasks from CRM Connect Tasks API`);
return {
success: true,
data: data.data,
message: `Retrieved ${data.data.length} tasks from CRM`
};
}
else {
this.debugLog('[MCP] Invalid response format from CRM tasks API:', JSON.stringify(data).substring(0, 500));
return {
success: false,
error: data.error || 'No tasks found or invalid response format',
data: []
};
}
}
catch (error) {
this.debugLog('[MCP] CRM tasks query error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred while querying CRM tasks',
data: []
};
}
}
async queryCRM(query, userToken) {
try {
// Use provided user token instead of system token for CRM queries
// This allows access to user-specific CRM Connect API keys stored in the database
const authToken = userToken || this.generateSystemJwtToken();
if (!authToken) {
return {
success: false,
error: 'Failed to generate authentication token for CRM query',
data: []
};
}
// Extract email from JWT token if available
let userEmail = null;
if (userToken) {
try {
const payload = JSON.parse(Buffer.from(userToken.split('.')[1], 'base64').toString());
userEmail = payload.email;
}
catch (error) {
this.debugLog('[MCP] Failed to extract email from JWT token:', error);
}
}
const params = new URLSearchParams();
// Smart page size: use larger pageSize when filtering by specific fields to ensure we get enough data to filter
let pageSize = query.limit || 10;
const hasSpecificFilters = query.filters && (query.filters.email ||
query.filters.firstName ||
query.filters.lastName ||
query.filters.name ||
query.filters.phone ||
query.filters.title);
if (hasSpecificFilters && pageSize < 100) {
// Use larger pageSize for filtering, but we'll limit results after filtering
pageSize = Math.min(200, Math.max(100, pageSize * 20));
this.debugLog(`[MCP] Using larger pageSize (${pageSize}) for specific field filtering`);
}
params.append('pageSize', pageSize.toString());
if (query.offset)
params.append('offset', query.offset.toString());
if (query.sort_by)
params.append('sortBy', query.sort_by);
if (query.sort_order)
params.append('sortOrder', query.sort_order);
// Add filters as query parameters with special handling for contact name searches
if (query.filters) {
Object.entries(query.filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
// Special handling for 'name' filter - split into firstName and lastName for both leads and contacts
if (key === 'name' && (query.entity_type === 'contacts' || query.entity_type === 'leads') && typeof value === 'string') {
const nameParts = value.toString().trim().split(' ');
if (nameParts.length >= 2) {
params.append('firstName', nameParts[0]);
params.append('lastName', nameParts.slice(1).join(' '));
this.debugLog(`[MCP] Split ${query.entity_type} name '${value}' into firstName='${nameParts[0]}' and lastName='${nameParts.slice(1).join(' ')}'`);
}
else if (nameParts.length === 1) {
// If only one name part, try as firstName first
params.append('firstName', nameParts[0]);
this.debugLog(`[MCP] Using single name '${value}' as firstName for ${query.entity_type} search`);
}
}
// Special handling for 'company' filter in leads - use 'company' field directly
else if (key === 'company' && query.entity_type === 'leads') {
params.append('company', value.toString());
this.debugLog(`[MCP] Using company filter '${value}' for leads search`);
}
// Special handling for 'company' filter in contacts - might need to use accountName
else if (key === 'company' && query.entity_type === 'contacts') {
params.append('accountName', value.toString());
this.debugLog(`[MCP] Using company filter '${value}' as accountName for contacts search`);
}
else {
params.append(key, value.toString());
}
}
});
}
const headers = {
'Authorization': `Bearer ${authToken}`
};
// Add MCP user email header if we have one (either from token or environment)
const mcpUserEmail = userEmail || process.env.MCP_DEFAULT_USER_EMAIL || 'system@iriseller.com';
if (mcpUserEmail) {
headers['x-mcp-user-email'] = mcpUserEmail;
this.debugLog(`[MCP] Using email for CRM Connect API: ${mcpUserEmail}`);
}
const response = await this.backendApi.get(`/api/crm-connect/${query.entity_type}?${params.toString()}`, {
headers
});
let finalData = response.data.data || response.data;
// If we used a larger pageSize for filtering, limit the final results to the original requested limit
const originalLimit = query.limit || 10;
if (hasSpecificFilters && finalData.length > originalLimit) {
this.debugLog(`[MCP] Limiting results from ${finalData.length} to ${originalLimit}`);
finalData = finalData.slice(0, originalLimit);
}
return {
success: true,
data: finalData,
metadata: response.data.metadata, // Pass through metadata from API response
message: `Retrieved ${query.entity_type} successfully`
};
}
catch (error) {
this.errorLog('[MCP] CRM query error:', error.message);
return {
success: false,
error: error.message || 'Failed to query CRM',
data: []
};
}
}
// Agent Execution - Fixed to use correct CrewAI endpoints
async executeAgent(request) {
try {
// Extract user context from stored token for CRM personalization AND configuration selection
let userEmail = process.env.MCP_DEFAULT_USER_EMAIL || 'system@iriseller.com'; // Default fallback
let userId = null;
if (this.userToken) {
try {
// Decode JWT token to extract user email and id (without verification for internal use)
const tokenParts = this.userToken.split('.');
if (tokenParts.length === 3) {
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
if (payload.email) {
userEmail = payload.email;
// Extract user ID from the real token payload
userId = payload.userId || payload.id || payload.sub;
this.debugLog(`[MCP] Using user context: ${userEmail} (ID: ${userId}) for agent execution`);
}
}
}
catch (tokenError) {
this.debugLog('[MCP] Failed to extract user context from token, using default');
}
}
// For configuration requests, get a real authentication token instead of using system token
// Use the existing auth token (no additional login needed)
let realAuthToken = this.userToken;
// Use the backend SDR crew endpoint instead of calling CrewAI directly
// This ensures proper execution tracking and database consistency
const response = await this.backendApi.post('/sdr-crew/agents/execute', {
agent_name: request.agent_name,
inputs: {
...request.input_data,
user_email: userEmail, // Pass user context for CRM lookups
user_id: userId, // Pass user ID for configuration lookup
config_id: request.input_data.config_id, // Allow explicit config selection
auth_token: realAuthToken // Pass real JWT token for backend authentication
},
config: request.options || {}
});
const executionId = response.data.execution_id || `exec_${Date.now()}`;
const initialStatus = response.data.status || 'success';
// If execution is running/pending, wait for completion for better UX
if (initialStatus === 'running' || initialStatus === 'pending') {
this.debugLog(`[MCP] Agent execution started, waiting for completion: ${executionId}`);
// Wait for completion with timeout (max 60 seconds)
const completedResults = await this.waitForAgentCompletion(executionId, request.agent_name, 60000);
if (completedResults) {
return {
execution_id: executionId,
status: completedResults.status || 'completed',
agent_name: request.agent_name,
results: completedResults.result || completedResults,
execution_time: completedResults.execution_time || 0
};
}
else {
// Fallback to async polling if wait times out
this.pollForAgentCompletion(executionId, request.agent_name);
return {
execution_id: executionId,
status: 'pending',
agent_name: request.agent_name,
results: {
message: `${request.agent_name} agent execution started. Polling for completion...`,
websocket_url: response.data.websocket_url,
initial_response: response.data
},
execution_time: 0
};
}
}
return {
execution_id: executionId,
status: initialStatus,
agent_name: request.agent_name,
results: response.data.results || response.data,
execution_time: response.data.execution_time || 0
};
}
catch (error) {
return {
execution_id: `error_${Date.now()}`,
status: 'error',
agent_name: request.agent_name,
error: error.message || 'Agent execution failed'
};
}
}
// Wait for agent execution completion with timeout
async waitForAgentCompletion(executionId, agentName, timeoutMs = 60000) {
this.debugLog(`[MCP] Waiting for agent completion: ${executionId} (timeout: ${timeoutMs}ms)`);
const startTime = Date.now();
const pollInterval = 2000; // 2 seconds
while (Date.now() - startTime < timeoutMs) {
try {
const results = await this.getAgentExecutionResults(executionId);
if (results && (results.status === 'completed' || results.status === 'failed')) {
this.debugLog(`[MCP] Agent execution ${executionId} completed with status: ${results.status}`);
return results;
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
catch (error) {
this.debugLog(`[MCP] Error waiting for execution ${executionId}:`, error);
// Continue waiting despite errors
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
}
this.debugLog(`[MCP] Wait timeout for execution ${executionId} after ${timeoutMs}ms`);
return null;
}
// Poll for agent execution completion
async pollForAgentCompletion(executionId, agentName, maxAttempts = 30) {
this.debugLog(`[MCP] Starting polling for agent completion: ${executionId}`);
let attempts = 0;
const pollInterval = 5000; // 5 seconds
const poll = async () => {
attempts++;
try {
// Check multiple endpoints for results
const results = await this.getAgentExecutionResults(executionId);
if (results && (results.status === 'completed' || results.status === 'failed')) {
this.debugLog(`[MCP] Agent execution ${executionId} completed with status: ${results.status}`);
// TODO: Implement callback mechanism to notify the requesting client
// For now, we'll log the completion and store it for retrieval
this.storeCompletedExecution(executionId, results);
return; // Stop polling
}
if (attempts >= maxAttempts) {
this.debugLog(`[MCP] Polling timeout for execution ${executionId} after ${maxAttempts} attempts`);
return;
}
// Continue polling
setTimeout(poll, pollInterval);
}
catch (error) {
this.debugLog(`[MCP] Error polling for execution ${executionId}:`, error);
if (attempts >= maxAttempts) {
return;
}
// Retry after delay
setTimeout(poll, pollInterval);
}
};
// Start polling after initial delay
setTimeout(poll, pollInterval);
}
// Get agent execution results from multiple possible endpoints
async getAgentExecutionResults(executionId) {
// Try CrewAI first since that's where the data actually is for most executions
const endpoints = [
`/sdr/agents/results/${executionId}`, // CrewAI service endpoint (primary)
`/api/sdr-crew/system/agents/results/${executionId}`, // Backend database (fallback)
];
for (const endpoint of endpoints) {
try {
let response;
if (endpoint.startsWith('/api/')) {
// Backend endpoint
response = await this.backendApi.get(endpoint);
}
else {
// CrewAI endpoint
response = await this.crewaiApi.get(endpoint);
}
if (response.data && (response.data.execution_id === executionId || response.data.id === executionId)) {
return response.data;
}
}
catch (error) {
// Only log non-404 errors to reduce noise - 404s are expected when polling
if (error.response?.status === 404 || error.message?.includes('404')) {
// 404s are expected when polling for completion, don't log as errors
continue;
}
else {
this.debugLog(`[MCP] Endpoint ${endpoint} failed for execution ${executionId}:`, error instanceof Error ? error.message : String(error));
}
continue;
}
}
return null;
}
// Store completed execution results for retrieval
completedExecutions = new Map();
storeCompletedExecution(executionId, results) {
this.completedExecutions.set(executionId, {
...results,
completed_at: new Date().toISOString(),
retrieved: false
});
// Clean up old executions after 1 hour
setTimeout(() => {
this.completedExecutions.delete(executionId);
}, 3600000);
}
// Get completed execution results
async getCompletedExecution(executionId) {
const stored = this.completedExecutions.get(executionId);
if (stored) {
// Mark as retrieved
stored.retrieved = true;
return stored;
}
// Try to fetch from backend if not in memory
return await this.getAgentExecutionResults(executionId);
}
// Workflow Execution - Fixed to use correct CrewAI endpoints
async executeWorkflow(request) {
try {
this.debugLog(`[MCP] Executing workflow: ${request.workflow_type} with input:`, JSON.stringify(request.input_data, null, 2));
let workflowPayload = {
workflow_type: request.workflow_type,
config: request.options || {}
};
// Handle different workflow types with proper parameter mapping
if (request.workflow_type === 'lead_qualification_enhancement') {
// Map the input parameters to what CrewAI expects for lead qualification
// The CrewAI service expects ProspectData model with specific field names
workflowPayload = {
...workflowPayload,
prospect_data: {
// Required ProspectData fields
name: request.input_data.contact_name || 'Unknown Contact',
company: request.input_data.company_name || 'Unknown Company',
title: request.input_data.title || 'Unknown Title',
// Optional ProspectData fields
email: request.input_data.email || null,
phone: request.input_data.phone || null,
linkedin_url: request.input_data.linkedin_url || null,
industry: request.input_data.industry || null,
company_size: request.input_data.company_size || null,
location: request.input_data.location || null,
website: request.input_data.website || null
},
company_data: request.input_data.company_data || {},
additional_context: {
criteria: request.input_data.criteria || 'BANT',
source: request.input_data.source || 'manual_request',
bulk_operation: request.input_data.bulk_operation || false,
additional_context: request.input_data.additional_context || `Lead qualification enhancement with ${request.input_data.criteria || 'BANT'} criteria`
}
};
}
else {
// For other workflow types, use the original structure
workflowPayload = {
...workflowPayload,
prospect_data: request.input_data.prospect_data || request.input_data,
company_data: request.input_data.company_data || {}
};
}
this.debugLog(`[MCP] Sending workflow payload:`, JSON.stringify(workflowPayload, null, 2));
// Use the correct CrewAI endpoint for workflow execution
const response = await this.crewaiApi.post('/sdr/workflows/execute', workflowPayload);
return {
workflow_id: response.data.execution_id || `workflow_${Date.now()}`,
status: response.data.status || 'success',
workflow_type: request.workflow_type,
results: response.data.results || response.data,
execution_time: response.data.execution_time || 0,
agents_executed: response.data.agents_executed || []
};
}
catch (error) {
this.debugLog(`[MCP] Workflow execution failed for ${request.workflow_type}:`, error.message);
if (error.response) {
this.debugLog(`[MCP] Error response:`, error.response.status, error.response.data);
}
return {
workflow_id: `error_${Date.now()}`,
status: 'error',
workflow_type: request.workflow_type,
error: error.response?.data?.message || error.message || 'Workflow execution failed'
};
}
}
// Company Research - Fixed to use correct CrewAI endpoints
async researchCompany(request) {
try {
// Use the enhanced ROSA SDR endpoint with research context
const response = await this.crewaiApi.post('/sdr/agents/enhanced-rosa/execute', {
contact_name: 'Research Request',
company_name: request.company_name,
title: 'Research Target',
industry: request.industry || 'Unknown',
additional_context: `Company research: ${request.research_depth || 'basic'} depth, competitors: ${request.include_competitors || false}, news: ${request.include_news || false}, financials: ${request.include_financials || false}`
});
return {
success: true,
data: response.data.results || response.data,
message: `Company research completed for ${request.company_name}`
};
}
catch (error) {
// Fallback to basic research endpoint if enhanced fails
try {
const fallbackResponse = await this.crewaiApi.post('/research/analyze', {
company: request.company_name,
industry: request.industry,
focus_areas: ['general_analysis', 'competitive_position']
});
return {
success: true,
data: fallbackResponse.data,
message: `Company research completed for ${request.company_name} (fallback method)`
};
}
catch (fallbackError) {
return {
success: false,
error: `Both primary and fallback research methods failed: ${fallbackError.message}`,
data: null
};
}
}
}
// Personalization - Fixed to use correct CrewAI endpoints
async personalizeOutreach(request) {
try {
// Use the personalization agent endpoint
const response = await this.crewaiApi.post('/sdr/agents/execute', {
agent_name: 'personalization',
inputs: {
prospect_data: request.prospect_data,
message_type: request.message_type,
tone: request.tone,
objectives: request.objectives,
context: request.context
},
config: {}
});
return {
success: true,
data: response.data.results || response.data,
message: `Personalized ${request.message_type} created for ${request.prospect_data.name}`
};
}
catch (error) {
// Fallback to enhanced ROSA with personalization context
try {
const fallbackResponse = await this.crewaiApi.post('/sdr/agents/enhanced-rosa/execute', {
contact_name: request.prospect_data.name,
company_name: request.prospect_data.company,
title: request.prospect_data.title || 'Contact',
industry: request.prospect_data.industry || 'Unknown',
additional_context: `Personalize ${request.message_type} with ${request.tone || 'professional'} tone. Objectives: ${request.objectives?.join(', ') || 'general outreach'}`
});
return {
success: true,
data: fallbackResponse.data.results || fallbackResponse.data,
message: `Personalized ${request.message_type} created for ${request.prospect_data.name} (fallback method)`
};
}
catch (fallbackError) {
return {
success: false,
error: `Both primary and fallback personalization methods failed: ${fallbackError.message}`,
data: null
};
}
}
}
// Get available agents - Fixed to use correct CrewAI endpoints
async getAvailableAgents() {
try {
const response = await this.crewaiApi.get('/sdr/agents');
return {
success: true,
data: response.data.agents || response.data,
message: 'Available agents retrieved successfully'
};
}
catch (error) {
this.debugLog('[MCP] Primary agents list failed, using fallback:', error.message);
return await FallbackService.fallbackAgentsList();
}
}
// Get available workflows - Fixed to use correct CrewAI endpoints
async getAvailableWorkflows() {
try {
const response = await this.crewaiApi.get('/sdr/workflows');
return {
success: true,
data: response.data.workflows || response.data,
message: 'Available workflows retrieved successfully'
};
}
catch (error) {
this.debugLog('[MCP] Primary workflows list failed, using fallback:', error.message);
return await FallbackService.fallbackWorkflowsList();
}
}
// Lead qualification (Enhanced ROSA SDR) - Fixed to use correct endpoint
// Create agent execution tracking record in database
async createAgentExecution({ userId, agentName, agentType, title, inputData, source = 'mcp_tool' }) {
try {
const authToken = this.generateSystemJwtToken();
// Use the ai-workflow service to create agent execution
const response = await this.backendApi.post('/api/ai-workflow/agents', {
userId,
agentName,
agentType,
title,
inputData,
source
}, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
return { id: response.data.id || response.data.execution_id };
}
catch (error) {
this.debugLog('[MCP] Failed to create agent execution:', error.message);
// Return a fallback ID to prevent crashes
return { id: `fallback_${Date.now()}` };
}
}
// Complete agent execution with results
async completeAgentExecution(executionId, results, executionTime, confidence, resultSummary) {
try {
const authToken = this.generateSystemJwtToken();
await this.backendApi.post(`/api/ai-workflow/agents/${executionId}/complete`, {
results,
executionTime,
confidence,
resultSummary
}, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
}
catch (error) {
this.debugLog('[MCP] Failed to complete agent execution:', error.message);
throw error;
}
}
async qualifyLead(leadData) {
try {
// Pass execution_id to CrewAI if provided
const requestData = { ...leadData };
if (leadData.execution_id) {
requestData.execution_id = leadData.execution_id;
}
// Use the enhanced ROSA SDR endpoint
const response = await this.crewaiApi.post('/sdr/agents/enhanced-rosa/execute', requestData);
return {
execution_id: leadData.execution_id || response.data.execution_id || `exec_${Date.now()}`,
status: response.data.status || 'success',
agent_name: 'enhanced_rosa_sdr',
results: response.data.results || response.data,
execution_time: response.data.execution_time || 0
};
}
catch (error) {
// Fallback to basic agent execution
try {
const fallbackResponse = await this.crewaiApi.post('/sdr/agents/execute', {
agent_name: 'rosa_sdr',
inputs: leadData,
config: {
include_research: true,
priority: 'high'
}
});
return {
execution_id: leadData.execution_id || fallbackResponse.data.execution_id || `exec_${Date.now()}`,
status: fallbackResponse.data.status || 'success',
agent_name: 'rosa_sdr',
results: fallbackResponse.data.results || fallbackResponse.data,
execution_time: fallbackResponse.data.execution_time || 0
};
}
catch (fallbackError) {
return {
execution_id: `error_${Date.now()}`,
status: 'error',
agent_name: 'rosa_sdr',
error: `Both enhanced and fallback qualification methods failed: ${fallbackError.message}`
};
}
}
}
// Sales forecasting - Fixed to use correct backend endpoint
async getForecast(params = {}, userToken) {
try {
// Use provided user token or fallback to system token
const authToken = userToken || this.generateSystemJwtToken();
if (!authToken) {
return {
success: false,
error: 'Authentication required for forecast data',
data: null
};
}
const queryParams = new URLSearchParams();
if (params.time_period)
queryParams.append('period', params.time_period);
// Don't include scenarios parameter as the endpoint doesn't use it
const response = await this.backendApi.get(`/api/revenue/forecast?${queryParams.toString()}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
return {
success: true,
data: response.data.forecast || response.data,
message: 'Sales forecast retrieved successfully'
};
}
catch (error) {
this.debugLog('[API] Forecast request failed:', error.message);
if (error.response) {
this.debugLog('[API] Error response:', error.response.status, error.response.data);
}
return {
success: false,
error: error.response?.data?.message || error.message || 'Failed to get sales forecast',
data: null
};
}
}
}