@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
JavaScript
/**
* 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