UNPKG

@venly/wallet-mcp

Version:

Production-ready MCP server enabling AI agents to perform Web3 wallet operations through Venly's blockchain infrastructure. Supports 14+ networks including Ethereum, Polygon, Arbitrum, and stablecoin operations.

374 lines 14.8 kB
/** * HTTP Server for Docker Deployment * * Provides HTTP endpoints for health checks and metrics * while maintaining MCP server functionality */ import http from 'http'; import url from 'url'; import { VenlyMCPServer } from './VenlyMCPServer.js'; import { HealthCheckService, HealthRoutes } from '../health/health.js'; import { PrometheusMetricsService, MetricsRoutes } from '../metrics/prometheus.js'; import { VenlyClient } from '../venly/VenlyClient.js'; import winston from 'winston'; export class HttpServer { server; venlyMcpServer; healthService; healthRoutes; metricsService; metricsRoutes; logger; port; constructor() { this.port = parseInt(process.env['MCP_SERVER_PORT'] || '3000', 10); this.logger = this.createLogger(); // Initialize MCP server const environment = process.env['VENLY_ENVIRONMENT'] || 'sandbox'; this.venlyMcpServer = new VenlyMCPServer(environment); // Get VenlyClient from MCP server (we'll need to expose this) const venlyClient = this.getVenlyClientFromMcpServer(); // Initialize health and metrics services this.healthService = new HealthCheckService(venlyClient, this.logger); this.healthRoutes = new HealthRoutes(this.healthService); this.metricsService = new PrometheusMetricsService(venlyClient, this.logger); this.metricsRoutes = new MetricsRoutes(this.metricsService); // Create HTTP server this.server = http.createServer(this.handleRequest.bind(this)); this.logger.info('HTTP Server initialized'); } /** * Get VenlyClient from MCP server (temporary workaround) */ getVenlyClientFromMcpServer() { // Create a VenlyClient without credentials since they're provided per-request // The credentials are validated and used per-request in the MCP tools const environment = process.env['VENLY_ENVIRONMENT'] || 'sandbox'; return new VenlyClient(environment); } /** * Handle HTTP requests */ async handleRequest(req, res) { const startTime = Date.now(); const parsedUrl = url.parse(req.url || '', true); const pathname = parsedUrl.pathname || ''; const method = req.method || 'GET'; // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Handle preflight requests if (method === 'OPTIONS') { res.writeHead(200); res.end(); return; } try { let result = null; // Route requests if (pathname.startsWith('/tools/') && method === 'POST') { // Handle MCP tool requests result = await this.handleMcpToolRequest(req, pathname); } else { switch (pathname) { case '/health': if (method === 'GET') { result = await this.healthRoutes.handleBasicHealth(); } break; case '/health/detailed': if (method === 'GET') { result = await this.healthRoutes.handleDetailedHealth(); } break; case '/health/ready': if (method === 'GET') { result = await this.healthRoutes.handleReadinessProbe(); } break; case '/health/live': if (method === 'GET') { result = await this.healthRoutes.handleLivenessProbe(); } break; case '/metrics': if (method === 'GET') { result = await this.metricsRoutes.handleMetrics(); } break; case '/status': if (method === 'GET') { result = { statusCode: 200, body: this.venlyMcpServer.getStatus() }; } break; default: // 404 for unknown routes result = { statusCode: 404, body: { error: 'Not Found', message: `Route ${pathname} not found`, availableRoutes: [ '/health', '/health/detailed', '/health/ready', '/health/live', '/metrics', '/status', '/tools/* (POST)' ] } }; break; } } if (result) { // Set response headers if (result.headers) { Object.entries(result.headers).forEach(([key, value]) => { res.setHeader(key, value); }); } // Set content type if not already set if (!res.getHeader('Content-Type')) { if (pathname === '/metrics') { res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); } else { res.setHeader('Content-Type', 'application/json'); } } // Send response res.writeHead(result.statusCode); if (typeof result.body === 'string') { res.end(result.body); } else { res.end(JSON.stringify(result.body, null, 2)); } // Record metrics const responseTime = Date.now() - startTime; const success = result.statusCode < 400; this.metricsService.recordRequest(pathname, responseTime, success); this.healthService.recordRequest(responseTime, success); this.logger.info(`${method} ${pathname} - ${result.statusCode} (${responseTime}ms)`); } else { // Method not allowed res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Method Not Allowed', message: `Method ${method} not allowed for ${pathname}` })); } } catch (error) { this.logger.error('Request handling error', { error, pathname, method }); // Record error metrics const responseTime = Date.now() - startTime; this.metricsService.recordRequest(pathname, responseTime, false); this.healthService.recordRequest(responseTime, false); // Send error response res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Internal Server Error', message: 'An unexpected error occurred' })); } } /** * Handle MCP tool requests */ async handleMcpToolRequest(req, pathname) { const toolName = pathname.replace('/tools/', ''); this.logger.info(`🔧 MCP Tool Request: ${toolName}`); // Read request body const body = await this.readRequestBody(req); let requestData; try { requestData = JSON.parse(body); this.logger.debug(`📋 Request data for ${toolName}:`, { hasClientId: !!requestData.venly_client_id, hasClientSecret: !!requestData.venly_client_secret, keys: Object.keys(requestData) }); } catch (error) { this.logger.error(`❌ Invalid JSON in request body for ${toolName}:`, error); return { statusCode: 400, body: { error: 'Bad Request', message: 'Invalid JSON in request body' } }; } try { // Call the MCP tool handler directly this.logger.info(`🚀 Calling MCP tool handler: ${toolName}`); const mcpResult = await this.venlyMcpServer.handleToolCall(toolName, requestData); this.logger.info(`✅ MCP tool ${toolName} completed successfully`); // Extract the actual JSON from MCP format for HTTP API compatibility if (mcpResult.content && mcpResult.content[0] && mcpResult.content[0].text) { try { const actualResult = JSON.parse(mcpResult.content[0].text); this.logger.debug(`📤 Extracted result for ${toolName}:`, { success: actualResult.success, hasTransaction: !!actualResult.transaction, hasError: !!actualResult.error }); return { statusCode: 200, body: actualResult }; } catch (parseError) { this.logger.warn(`⚠️ Could not parse MCP result text for ${toolName}, returning raw MCP format`); return { statusCode: 200, body: mcpResult }; } } else { // Fallback to raw MCP result if format is unexpected this.logger.warn(`⚠️ Unexpected MCP result format for ${toolName}, returning raw result`); return { statusCode: 200, body: mcpResult }; } } catch (error) { this.logger.error(`❌ MCP tool error: ${toolName}`, { error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined, requestData: { hasClientId: !!requestData.venly_client_id, hasClientSecret: !!requestData.venly_client_secret, toolName } }); return { statusCode: 400, body: { content: [{ type: 'text', text: JSON.stringify({ error: 'MCP Tool Error', message: error instanceof Error ? error.message : 'Unknown error occurred', tool: toolName }) }] } }; } } /** * Read request body */ async readRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', (error) => { reject(error); }); }); } /** * Create Winston logger instance */ createLogger() { const logLevel = process.env['MCP_LOG_LEVEL'] || 'info'; return winston.createLogger({ level: logLevel, format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()), defaultMeta: { service: 'venly-mcp-http-server' }, transports: [ new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.simple()) }) ] }); } /** * Start the HTTP server */ async start() { return new Promise((resolve, reject) => { this.server.listen(this.port, () => { this.logger.info(`HTTP Server listening on port ${this.port}`); this.logger.info('Available endpoints:'); this.logger.info(' GET /health - Basic health check'); this.logger.info(' GET /health/detailed - Detailed health information'); this.logger.info(' GET /health/ready - Readiness probe'); this.logger.info(' GET /health/live - Liveness probe'); this.logger.info(' GET /metrics - Prometheus metrics'); this.logger.info(' GET /status - Server status'); resolve(); }); this.server.on('error', (error) => { this.logger.error('HTTP Server error', { error }); reject(error); }); }); } /** * Stop the HTTP server */ async stop() { return new Promise((resolve, reject) => { this.server.close((error) => { if (error) { this.logger.error('Error stopping HTTP server', { error }); reject(error); } else { this.logger.info('HTTP Server stopped'); resolve(); } }); }); } /** * Start MCP server (for stdio transport) */ async startMcpServer() { // Only start MCP server if transport is stdio const transport = process.env['MCP_TRANSPORT'] || 'stdio'; if (transport === 'stdio') { await this.venlyMcpServer.start(); } } /** * Stop MCP server */ async stopMcpServer() { await this.venlyMcpServer.stop(); } /** * Get server status */ getStatus() { return { http: { port: this.port, listening: this.server.listening }, mcp: this.venlyMcpServer.getStatus(), metrics: this.metricsService.getMetricsSummary() }; } } //# sourceMappingURL=HttpServer.js.map