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