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 (detection/sending/campaigns), and robust asynchronous agent execution h

377 lines (376 loc) 13.9 kB
#!/usr/bin/env node /** * HTTP Wrapper for IRISeller MCP Server * Provides HTTP endpoints for MCP functionality to enable web-based access */ import express from 'express'; import cors from 'cors'; import jwt from 'jsonwebtoken'; import { IRISellerAPIService } from './services/iriseller-api.js'; import { ToolHandlers } from './handlers/tool-handlers.js'; import { ALL_TOOLS } from './tools/index.js'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); // Generate user token if credentials provided async function generateUserToken(email, password, jwtSecret) { if (!email || !password || !jwtSecret) { return undefined; } try { // Create a system JWT token that bypasses database lookup // The backend auth middleware supports system tokens for internal operations const payload = { email, userId: `mcp-user-${email}`, // Unique system user ID id: `mcp-user-${email}`, // Alternative field that backend accepts role: 'user', // Default role isSystemToken: true, // Flag to indicate this is a system token sub: email, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (4 * 60 * 60) // 4 hours (will be automatically refreshed) }; return jwt.sign(payload, jwtSecret); } catch (error) { console.warn('[MCP-HTTP] Failed to generate user token:', error); return undefined; } } // Configuration const config = { name: process.env.MCP_SERVER_NAME || 'iriseller-mcp-server', version: process.env.MCP_SERVER_VERSION || '1.0.0', iriseller_api_url: process.env.IRISELLER_API_URL || 'http://localhost:3001', crewai_api_url: process.env.CREWAI_API_URL || 'http://localhost:8001', crm_connect_api_url: process.env.CRM_CONNECT_API_URL || 'http://localhost:3001/api/crm-connect', api_key: process.env.IRISELLER_API_KEY, jwt_secret: process.env.JWT_SECRET, rate_limit: { requests_per_minute: parseInt(process.env.RATE_LIMIT_REQUESTS_PER_MINUTE || '100'), burst: parseInt(process.env.RATE_LIMIT_BURST || '20') }, cache: { ttl_seconds: parseInt(process.env.CACHE_TTL_SECONDS || '300'), enabled: process.env.ENABLE_CACHE !== 'false' }, debug: process.env.DEBUG === 'true' }; const PORT = parseInt(process.env.MCP_HTTP_PORT || '3002'); // Initialize services const apiService = new IRISellerAPIService(config); const toolHandlers = new ToolHandlers(apiService); // Generate and set user token for authenticated operations let globalUserToken; async function initializeAuthentication() { // Use environment variables for credentials const defaultEmail = process.env.MCP_DEFAULT_USER_EMAIL; const defaultPassword = process.env.MCP_DEFAULT_USER_PASSWORD; if (!defaultEmail || !defaultPassword) { console.warn(`[MCP-HTTP] Warning: No default credentials configured. Set MCP_DEFAULT_USER_EMAIL and MCP_DEFAULT_USER_PASSWORD environment variables.`); return; } // Try to generate user token globalUserToken = await generateUserToken(defaultEmail, defaultPassword, config.jwt_secret); if (globalUserToken) { toolHandlers.setUserToken(globalUserToken); console.log(`[MCP-HTTP] Generated user token for ${defaultEmail}`); console.log('[MCP-HTTP] User token configured for authenticated operations'); } else { console.warn('[MCP-HTTP] No user token generated - some tools may not work without authentication'); } } // Create Express app const app = express(); // Middleware app.use(cors({ origin: ['https://beta.iriseller.com', 'http://localhost:3000', 'http://localhost:5176'], credentials: true })); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); // Request logging middleware app.use((req, res, next) => { if (config.debug) { console.log(`[MCP-HTTP] ${req.method} ${req.path}`, req.body ? JSON.stringify(req.body).substring(0, 200) : ''); } next(); }); // Health check endpoint app.get('/health', async (req, res) => { try { const health = await apiService.checkHealth(); const overallHealth = health.backend && health.crewai; res.status(overallHealth ? 200 : 503).json({ status: overallHealth ? 'healthy' : 'degraded', services: health, timestamp: new Date().toISOString(), version: config.version }); } catch (error) { res.status(500).json({ status: 'error', error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date().toISOString() }); } }); // List available tools app.get('/tools', (req, res) => { res.json({ tools: ALL_TOOLS, count: ALL_TOOLS.length, server: { name: config.name, version: config.version }, timestamp: new Date().toISOString() }); }); // Execute a tool app.post('/tools/:toolName', async (req, res) => { const { toolName } = req.params; const { arguments: toolArgs = {} } = req.body; try { // Use the global user token or extract from Authorization header const authHeader = req.headers.authorization; let userToken = globalUserToken; if (authHeader && authHeader.startsWith('Bearer ')) { // If client provides a token, use that instead userToken = authHeader.split(' ')[1]; console.log(`[MCP-HTTP] Tool ${toolName} - Using client-provided token`); } else { console.log(`[MCP-HTTP] Tool ${toolName} - Using global user token:`, userToken ? 'Present' : 'Missing'); } // Temporarily set the user token for this request if (userToken) { toolHandlers.setUserToken(userToken); } // Create MCP-style request const mcpRequest = { params: { name: toolName, arguments: toolArgs }, method: 'tools/call' }; const result = await toolHandlers.handleToolCall(mcpRequest); res.json({ success: true, tool: toolName, result: result, timestamp: new Date().toISOString() }); } catch (error) { console.error(`[MCP-HTTP] Tool execution error for ${toolName}:`, error); res.status(400).json({ success: false, tool: toolName, error: { code: error.code || 'EXECUTION_ERROR', message: error.message || 'Tool execution failed' }, timestamp: new Date().toISOString() }); } }); // Batch tool execution app.post('/tools/batch', async (req, res) => { const { tools = [] } = req.body; if (!Array.isArray(tools) || tools.length === 0) { return res.status(400).json({ success: false, error: 'Invalid request: tools array is required and must not be empty', timestamp: new Date().toISOString() }); } try { const results = []; for (const tool of tools) { try { const mcpRequest = { params: { name: tool.name, arguments: tool.arguments || {} }, method: 'tools/call' }; const result = await toolHandlers.handleToolCall(mcpRequest); results.push({ success: true, tool: tool.name, result: result }); } catch (error) { results.push({ success: false, tool: tool.name, error: { code: error.code || 'EXECUTION_ERROR', message: error.message || 'Tool execution failed' } }); } } res.json({ success: true, results: results, count: results.length, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ success: false, error: error.message || 'Batch execution failed', timestamp: new Date().toISOString() }); } }); // Documentation endpoint app.get('/docs', (req, res) => { const baseUrl = `http://${req.get('host')}`; res.json({ name: config.name, version: config.version, description: 'IRISeller MCP Server HTTP API', endpoints: { health: { method: 'GET', path: '/health', description: 'Check service health status' }, tools: { method: 'GET', path: '/tools', description: 'List all available tools' }, execute_tool: { method: 'POST', path: '/tools/{toolName}', description: 'Execute a specific tool', body: { arguments: 'object // Tool-specific arguments' } }, batch_execute: { method: 'POST', path: '/tools/batch', description: 'Execute multiple tools in sequence', body: { tools: [ { name: 'string // Tool name', arguments: 'object // Tool arguments' } ] } } }, available_tools: ALL_TOOLS.map(tool => ({ name: tool.name, description: tool.description })), examples: { health_check: `${baseUrl}/health`, list_tools: `${baseUrl}/tools`, execute_forecast: { url: `${baseUrl}/tools/forecast_sales`, method: 'POST', body: { arguments: { time_period: 'Q4', include_scenarios: true } } } }, timestamp: new Date().toISOString() }); }); // Error handling middleware app.use((error, req, res, next) => { console.error('[MCP-HTTP] Unhandled error:', error); res.status(500).json({ success: false, error: 'Internal server error', message: error.message || 'An unexpected error occurred', timestamp: new Date().toISOString() }); }); // 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Not found', message: `Endpoint ${req.method} ${req.originalUrl} not found`, available_endpoints: ['/health', '/tools', '/tools/{toolName}', '/tools/batch', '/docs'], timestamp: new Date().toISOString() }); }); // Start server async function main() { try { console.log(`[MCP-HTTP] Starting ${config.name} v${config.version}`); console.log(`[MCP-HTTP] IRISeller API: ${config.iriseller_api_url}`); console.log(`[MCP-HTTP] CrewAI API: ${config.crewai_api_url}`); console.log(`[MCP-HTTP] CRM Connect API: ${config.crm_connect_api_url}`); console.log(`[MCP-HTTP] Debug mode: ${config.debug}`); console.log(`[MCP-HTTP] Available tools: ${ALL_TOOLS.length}`); // Initialize authentication await initializeAuthentication(); // Test connectivity on startup (non-blocking) try { const health = await apiService.checkHealth(); console.log('[MCP-HTTP] Service health check:', { backend: health.backend ? '✅' : '❌', crewai: health.crewai ? '✅' : '❌', crmConnect: health.crmConnect ? '✅' : '❌' }); if (!health.backend && !health.crewai && !health.crmConnect) { console.warn('[MCP-HTTP] Warning: No services are reachable. HTTP server will start but tools may not work.'); } } catch (error) { console.warn('[MCP-HTTP] Warning: Could not perform initial health check:', error); console.warn('[MCP-HTTP] Continuing to start HTTP server anyway...'); } // Start the server app.listen(PORT, '0.0.0.0', () => { console.log(`[MCP-HTTP] IRISeller MCP Server HTTP Wrapper started`); console.log(`[MCP-HTTP] Server: http://0.0.0.0:${PORT}`); console.log(`[MCP-HTTP] Health: http://0.0.0.0:${PORT}/health`); console.log(`[MCP-HTTP] Tools: http://0.0.0.0:${PORT}/tools`); console.log(`[MCP-HTTP] Docs: http://0.0.0.0:${PORT}/docs`); console.log(`[MCP-HTTP] Available tools: ${ALL_TOOLS.length}`); }); } catch (error) { console.error('[MCP-HTTP] Failed to start server:', error); process.exit(1); } } // Graceful shutdown process.on('SIGINT', () => { console.log('[MCP-HTTP] Received SIGINT, shutting down gracefully...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('[MCP-HTTP] Received SIGTERM, shutting down gracefully...'); process.exit(0); }); // Start the server only when run as HTTP wrapper if (import.meta.url.startsWith('file:') && (import.meta.url === `file://${process.argv[1]}` || process.argv[1].includes('http-wrapper') || process.argv[1].includes('dist/http-wrapper') || process.env.MCP_HTTP_MODE === 'true')) { main().catch((error) => { console.error('[MCP-HTTP] Failed to start server:', error); process.exit(1); }); }