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

249 lines (248 loc) 10.5 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { IRISellerAPIService } from './services/iriseller-api.js'; import { ToolHandlers } from './handlers/tool-handlers.js'; import { ALL_TOOLS } from './tools/index.js'; import { Command } from 'commander'; import dotenv from 'dotenv'; import jwt from 'jsonwebtoken'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; // Debug logging utility that respects debug flag and doesn't contaminate MCP stream let debugEnabled = false; const debugLog = (message, ...args) => { if (debugEnabled) { // Use stderr for debug messages to avoid contaminating MCP JSON-RPC stream console.error(message, ...args); } }; const errorLog = (message, ...args) => { // Always log errors to stderr (for critical issues only) console.error(message, ...args); }; // Load environment variables dotenv.config(); // Get package version const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')); const VERSION = packageJson.version; // CLI Setup const program = new Command(); program .name('iriseller-mcp-server') .description('IRISeller MCP Server - AI Sales Intelligence Platform') .version(VERSION) .option('--email <email>', 'IRISeller account email') .option('--password <password>', 'IRISeller account password') .option('--api-url <url>', 'IRISeller API URL', process.env.IRISELLER_API_URL || 'https://beta.iriseller.com') .option('--crewai-url <url>', 'CrewAI API URL', process.env.CREWAI_API_URL) .option('--debug', 'Enable debug mode', false) .option('--jwt-secret <secret>', 'JWT secret for token generation', process.env.JWT_SECRET) .parse(); const options = program.opts(); // 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) { // Note: This runs before debugEnabled is set, so we always log this error errorLog('[MCP] Failed to generate user token:', error); return undefined; } } // Configuration const baseApiUrl = options.apiUrl || process.env.IRISELLER_API_URL || 'http://localhost:3001'; const config = { name: process.env.MCP_SERVER_NAME || 'iriseller-mcp-server', version: process.env.MCP_SERVER_VERSION || VERSION, iriseller_api_url: baseApiUrl, crewai_api_url: options.crewaiUrl || process.env.CREWAI_API_URL || (baseApiUrl.includes('localhost') ? 'http://localhost:8001' : `${baseApiUrl.replace('/api', '')}/crewai`), crm_connect_api_url: process.env.CRM_CONNECT_API_URL || `${baseApiUrl}/api/crm-connect`, api_key: process.env.IRISELLER_API_KEY, jwt_secret: options.jwtSecret || 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: options.debug || process.env.DEBUG === 'true' }; // Set debug logging flag debugEnabled = config.debug ?? false; // Initialize services const apiService = new IRISellerAPIService(config); const toolHandlers = new ToolHandlers(apiService); // Create MCP server const server = new Server({ name: config.name, version: config.version, description: 'MCP Server for IRISeller AI Agent Ecosystem - provides access to AI agents, CRM data, and sales intelligence' }, { capabilities: { tools: { listChanged: true } } }); // Handle tool listing server.setRequestHandler(ListToolsRequestSchema, async () => { debugLog('[MCP] Listing available tools'); return { tools: ALL_TOOLS }; }); // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { debugLog(`[MCP] Executing tool: ${request.params.name}`); try { return await toolHandlers.handleToolCall(request); } catch (error) { if (error instanceof McpError) { throw error; } errorLog(`[MCP] Unexpected error executing tool ${request.params.name}:`, error); throw new McpError(ErrorCode.InternalError, `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`); } }); // Error handling process.on('uncaughtException', (error) => { errorLog('[MCP] Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { errorLog('[MCP] Unhandled rejection at:', promise, 'reason:', reason); process.exit(1); }); // Graceful shutdown with single handler let isShuttingDown = false; const gracefulShutdown = async (signal) => { if (isShuttingDown) { debugLog(`[MCP] Already shutting down, ignoring ${signal}`); return; } isShuttingDown = true; debugLog(`[MCP] Received ${signal}, shutting down gracefully...`); try { // Close server connections gracefully await server.close(); debugLog('[MCP] Server closed successfully'); } catch (error) { errorLog('[MCP] Error during shutdown:', error); } process.exit(0); }; process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Start the server async function main() { debugLog(`[MCP] Starting ${config.name} v${config.version}`); debugLog(`[MCP] IRISeller API: ${config.iriseller_api_url}`); debugLog(`[MCP] CrewAI API: ${config.crewai_api_url}`); debugLog(`[MCP] CRM Connect API: ${config.crm_connect_api_url}`); debugLog(`[MCP] Debug mode: ${config.debug}`); debugLog(`[MCP] Available tools: ${ALL_TOOLS.length}`); // Set user token if provided (for authenticated CRM operations) let userToken = process.env.USER_TOKEN; // Generate token from CLI credentials if provided if (!userToken && options.email && options.password) { userToken = await generateUserToken(options.email, options.password, config.jwt_secret); if (userToken) { debugLog(`[MCP] Generated user token for ${options.email}`); } } // If no user token and no CLI credentials, use environment variable credentials or show warning if (!userToken && !options.email && !options.password && config.jwt_secret) { const defaultEmail = process.env.MCP_DEFAULT_USER_EMAIL; const defaultPassword = process.env.MCP_DEFAULT_USER_PASSWORD; if (defaultEmail && defaultPassword) { userToken = await generateUserToken(defaultEmail, defaultPassword, config.jwt_secret); if (userToken) { debugLog(`[MCP] Generated default user token for ${defaultEmail} (LiraAssistant compatibility)`); } } else { debugLog(`[MCP] Warning: No credentials provided. Set MCP_DEFAULT_USER_EMAIL and MCP_DEFAULT_USER_PASSWORD environment variables or provide --email and --password arguments.`); } } if (userToken) { toolHandlers.setUserToken(userToken); debugLog('[MCP] User token configured for authenticated operations'); } else { debugLog('[MCP] No user token provided - using system authentication'); if (options.email && !config.jwt_secret) { debugLog('[MCP] Warning: Email provided but no JWT secret available for token generation'); } } // Test connectivity on startup (non-blocking) try { const health = await apiService.checkHealth(); debugLog('[MCP] Service health check:', { backend: health.backend ? '✅' : '❌', crewai: health.crewai ? '✅' : '❌', crmConnect: health.crmConnect ? '✅' : '❌' }); if (!health.backend && !health.crewai && !health.crmConnect) { debugLog('[MCP] Warning: No services are reachable. MCP server will start but tools may not work.'); } } catch (error) { debugLog('[MCP] Warning: Could not perform initial health check:', error); debugLog('[MCP] Continuing to start MCP server anyway...'); } // Start the transport try { const transport = new StdioServerTransport(); debugLog('[MCP] Initializing STDIO transport...'); // Add transport event handlers for debugging transport.onclose = () => { debugLog('[MCP] Transport closed'); }; transport.onerror = (error) => { errorLog('[MCP] Transport error:', error); }; await server.connect(transport); debugLog('[MCP] Server started and ready for connections'); // Keep the process alive process.stdin.resume(); } catch (error) { errorLog('[MCP] Failed to start transport:', error); throw error; } } // Run the server if this file is executed directly if (import.meta.url.startsWith('file:') && (import.meta.url === `file://${process.argv[1]}` || process.argv[1].endsWith('index.js') || process.argv[1].includes('iriseller-mcp-server'))) { main().catch((error) => { errorLog('[MCP] Failed to start server:', error); errorLog('[MCP] Error details:', error.stack); process.exit(1); }); }