UNPKG

@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
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 }; } } }