import express, { Request, Response } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { randomUUID } from 'uuid';
import promClient from 'prom-client';
import { createLogger } from './utils/logger';
import { RpcBaseError, InternalError } from './utils/errors';
import { JsonRpcRequest, JsonRpcResponse } from './schema';
import { veeqoMethods } from './methods/veeqo';
import { easypostMethods } from './methods/easypost';
import { unifiedMethods } from './methods/unified';
import { webMethods } from './methods/web';
import { authenticateToken } from './utils/middleware';
const logger = createLogger();
const app = express();
const port = process.env.PORT || 3000;
// Prometheus metrics
const collectDefaultMetrics = promClient.collectDefaultMetrics;
collectDefaultMetrics({ register: promClient.register });
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'code'],
buckets: [0.1, 5, 15, 50, 100, 200, 300, 400, 500]
});
const httpRequestTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'code']
});
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// Request logging middleware
app.use((req, res, next) => {
const correlationId = req.headers['x-correlation-id'] as string || randomUUID();
req.headers['x-correlation-id'] = correlationId;
res.setHeader('X-Correlation-ID', correlationId);
logger.info('Incoming request', {
correlationId,
method: req.method,
url: req.url,
body: req.body
});
const end = httpRequestDurationMicroseconds.startTimer();
res.on('finish', () => {
const duration = end();
httpRequestTotal.inc({
method: req.method,
route: req.route?.path || req.url,
code: res.statusCode
});
logger.info('Request completed', {
correlationId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration
});
});
next();
});
// Method registry
const methodRegistry: Record<string, Function> = {
// Veeqo methods
'veeqo.createOrder': veeqoMethods.createOrder,
'veeqo.getOrder': veeqoMethods.getOrder,
'veeqo.listOrders': veeqoMethods.listOrders,
'veeqo.updateOrder': veeqoMethods.updateOrder,
'veeqo.deleteOrder': veeqoMethods.deleteOrder,
'veeqo.syncInventory': veeqoMethods.syncInventory,
// EasyPost methods
'easypost.createShipment': easypostMethods.createShipment,
'easypost.getRates': easypostMethods.getRates,
'easypost.buyLabel': easypostMethods.buyLabel,
'easypost.trackShipment': easypostMethods.trackShipment,
'easypost.refundLabel': easypostMethods.refundLabel,
// Unified methods
'unified.createOrderWithLabel': unifiedMethods.createOrderWithLabel,
'unified.syncTracking': unifiedMethods.syncTracking,
'unified.fulfillOrder': unifiedMethods.fulfillOrder,
// Web methods (no authentication required)
'web.login': webMethods.login,
'web.getDashboardStats': webMethods.getDashboardStats,
'web.searchOrders': webMethods.searchOrders,
'web.getLowStockItems': webMethods.getLowStockItems
};
// Authenticated method registry
const authenticatedMethodRegistry: Record<string, Function> = {
'web.changePassword': webMethods.changePassword
};
// JSON-RPC endpoint
app.post('/rpc', async (req: Request, res: Response) => {
const correlationId = req.headers['x-correlation-id'] as string;
const localLogger = createLogger(correlationId);
try {
const jsonRpcRequest: JsonRpcRequest = req.body;
// Validate JSON-RPC format
if (jsonRpcRequest.jsonrpc !== '2.0') {
const errorResponse: JsonRpcResponse = {
jsonrpc: '2.0',
error: {
code: -32600,
message: 'Invalid Request',
'Invalid JSON-RPC version'
},
id: jsonRpcRequest.id || null
};
return res.status(400).json(errorResponse);
}
// Check if method exists
let method = methodRegistry[jsonRpcRequest.method];
let requiresAuth = false;
if (!method) {
method = authenticatedMethodRegistry[jsonRpcRequest.method];
requiresAuth = true;
}
if (!method) {
const errorResponse: JsonRpcResponse = {
jsonrpc: '2.0',
error: {
code: -32601,
message: 'Method not found',
`Method '${jsonRpcRequest.method}' not found`
},
id: jsonRpcRequest.id || null
};
return res.status(404).json(errorResponse);
}
// Check authentication for protected methods
if (requiresAuth) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
const errorResponse: JsonRpcResponse = {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Authentication required',
data: 'Access token required for this method'
},
id: jsonRpcRequest.id || null
};
return res.status(401).json(errorResponse);
}
const user = require('./utils/auth').AuthService.verifyToken(token);
if (!user) {
const errorResponse: JsonRpcResponse = {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Invalid token',
data: 'Invalid or expired token'
},
id: jsonRpcRequest.id || null
};
return res.status(403).json(errorResponse);
}
// Add user to params for methods that need it
if (jsonRpcRequest.method === 'web.changePassword') {
jsonRpcRequest.params = jsonRpcRequest.params || {};
jsonRpcRequest.params.userId = user.id;
}
}
// Execute method
localLogger.info('Executing method', { method: jsonRpcRequest.method });
const result = await method(jsonRpcRequest.params || {});
// Return success response
const successResponse: JsonRpcResponse = {
jsonrpc: '2.0',
result,
id: jsonRpcRequest.id || null
};
res.status(200).json(successResponse);
} catch (error) {
localLogger.error('Error processing request', { error });
let rpcError;
if (error instanceof RpcBaseError) {
rpcError = error.toJSON();
} else {
rpcError = new InternalError('Internal server error').toJSON();
}
const errorResponse: JsonRpcResponse = {
jsonrpc: '2.0',
error: {
code: -32000,
message: rpcError.message,
{
code: rpcError.code,
details: rpcError.details
}
},
id: req.body.id || null
};
res.status(500).json(errorResponse);
}
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Metrics endpoint
app.get('/metrics', async (req, res) => {
try {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
} catch (ex) {
res.status(500).end(ex);
}
});
// Serve static files for web interface
app.use(express.static('web/build'));
// Catch-all handler for React Router
app.get('*', (req, res) => {
res.sendFile('web/build/index.html', { root: '.' });
});
// Global error handler
app.use((error: Error, req: Request, res: Response, next: Function) => {
logger.error('Unhandled error', { error });
const errorResponse: JsonRpcResponse = {
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal error',
data: error.message
},
id: null
};
res.status(500).json(errorResponse);
});
// Start server
app.listen(port, () => {
logger.info(`MCP Server listening at http://localhost:${port}`);
});
export default app;