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