Skip to main content
Glama

Kommo CRM MCP Server

http-streamable.ts57.6 kB
import express from 'express'; import cors from 'cors'; import { KommoAPI } from './kommo-api.js'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const app = express(); const PORT = process.env.PORT || 3001; // Environment configuration const isDevelopment = process.env.NODE_ENV !== 'production'; const logLevel = process.env.LOG_LEVEL || 'info'; // Structured logging const logger = { info: (message: string, data?: any) => { if (logLevel === 'info' || logLevel === 'debug') { console.log(`[${new Date().toISOString()}] INFO: ${message}`, data ? JSON.stringify(data, null, 2) : ''); } }, debug: (message: string, data?: any) => { if (logLevel === 'debug') { console.log(`[${new Date().toISOString()}] DEBUG: ${message}`, data ? JSON.stringify(data, null, 2) : ''); } }, error: (message: string, error?: any) => { console.error(`[${new Date().toISOString()}] ERROR: ${message}`, error); } }; // Initialize Kommo API const kommoAPI = new KommoAPI({ baseUrl: process.env.KOMMO_BASE_URL || 'https://api-g.kommo.com', accessToken: process.env.KOMMO_ACCESS_TOKEN || '' }); // Middleware app.use(cors()); app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), environment: process.env.NODE_ENV || 'development', kommo_base_url: process.env.KOMMO_BASE_URL || 'https://api-g.kommo.com' }); }); // Current year for filtering (2025 as per user preference) const currentYear = new Date().getFullYear(); // Cache system for leads data interface CacheEntry { data: any[]; timestamp: number; expiresAt: number; } const leadsCache: CacheEntry = { data: [], timestamp: 0, expiresAt: 0 }; const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds // Performance metrics interface PerformanceMetrics { totalRequests: number; cacheHits: number; cacheMisses: number; averageResponseTime: number; lastRequestTime: number; } // AI Intelligence interfaces interface SemanticAnalysis { intent: string; entities: Entity[]; context: string; confidence: number; } interface Entity { type: 'date' | 'category' | 'metric' | 'action' | 'comparison'; value: string; confidence: number; } interface TrendAnalysis { period: string; metric: string; trend: 'up' | 'down' | 'stable'; percentage: number; significance: 'high' | 'medium' | 'low'; } interface SmartSuggestion { type: 'question' | 'insight' | 'action'; content: string; relevance: number; basedOn: string; } interface ConversationMemory { sessionId: string; previousQuestions: string[]; extractedContext: any; userPreferences: any; } // Advanced AI Intelligence Interfaces interface SalesForecast { period: string; predictedSales: number; confidence: number; factors: string[]; trend: 'increasing' | 'decreasing' | 'stable'; } interface AnomalyDetection { type: 'spike' | 'drop' | 'pattern_change'; severity: 'low' | 'medium' | 'high' | 'critical'; description: string; suggestedAction: string; confidence: number; } interface CorrelationAnalysis { metric1: string; metric2: string; correlation: number; significance: number; insight: string; recommendation: string; } interface AutoInsight { type: 'opportunity' | 'warning' | 'trend' | 'anomaly'; title: string; description: string; impact: 'low' | 'medium' | 'high'; actionRequired: boolean; suggestedActions: string[]; } interface UserPattern { commonQueries: string[]; peakUsageTimes: string[]; preferredCategories: string[]; responseTimePreferences: number; } const performanceMetrics: PerformanceMetrics = { totalRequests: 0, cacheHits: 0, cacheMisses: 0, averageResponseTime: 0, lastRequestTime: 0 }; function updatePerformanceMetrics(isCacheHit: boolean, responseTime: number): void { performanceMetrics.totalRequests++; if (isCacheHit) { performanceMetrics.cacheHits++; } else { performanceMetrics.cacheMisses++; } // Update average response time const totalTime = performanceMetrics.averageResponseTime * (performanceMetrics.totalRequests - 1) + responseTime; performanceMetrics.averageResponseTime = totalTime / performanceMetrics.totalRequests; performanceMetrics.lastRequestTime = Date.now(); logger.info(`📊 Métricas: ${performanceMetrics.totalRequests} requests, ${performanceMetrics.cacheHits} cache hits, ${performanceMetrics.averageResponseTime.toFixed(2)}s média`); } // AI Intelligence Functions function analyzeSemantics(question: string): SemanticAnalysis { const questionLower = question.toLowerCase(); const entities: Entity[] = []; let intent = 'general_query'; let context = 'sales_analysis'; let confidence = 0.8; // Intent detection if (questionLower.includes('quantas') || questionLower.includes('quantos')) { intent = 'count_query'; confidence = 0.9; } else if (questionLower.includes('qual') || questionLower.includes('quais')) { intent = 'detail_query'; confidence = 0.9; } else if (questionLower.includes('mostre') || questionLower.includes('analise')) { intent = 'analysis_query'; confidence = 0.9; } else if (questionLower.includes('compare') || questionLower.includes('versus')) { intent = 'comparison_query'; confidence = 0.8; } // Entity extraction // Date entities if (questionLower.includes('hoje')) entities.push({ type: 'date', value: 'today', confidence: 0.95 }); if (questionLower.includes('ontem')) entities.push({ type: 'date', value: 'yesterday', confidence: 0.95 }); if (questionLower.includes('semana')) entities.push({ type: 'date', value: 'week', confidence: 0.9 }); if (questionLower.includes('mês') || questionLower.includes('mes')) entities.push({ type: 'date', value: 'month', confidence: 0.9 }); if (questionLower.includes('ano')) entities.push({ type: 'date', value: 'year', confidence: 0.9 }); // Category entities if (questionLower.includes('tráfego') || questionLower.includes('trafego')) entities.push({ type: 'category', value: 'tráfego', confidence: 0.95 }); if (questionLower.includes('design')) entities.push({ type: 'category', value: 'design', confidence: 0.95 }); if (questionLower.includes('marketing')) entities.push({ type: 'category', value: 'marketing', confidence: 0.95 }); if (questionLower.includes('suporte')) entities.push({ type: 'category', value: 'suporte', confidence: 0.95 }); if (questionLower.includes('contato') || questionLower.includes('contatos')) entities.push({ type: 'category', value: 'contatos', confidence: 0.9 }); // Metric entities if (questionLower.includes('vendas')) entities.push({ type: 'metric', value: 'sales', confidence: 0.9 }); if (questionLower.includes('leads')) entities.push({ type: 'metric', value: 'leads', confidence: 0.9 }); if (questionLower.includes('valor')) entities.push({ type: 'metric', value: 'value', confidence: 0.9 }); if (questionLower.includes('ticket')) entities.push({ type: 'metric', value: 'ticket', confidence: 0.9 }); // Action entities if (questionLower.includes('mostre')) entities.push({ type: 'action', value: 'show', confidence: 0.9 }); if (questionLower.includes('analise')) entities.push({ type: 'action', value: 'analyze', confidence: 0.9 }); if (questionLower.includes('compare')) entities.push({ type: 'action', value: 'compare', confidence: 0.9 }); return { intent, entities, context, confidence }; } function generateSmartSuggestions(analysis: SemanticAnalysis, leadsData: any[]): SmartSuggestion[] { const suggestions: SmartSuggestion[] = []; // Base suggestions on detected entities const hasDate = analysis.entities.some(e => e.type === 'date'); const hasCategory = analysis.entities.some(e => e.type === 'category'); const hasMetric = analysis.entities.some(e => e.type === 'metric'); if (hasDate && !hasCategory) { suggestions.push({ type: 'question', content: 'Quer analisar por categoria específica? (tráfego, design, marketing, suporte)', relevance: 0.8, basedOn: 'temporal_analysis' }); } if (hasCategory && !hasDate) { suggestions.push({ type: 'question', content: 'Quer ver dados de um período específico? (hoje, ontem, esta semana, mês passado)', relevance: 0.8, basedOn: 'category_analysis' }); } if (analysis.intent === 'count_query') { suggestions.push({ type: 'insight', content: 'Considere analisar também o valor total e ticket médio', relevance: 0.7, basedOn: 'count_intent' }); } if (analysis.intent === 'analysis_query') { suggestions.push({ type: 'action', content: 'Posso gerar insights comparativos com períodos anteriores', relevance: 0.8, basedOn: 'analysis_intent' }); } return suggestions; } function analyzeTrends(leadsData: any[], period: string): TrendAnalysis[] { const trends: TrendAnalysis[] = []; // Simple trend analysis based on lead creation dates const now = new Date(); const currentPeriod = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); return createdAt >= getDateRange(period).start && createdAt <= getDateRange(period).end; }); const previousPeriod = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); const prevStart = new Date(getDateRange(period).start); const prevEnd = new Date(getDateRange(period).end); prevStart.setDate(prevStart.getDate() - (getDateRange(period).end.getTime() - getDateRange(period).start.getTime()) / (1000 * 60 * 60 * 24)); prevEnd.setDate(prevEnd.getDate() - (getDateRange(period).end.getTime() - getDateRange(period).start.getTime()) / (1000 * 60 * 60 * 24)); return createdAt >= prevStart && createdAt <= prevEnd; }); if (previousPeriod.length > 0) { const change = ((currentPeriod.length - previousPeriod.length) / previousPeriod.length) * 100; trends.push({ period: period, metric: 'leads_count', trend: change > 5 ? 'up' : change < -5 ? 'down' : 'stable', percentage: Math.abs(change), significance: Math.abs(change) > 20 ? 'high' : Math.abs(change) > 10 ? 'medium' : 'low' }); } return trends; } // Advanced AI Intelligence Functions function detectAnomalies(leadsData: any[], period: string): AnomalyDetection[] { const anomalies: AnomalyDetection[] = []; // Get current period data const currentPeriod = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); const { start, end } = getDateRange(period); return createdAt >= start && createdAt <= end; }); // Get historical average const historicalData = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); const { start, end } = getDateRange(period); const historicalStart = new Date(start); const historicalEnd = new Date(end); historicalStart.setDate(historicalStart.getDate() - 30); // 30 days ago historicalEnd.setDate(historicalEnd.getDate() - 30); return createdAt >= historicalStart && createdAt <= historicalEnd; }); if (historicalData.length > 0) { const currentCount = currentPeriod.length; const historicalAvg = historicalData.length; const deviation = Math.abs(currentCount - historicalAvg) / historicalAvg; if (deviation > 0.5) { // 50% deviation anomalies.push({ type: currentCount > historicalAvg ? 'spike' : 'drop', severity: deviation > 1 ? 'high' : deviation > 0.7 ? 'medium' : 'low', description: `${currentCount > historicalAvg ? 'Pico' : 'Queda'} de ${Math.round(deviation * 100)}% em relação à média histórica`, suggestedAction: currentCount > historicalAvg ? 'Investigar causa do aumento' : 'Analisar possíveis problemas', confidence: Math.min(deviation, 1.0) }); } } return anomalies; } function findCorrelations(leadsData: any[]): CorrelationAnalysis[] { const correlations: CorrelationAnalysis[] = []; // Simple correlation analysis between different metrics const salesByDay = new Map<string, number>(); const leadsByDay = new Map<string, number>(); leadsData.forEach(lead => { const date = new Date(lead.created_at * 1000).toDateString(); leadsByDay.set(date, (leadsByDay.get(date) || 0) + 1); if (lead.status_id === 142) { // Assuming 142 is "Won" status salesByDay.set(date, (salesByDay.get(date) || 0) + 1); } }); // Calculate correlation between leads and sales const leadValues = Array.from(leadsByDay.values()); const salesValues = Array.from(salesByDay.values()); if (leadValues.length > 5 && salesValues.length > 5) { const correlation = calculateCorrelation(leadValues, salesValues); if (Math.abs(correlation) > 0.5) { correlations.push({ metric1: 'leads_count', metric2: 'sales_count', correlation: correlation, significance: Math.abs(correlation), insight: correlation > 0 ? 'Mais leads resultam em mais vendas' : 'Menos leads resultam em menos vendas', recommendation: correlation > 0 ? 'Foque em gerar mais leads' : 'Investigue qualidade dos leads' }); } } return correlations; } function calculateCorrelation(x: number[], y: number[]): number { const n = Math.min(x.length, y.length); if (n === 0) return 0; const sumX = x.slice(0, n).reduce((a, b) => a + b, 0); const sumY = y.slice(0, n).reduce((a, b) => a + b, 0); const sumXY = x.slice(0, n).reduce((sum, xi, i) => sum + xi * y[i], 0); const sumXX = x.slice(0, n).reduce((sum, xi) => sum + xi * xi, 0); const sumYY = y.slice(0, n).reduce((sum, yi) => sum + yi * yi, 0); const numerator = n * sumXY - sumX * sumY; const denominator = Math.sqrt((n * sumXX - sumX * sumX) * (n * sumYY - sumY * sumY)); return denominator === 0 ? 0 : numerator / denominator; } function generateAutoInsights(leadsData: any[], analysis: SemanticAnalysis): AutoInsight[] { const insights: AutoInsight[] = []; // Analyze conversion rate const totalLeads = leadsData.length; const wonLeads = leadsData.filter(lead => lead.status_id === 142).length; const conversionRate = totalLeads > 0 ? (wonLeads / totalLeads) * 100 : 0; if (conversionRate < 5) { insights.push({ type: 'warning', title: 'Taxa de Conversão Baixa', description: `Taxa de conversão de apenas ${conversionRate.toFixed(1)}%`, impact: 'high', actionRequired: true, suggestedActions: [ 'Revisar processo de qualificação de leads', 'Analisar qualidade dos leads gerados', 'Implementar follow-up mais agressivo' ] }); } // Analyze lead velocity const recentLeads = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 7); return createdAt >= weekAgo; }); if (recentLeads.length > totalLeads * 0.3) { insights.push({ type: 'opportunity', title: 'Alto Volume de Leads Recentes', description: `${recentLeads.length} leads criados na última semana`, impact: 'medium', actionRequired: false, suggestedActions: [ 'Acelerar processo de qualificação', 'Aumentar capacidade de atendimento', 'Implementar automação de follow-up' ] }); } // Analyze category performance const categoryPerformance = new Map<string, number>(); leadsData.forEach(lead => { const category = getCategoryFromQuestion(lead.name || ''); if (category) { categoryPerformance.set(category, (categoryPerformance.get(category) || 0) + 1); } }); const topCategory = Array.from(categoryPerformance.entries()) .sort((a, b) => b[1] - a[1])[0]; if (topCategory && topCategory[1] > totalLeads * 0.4) { insights.push({ type: 'trend', title: 'Categoria Dominante', description: `${topCategory[0]} representa ${((topCategory[1] / totalLeads) * 100).toFixed(1)}% dos leads`, impact: 'medium', actionRequired: false, suggestedActions: [ `Investir mais em ${topCategory[0]}`, 'Diversificar fontes de leads', 'Analisar ROI por categoria' ] }); } return insights; } function predictSales(leadsData: any[], period: string): SalesForecast { // Simple prediction based on historical trends const historicalData = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); const { start, end } = getDateRange(period); const historicalStart = new Date(start); const historicalEnd = new Date(end); historicalStart.setDate(historicalStart.getDate() - 30); historicalEnd.setDate(historicalEnd.getDate() - 30); return createdAt >= historicalStart && createdAt <= historicalEnd; }); const currentData = leadsData.filter(lead => { const createdAt = new Date(lead.created_at * 1000); const { start, end } = getDateRange(period); return createdAt >= start && createdAt <= end; }); const historicalSales = historicalData.filter(lead => lead.status_id === 142).length; const currentSales = currentData.filter(lead => lead.status_id === 142).length; const growthRate = historicalSales > 0 ? (currentSales - historicalSales) / historicalSales : 0; const predictedSales = Math.round(currentSales * (1 + growthRate)); return { period: period, predictedSales: predictedSales, confidence: Math.min(Math.abs(growthRate) + 0.5, 1.0), factors: ['tendência histórica', 'crescimento atual', 'sazonalidade'], trend: growthRate > 0.1 ? 'increasing' : growthRate < -0.1 ? 'decreasing' : 'stable' }; } function isCacheValid(): boolean { return Date.now() < leadsCache.expiresAt && leadsCache.data.length > 0; } function setCacheData(data: any[]): void { leadsCache.data = data; leadsCache.timestamp = Date.now(); leadsCache.expiresAt = Date.now() + CACHE_DURATION; logger.info(`💾 Cache atualizado com ${data.length} leads (expira em ${new Date(leadsCache.expiresAt).toLocaleTimeString()})`); } function getCacheData(): any[] { logger.info(`📦 Cache hit: ${leadsCache.data.length} leads (${Math.round((leadsCache.expiresAt - Date.now()) / 1000)}s restantes)`); return leadsCache.data; } // Helper function to get date range for temporal filtering function getDateRange(period: string): { start: Date; end: Date } { const now = new Date(); const start = new Date(); const end = new Date(); switch (period) { case 'today': start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); break; case 'yesterday': start.setDate(now.getDate() - 1); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 1); end.setHours(23, 59, 59, 999); break; case 'week': start.setDate(now.getDate() - 7); start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); break; case 'last_week': // Last week: 7 days ago to 14 days ago start.setDate(now.getDate() - 14); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 8); end.setHours(23, 59, 59, 999); break; case 'month': start.setDate(now.getDate() - 30); start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); break; case 'last_month': // Last month: previous calendar month const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); start.setFullYear(lastMonth.getFullYear(), lastMonth.getMonth(), 1); start.setHours(0, 0, 0, 0); end.setFullYear(lastMonth.getFullYear(), lastMonth.getMonth() + 1, 0); end.setHours(23, 59, 59, 999); break; case 'year': start.setFullYear(currentYear, 0, 1); start.setHours(0, 0, 0, 0); end.setFullYear(currentYear, 11, 31); end.setHours(23, 59, 59, 999); break; case 'last_year': // Last year: previous calendar year start.setFullYear(currentYear - 1, 0, 1); start.setHours(0, 0, 0, 0); end.setFullYear(currentYear - 1, 11, 31); end.setHours(23, 59, 59, 999); break; default: start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); } return { start, end }; } // Helper function to detect category from question function getCategoryFromQuestion(question: string): string | null { const questionLower = question.toLowerCase(); const categories: { [key: string]: string[] } = { 'tráfego': ['trafego', 'tráfego', 'traffic', 'ads', 'anúncios', 'facebook', 'google', 'instagram'], 'design': ['design', 'logo', 'identidade', 'visual', 'criativo'], 'marketing': ['marketing', 'digital', 'social', 'redes sociais', 'conteúdo'], 'suporte': ['suporte', 'atendimento', 'help', 'ajuda', 'técnico'], 'contatos': ['contato', 'contatos', 'telefone', 'telefones', 'nome', 'nomes', 'cliente', 'clientes'], 'status': ['status', 'estado', 'situação', 'situacao', 'andamento', 'fechado', 'perdido', 'ganho'], 'valores': ['valor', 'valores', 'preço', 'preco', 'ticket', 'faturamento', 'receita'], 'origem': ['origem', 'fonte', 'canal', 'utm', 'facebook', 'google', 'instagram'], 'produtos': ['produto', 'produtos', 'item', 'items', 'serviço', 'servico', 't-shirt', 'camiseta'] }; for (const [category, keywords] of Object.entries(categories)) { if (keywords.some(keyword => questionLower.includes(keyword))) { return category; } } return null; } // Helper function to detect month from question function getMonthFromQuestion(question: string): number | null { const questionLower = question.toLowerCase(); const months: { [key: string]: number } = { 'janeiro': 0, 'jan': 0, 'fevereiro': 1, 'fev': 1, 'março': 2, 'mar': 2, 'abril': 3, 'abr': 3, 'maio': 4, 'mai': 4, 'junho': 5, 'jun': 5, 'julho': 6, 'jul': 6, 'agosto': 7, 'ago': 7, 'setembro': 8, 'set': 8, 'outubro': 9, 'out': 9, 'novembro': 10, 'nov': 10, 'dezembro': 11, 'dez': 11 }; for (const [monthName, monthNumber] of Object.entries(months)) { if (questionLower.includes(monthName)) { return monthNumber; } } return null; } // MCP endpoint app.post('/mcp', async (req, res) => { logger.info('🚀 Iniciando servidor MCP Kommo', { baseUrl: process.env.KOMMO_BASE_URL, environment: process.env.NODE_ENV }); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); try { const { method, params, id } = req.body; logger.debug('📨 Requisição MCP recebida', { method, params, id }); if (method === 'tools/list') { const response = { jsonrpc: '2.0', id, result: { tools: [ { name: 'get_leads', description: 'Obter lista de leads do Kommo CRM', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Número máximo de leads (padrão: 1000)' }, page: { type: 'number', description: 'Página para paginação (padrão: 1)' } } } }, { name: 'create_lead', description: 'Criar um novo lead no Kommo CRM', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Nome do lead' }, price: { type: 'number', description: 'Valor do lead' }, status_id: { type: 'number', description: 'ID do status' } }, required: ['name'] } }, { name: 'get_sales_report', description: 'Obter relatório de vendas do Kommo CRM', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Número máximo de leads (padrão: 1000)' }, page: { type: 'number', description: 'Página para paginação (padrão: 1)' } } } }, { name: 'get_contacts', description: 'Obter lista de contatos do Kommo CRM', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Número máximo de contatos (padrão: 1000)' }, page: { type: 'number', description: 'Página para paginação (padrão: 1)' } } } }, { name: 'get_companies', description: 'Obter lista de empresas do Kommo CRM', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Número máximo de empresas (padrão: 1000)' }, page: { type: 'number', description: 'Página para paginação (padrão: 1)' } } } }, { name: 'get_tasks', description: 'Obter lista de tarefas do Kommo CRM', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Número máximo de tarefas (padrão: 1000)' }, page: { type: 'number', description: 'Página para paginação (padrão: 1)' } } } }, { name: 'ask_kommo', description: 'Fazer perguntas inteligentes sobre dados do Kommo CRM usando IA conversacional', inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'Pergunta sobre dados do Kommo CRM' } }, required: ['question'] } } ] } }; res.write(`data: ${JSON.stringify(response)}\n\n`); res.end(); } else if (method === 'tools/call') { const { name, arguments: args } = params; logger.debug('🔧 Executando ferramenta', { name, args }); let result: any; try { switch (name) { case 'get_leads': const leadsLimit = args?.limit || 1000; const leadsPage = args?.page || 1; const leadsData = await kommoAPI.getLeads({ limit: leadsLimit, page: leadsPage }); result = { content: [ { type: 'text', text: JSON.stringify(leadsData, null, 2) } ] }; break; case 'create_lead': const leadData = await kommoAPI.createLead(args); result = { content: [ { type: 'text', text: JSON.stringify(leadData, null, 2) } ] }; break; case 'get_sales_report': const salesLimit = args?.limit || 1000; const salesPage = args?.page || 1; const dateFrom = args?.dateFrom || '2024-01-01'; const dateTo = args?.dateTo || '2024-12-31'; const salesData = await kommoAPI.getSalesReport(dateFrom, dateTo); result = { content: [ { type: 'text', text: JSON.stringify(salesData, null, 2) } ] }; break; case 'ask_kommo': const question = args?.question || ''; const startTime = Date.now(); logger.debug('🤖 Processando pergunta inteligente', { question }); // Get leads data with cache optimization let leadsArray: any[]; let isCacheHit = false; if (isCacheValid()) { leadsArray = getCacheData(); isCacheHit = true; } else { logger.info('📊 Cache expirado, buscando leads com paginação...'); leadsArray = await kommoAPI.getAllLeads(); setCacheData(leadsArray); isCacheHit = false; } const questionLower = question.toLowerCase(); let response = ''; const insights: string[] = []; const suggestions: string[] = []; // AI Semantic Analysis const semanticAnalysis = analyzeSemantics(question); logger.info(`🧠 Análise semântica: Intent=${semanticAnalysis.intent}, Entities=${semanticAnalysis.entities.length}, Confidence=${semanticAnalysis.confidence}`); // Detect temporal context with improved relative period detection let temporalFilter = null; if (questionLower.includes('hoje')) temporalFilter = 'today'; else if (questionLower.includes('ontem')) temporalFilter = 'yesterday'; else if (questionLower.includes('semana passada') || questionLower.includes('semana anterior')) temporalFilter = 'last_week'; else if (questionLower.includes('mês passado') || questionLower.includes('mes passado') || questionLower.includes('mês anterior') || questionLower.includes('mes anterior')) temporalFilter = 'last_month'; else if (questionLower.includes('ano passado') || questionLower.includes('ano anterior')) temporalFilter = 'last_year'; else if (questionLower.includes('esta semana') || questionLower.includes('esta semana')) temporalFilter = 'week'; else if (questionLower.includes('este mês') || questionLower.includes('este mes')) temporalFilter = 'month'; else if (questionLower.includes('este ano')) temporalFilter = 'year'; else if (questionLower.includes('semana')) temporalFilter = 'week'; else if (questionLower.includes('mês') || questionLower.includes('mes')) temporalFilter = 'month'; else if (questionLower.includes('ano')) temporalFilter = 'year'; // Detect category const category = getCategoryFromQuestion(question); // Detect month const month = getMonthFromQuestion(question); // Sales analysis if (questionLower.includes('venda') || questionLower.includes('vendas') || questionLower.includes('fechado') || questionLower.includes('ganho')) { let salesLeads = leadsArray.filter((lead: any) => { const status = lead.status?.toString().toLowerCase() || ''; const isClosedStatus = status.includes('fechado') || status.includes('ganho') || status.includes('concluído') || status.includes('vendido'); const hasValue = (lead.price || 0) > 0; return isClosedStatus || hasValue; }); // Apply temporal filter if (temporalFilter) { const { start, end } = getDateRange(temporalFilter); salesLeads = salesLeads.filter((lead: any) => { const updatedAt = new Date(lead.updated_at * 1000); return updatedAt >= start && updatedAt <= end; }); } // Apply month filter if (month !== null) { salesLeads = salesLeads.filter((lead: any) => { const updatedAt = new Date(lead.updated_at * 1000); return updatedAt.getMonth() === month && updatedAt.getFullYear() === currentYear; }); } // Apply category filter if (category) { salesLeads = salesLeads.filter((lead: any) => { const leadName = (lead.name || '').toLowerCase(); const leadStatus = (lead.status?.toString() || '').toLowerCase(); return leadName.includes(category) || leadStatus.includes(category); }); } const totalSales = salesLeads.length; const totalValue = salesLeads.reduce((sum: number, lead: any) => sum + (lead.price || 0), 0); const averageTicket = totalSales > 0 ? totalValue / totalSales : 0; response = `💰 **Análise de Vendas**\n\n`; response += `📊 **Total de vendas:** ${totalSales}\n`; response += `💵 **Valor total:** R$ ${totalValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}\n`; response += `📈 **Ticket médio:** R$ ${averageTicket.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}\n`; if (temporalFilter) { const periodNames: { [key: string]: string } = { 'today': 'Hoje', 'yesterday': 'Ontem', 'week': 'Esta semana', 'last_week': 'Semana passada', 'month': 'Este mês', 'last_month': 'Mês passado', 'year': 'Este ano', 'last_year': 'Ano passado' }; response += `\n⏰ **Período:** ${periodNames[temporalFilter] || temporalFilter}\n`; } if (month !== null) { const monthNames = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; response += `\n📅 **Mês:** ${monthNames[month]} de ${currentYear}\n`; } if (category) { response += `\n🏷️ **Categoria:** ${category}\n`; } if (totalSales > 0) { insights.push(`🎯 ${totalSales} vendas identificadas`); insights.push(`💎 Ticket médio de R$ ${averageTicket.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`); // Top status analysis const statusCounts: { [key: string]: number } = {}; salesLeads.forEach((lead: any) => { const status = lead.status?.toString() || 'Sem status'; statusCounts[status] = (statusCounts[status] || 0) + 1; }); const topStatuses = Object.entries(statusCounts) .sort(([,a], [,b]) => b - a) .slice(0, 3); if (topStatuses.length > 0) { insights.push(`🏆 Top status: ${topStatuses.map(([status, count]) => `${status} (${count})`).join(', ')}`); } suggestions.push('Pergunte sobre detalhes de contatos das vendas'); suggestions.push('Analise conversão de leads para vendas'); suggestions.push('Compare com períodos anteriores'); } else { insights.push('📭 Nenhuma venda encontrada no período'); suggestions.push('Verifique outros períodos ou categorias'); suggestions.push('Analise leads em andamento'); } } // Contact analysis else if (category === 'contatos' || questionLower.includes('contato') || questionLower.includes('telefone') || questionLower.includes('nome') || questionLower.includes('cliente')) { let filteredLeads = leadsArray; // Apply temporal filter if (temporalFilter) { const { start, end } = getDateRange(temporalFilter); filteredLeads = filteredLeads.filter((lead: any) => { const createdAt = new Date(lead.created_at * 1000); return createdAt >= start && createdAt <= end; }); } // Apply month filter if (month !== null) { filteredLeads = filteredLeads.filter((lead: any) => { const createdAt = new Date(lead.created_at * 1000); return createdAt.getMonth() === month && createdAt.getFullYear() === currentYear; }); } // Get contacts for leads const contacts = await kommoAPI.getContacts(); const contactsArray = contacts._embedded?.contacts || []; // Correlate leads with contacts const leadsWithContacts = filteredLeads.map((lead: any) => { const relatedContact = contactsArray.find((contact: any) => contact.name && lead.name && contact.name.toLowerCase().includes(lead.name.toLowerCase().split(' ')[0]) ); return { ...lead, contact: relatedContact || null }; }); const totalLeads = leadsWithContacts.length; const leadsWithContactInfo = leadsWithContacts.filter((lead: any) => lead.contact); const contactRate = totalLeads > 0 ? (leadsWithContactInfo.length / totalLeads * 100) : 0; response = `📞 **Análise de Contatos**\n\n`; response += `📊 **Total de leads:** ${totalLeads}\n`; response += `👥 **Leads com contatos:** ${leadsWithContactInfo.length}\n`; response += `📈 **Taxa de contatos:** ${contactRate.toFixed(1)}%\n\n`; if (temporalFilter) { const periodNames: { [key: string]: string } = { 'today': 'Hoje', 'yesterday': 'Ontem', 'week': 'Esta semana', 'last_week': 'Semana passada', 'month': 'Este mês', 'last_month': 'Mês passado', 'year': 'Este ano', 'last_year': 'Ano passado' }; response += `⏰ **Período:** ${periodNames[temporalFilter] || temporalFilter}\n`; } if (month !== null) { const monthNames = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; response += `\n📅 **Mês:** ${monthNames[month]} de ${currentYear}\n`; } if (leadsWithContactInfo.length > 0) { response += `\n📋 **Exemplos de Contatos:**\n`; leadsWithContactInfo.slice(0, 5).forEach((lead: any, index: number) => { const contact = lead.contact; const phone = contact.phone?.[0]?.value || 'N/A'; response += `${index + 1}. **${contact.name}** - ${phone}\n`; }); } insights.push(`📞 Taxa de contatos: ${contactRate.toFixed(1)}%`); insights.push(`👥 ${leadsWithContactInfo.length} leads com informações de contato`); suggestions.push('Pergunte sobre detalhes específicos de contatos'); suggestions.push('Analise leads sem informações de contato'); } // Lead analysis else if (questionLower.includes('lead') || questionLower.includes('leads')) { let filteredLeads = leadsArray; // Apply temporal filter if (temporalFilter) { const { start, end } = getDateRange(temporalFilter); filteredLeads = filteredLeads.filter((lead: any) => { const createdAt = new Date(lead.created_at * 1000); return createdAt >= start && createdAt <= end; }); } // Apply month filter if (month !== null) { filteredLeads = filteredLeads.filter((lead: any) => { const createdAt = new Date(lead.created_at * 1000); return createdAt.getMonth() === month && createdAt.getFullYear() === currentYear; }); } // Apply category filter if (category) { filteredLeads = filteredLeads.filter((lead: any) => { const leadName = (lead.name || '').toLowerCase(); const leadStatus = (lead.status?.toString() || '').toLowerCase(); return leadName.includes(category) || leadStatus.includes(category); }); } const totalLeads = filteredLeads.length; const totalValue = filteredLeads.reduce((sum: number, lead: any) => sum + (lead.price || 0), 0); const averageValue = totalLeads > 0 ? totalValue / totalLeads : 0; response = `📋 **Análise de Leads**\n\n`; response += `📊 **Total de leads:** ${totalLeads}\n`; response += `💵 **Valor total:** R$ ${totalValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}\n`; response += `📈 **Valor médio:** R$ ${averageValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}\n`; if (temporalFilter) { const periodNames: { [key: string]: string } = { 'today': 'Hoje', 'yesterday': 'Ontem', 'week': 'Esta semana', 'last_week': 'Semana passada', 'month': 'Este mês', 'last_month': 'Mês passado', 'year': 'Este ano', 'last_year': 'Ano passado' }; response += `\n⏰ **Período:** ${periodNames[temporalFilter] || temporalFilter}\n`; } if (month !== null) { const monthNames = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; response += `\n📅 **Mês:** ${monthNames[month]} de ${currentYear}\n`; } if (category) { response += `\n🏷️ **Categoria:** ${category}\n`; } if (totalLeads > 0) { insights.push(`📈 ${totalLeads} leads identificados`); insights.push(`💰 Valor total de R$ ${totalValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`); // Status analysis const statusCounts: { [key: string]: number } = {}; filteredLeads.forEach((lead: any) => { const status = lead.status?.toString() || 'Sem status'; statusCounts[status] = (statusCounts[status] || 0) + 1; }); const topStatuses = Object.entries(statusCounts) .sort(([,a], [,b]) => b - a) .slice(0, 3); if (topStatuses.length > 0) { insights.push(`🏆 Top status: ${topStatuses.map(([status, count]) => `${status} (${count})`).join(', ')}`); } suggestions.push('Pergunte sobre vendas de hoje ou ontem'); suggestions.push('Analise conversão de leads'); suggestions.push('Compare períodos diferentes'); } } // Help/commands else if (questionLower.includes('ajuda') || questionLower.includes('help') || questionLower.includes('comandos')) { response = `🤖 **Como posso ajudar você:**\n\n`; insights.push('📊 **Análises:** Pergunte sobre leads criados hoje, ontem, este mês'); insights.push('💰 **Financeiro:** Solicite valores totais, médias, faturamento'); insights.push('🔄 **Pipelines:** Analise funis de vendas e conversões'); insights.push('📈 **Performance:** Relatórios e métricas de performance'); insights.push('🎯 **Insights:** Identifique padrões e oportunidades'); insights.push('💼 **Vendas:** Analise vendas, conversões e fechamentos'); suggestions.push('"Quantos leads foram criados hoje?"'); suggestions.push('"Quantas vendas tivemos hoje?"'); suggestions.push('"Qual o valor total dos leads?"'); suggestions.push('"Mostre análise de performance"'); } // Default response else { const totalLeads = leadsArray.length; const totalValue = leadsArray.reduce((sum: number, lead: any) => sum + (lead.price || 0), 0); response = `🤔 Entendi sua mensagem: "${question}"\n\n`; response += `📊 **Resumo geral:**\n`; response += `• Total de leads: ${totalLeads}\n`; response += `• Valor total: R$ ${totalValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}\n\n`; response += `💡 **Sugestões de perguntas:**\n`; response += `• "Quantos leads foram criados hoje?"\n`; response += `• "Quantas vendas tivemos hoje?"\n`; response += `• "Qual o valor total dos leads?"\n`; response += `• "Mostre análise de performance"`; insights.push(`📈 ${totalLeads} leads no total`); insights.push(`💰 Valor total de R$ ${totalValue.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`); suggestions.push('Pergunte sobre vendas específicas'); suggestions.push('Analise leads por período'); suggestions.push('Verifique conversões'); } // Add insights and suggestions to response if (insights.length > 0) { response += `\n\n🔍 **Insights:**\n`; insights.forEach(insight => response += `• ${insight}\n`); } // Add AI-powered smart suggestions const smartSuggestions = generateSmartSuggestions(semanticAnalysis, leadsArray); if (smartSuggestions.length > 0) { response += `\n\n🤖 **Sugestões Inteligentes:**\n`; smartSuggestions.forEach(suggestion => { const icon = suggestion.type === 'question' ? '❓' : suggestion.type === 'insight' ? '💡' : '🎯'; response += `${icon} ${suggestion.content}\n`; }); } if (suggestions.length > 0) { response += `\n\n💡 **Sugestões:**\n`; suggestions.forEach(suggestion => response += `• ${suggestion}\n`); } // Add trend analysis if temporal filter is present if (temporalFilter) { const trends = analyzeTrends(leadsArray, temporalFilter); if (trends.length > 0) { response += `\n\n📈 **Análise de Tendências:**\n`; trends.forEach(trend => { const trendIcon = trend.trend === 'up' ? '📈' : trend.trend === 'down' ? '📉' : '➡️'; const significanceIcon = trend.significance === 'high' ? '🔴' : trend.significance === 'medium' ? '🟡' : '🟢'; response += `${trendIcon} ${trend.metric}: ${trend.trend} ${trend.percentage.toFixed(1)}% ${significanceIcon}\n`; }); } } // Add advanced AI insights const autoInsights = generateAutoInsights(leadsArray, semanticAnalysis); if (autoInsights.length > 0) { response += `\n\n🧠 **Insights Automáticos:**\n`; autoInsights.forEach(insight => { const icon = insight.type === 'opportunity' ? '🎯' : insight.type === 'warning' ? '⚠️' : insight.type === 'trend' ? '📊' : '🔍'; const impactIcon = insight.impact === 'high' ? '🔴' : insight.impact === 'medium' ? '🟡' : '🟢'; response += `${icon} **${insight.title}** ${impactIcon}\n`; response += ` ${insight.description}\n`; if (insight.actionRequired) { response += ` 🎯 Ação necessária: ${insight.suggestedActions[0]}\n`; } }); } // Add anomaly detection if (temporalFilter) { const anomalies = detectAnomalies(leadsArray, temporalFilter); if (anomalies.length > 0) { response += `\n\n🚨 **Detecção de Anomalias:**\n`; anomalies.forEach(anomaly => { const severityIcon = anomaly.severity === 'critical' ? '🔴' : anomaly.severity === 'high' ? '🟠' : anomaly.severity === 'medium' ? '🟡' : '🟢'; response += `${severityIcon} **${anomaly.type.toUpperCase()}** - ${anomaly.description}\n`; response += ` 💡 ${anomaly.suggestedAction}\n`; }); } } // Add correlation analysis const correlations = findCorrelations(leadsArray); if (correlations.length > 0) { response += `\n\n🔗 **Análise de Correlações:**\n`; correlations.forEach(correlation => { const correlationIcon = correlation.correlation > 0 ? '📈' : '📉'; response += `${correlationIcon} ${correlation.metric1} ↔ ${correlation.metric2}: ${correlation.correlation.toFixed(2)}\n`; response += ` 💡 ${correlation.insight}\n`; response += ` 🎯 ${correlation.recommendation}\n`; }); } // Add sales prediction if (temporalFilter && semanticAnalysis.intent === 'analysis_query') { const forecast = predictSales(leadsArray, temporalFilter); response += `\n\n🔮 **Previsão de Vendas:**\n`; response += `📊 Próximo período: ~${forecast.predictedSales} vendas\n`; response += `📈 Tendência: ${forecast.trend === 'increasing' ? 'Crescimento' : forecast.trend === 'decreasing' ? 'Queda' : 'Estável'}\n`; response += `🎯 Confiança: ${(forecast.confidence * 100).toFixed(0)}%\n`; response += `📋 Fatores: ${forecast.factors.join(', ')}\n`; } // Add performance info const cacheStatus = isCacheValid() ? '✅ Cache ativo' : '🔄 Dados atualizados'; response += `\n\n⚡ **Performance:** ${cacheStatus}`; // Validate data consistency const expectedLeadsCount = 13928; // Expected total leads const actualLeadsCount = leadsArray.length; const isDataConsistent = Math.abs(actualLeadsCount - expectedLeadsCount) <= 1; if (!isDataConsistent) { logger.info(`⚠️ Inconsistência detectada: ${actualLeadsCount} leads vs ${expectedLeadsCount} esperados`); } // Update performance metrics const responseTime = (Date.now() - startTime) / 1000; updatePerformanceMetrics(isCacheHit, responseTime); const finalResponse = { response: response, metadata: { total_leads_analyzed: actualLeadsCount, temporal_filter: temporalFilter, category_filter: category, month_filter: month, current_year: currentYear, data_consistency: isDataConsistent, cache_hit: isCacheHit, response_time_seconds: responseTime, ai_analysis: { intent: semanticAnalysis.intent, entities: semanticAnalysis.entities, confidence: semanticAnalysis.confidence, smart_suggestions_count: smartSuggestions.length, trend_analysis: temporalFilter ? analyzeTrends(leadsArray, temporalFilter) : null, auto_insights: autoInsights, anomalies_detected: temporalFilter ? detectAnomalies(leadsArray, temporalFilter) : [], correlations_found: correlations, sales_forecast: temporalFilter && semanticAnalysis.intent === 'analysis_query' ? predictSales(leadsArray, temporalFilter) : null }, performance_metrics: { total_requests: performanceMetrics.totalRequests, cache_hit_rate: performanceMetrics.totalRequests > 0 ? (performanceMetrics.cacheHits / performanceMetrics.totalRequests * 100).toFixed(1) + '%' : '0%', average_response_time: performanceMetrics.averageResponseTime.toFixed(2) + 's' }, timestamp: new Date().toISOString() } }; result = { content: [ { type: 'text', text: JSON.stringify({ agent_response: finalResponse, user_message: question, metadata: { total_leads_analyzed: leadsArray.length, response_type: 'conversational_ai', timestamp: new Date().toISOString() } }, null, 2) } ] }; break; case 'get_contacts': const contactsLimit = args?.limit || 1000; const contactsPage = args?.page || 1; const contactsData = await kommoAPI.getContacts({ limit: contactsLimit, page: contactsPage }); result = { content: [ { type: 'text', text: JSON.stringify(contactsData, null, 2) } ] }; break; case 'get_companies': const companiesLimit = args?.limit || 1000; const companiesPage = args?.page || 1; const companiesData = await kommoAPI.getCompanies({ limit: companiesLimit, page: companiesPage }); result = { content: [ { type: 'text', text: JSON.stringify(companiesData, null, 2) } ] }; break; case 'get_tasks': const tasksLimit = args?.limit || 1000; const tasksPage = args?.page || 1; const tasksData = await kommoAPI.getTasks({ limit: tasksLimit, page: tasksPage }); result = { content: [ { type: 'text', text: JSON.stringify(tasksData, null, 2) } ] }; break; default: throw new Error(`Unknown tool: ${name}`); } const response = { jsonrpc: '2.0', id, result }; res.write(`data: ${JSON.stringify(response)}\n\n`); res.end(); } catch (error) { logger.error(`❌ Erro ao executar ferramenta ${name}`, error); const errorResponse = { jsonrpc: '2.0', id, error: { code: -32603, message: error instanceof Error ? error.message : 'Internal error' } }; res.write(`data: ${JSON.stringify(errorResponse)}\n\n`); res.end(); } } else { const errorResponse = { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }; res.write(`data: ${JSON.stringify(errorResponse)}\n\n`); res.end(); } } catch (error) { logger.error('❌ Erro no endpoint MCP', error); const errorResponse = { jsonrpc: '2.0', id: req.body?.id || 1, error: { code: -32603, message: 'Internal error' } }; res.write(`data: ${JSON.stringify(errorResponse)}\n\n`); res.end(); } }); // CORS preflight app.options('/mcp', (req, res) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type'); res.sendStatus(200); }); // Start server app.listen(PORT, () => { logger.info(`🚀 Servidor MCP Kommo rodando na porta ${PORT}`, { environment: process.env.NODE_ENV || 'development', kommo_base_url: process.env.KOMMO_BASE_URL || 'https://api-g.kommo.com', current_year: currentYear }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Miguelgbastos/Kommo-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server