UNPKG

vineguard-mcp-server-standalone

Version:

VineGuard MCP Server v2.1 - Intelligent QA Workflow System with advanced test generation for Jest/RTL, Cypress, and Playwright. Features smart project analysis, progressive testing strategies, and comprehensive quality patterns for React/Vue/Angular proje

382 lines 13.6 kB
/** * HTTP transport implementation for VineGuard MCP Server * Enables running the server as an HTTP service for cloud deployments */ import * as http from 'http'; import * as url from 'url'; import { defaultRateLimiter } from '../security/rate-limiter.js'; import { InputValidator } from '../security/input-validator.js'; export class HttpTransport { server; mcpServer; options; metrics; constructor(mcpServer, options = {}) { this.mcpServer = mcpServer; this.options = { port: 3001, host: '0.0.0.0', enableCors: true, corsOrigins: ['*'], enableRateLimit: true, enableMetrics: true, maxRequestSize: 10 * 1024 * 1024, // 10MB timeout: 30000, // 30 seconds ...options }; this.metrics = { totalRequests: 0, requestsByTool: new Map(), errorCount: 0, averageResponseTime: 0, lastRequestTime: 0 }; this.server = this.createServer(); } /** * Create HTTP server with proper middleware */ createServer() { const server = http.createServer((req, res) => { const startTime = Date.now(); // Set basic security headers this.setSecurityHeaders(res); // Handle CORS if (this.options.enableCors) { this.handleCors(req, res); } // Handle preflight requests if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } // Only allow POST requests for MCP if (req.method !== 'POST') { this.sendError(res, 405, 'Method Not Allowed'); return; } // Parse URL and route requests const parsedUrl = url.parse(req.url, true); switch (parsedUrl.pathname) { case '/mcp': this.handleMcpRequest(req, res, startTime); break; case '/health': this.handleHealthCheck(req, res); break; case '/metrics': this.handleMetrics(req, res); break; default: this.sendError(res, 404, 'Not Found'); } }); // Set server timeout server.timeout = this.options.timeout; return server; } /** * Set security headers */ setSecurityHeaders(res) { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.setHeader('Content-Security-Policy', "default-src 'self'"); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); } /** * Handle CORS headers */ handleCors(req, res) { const origin = req.headers.origin; if (this.options.corsOrigins?.includes('*') || (origin && this.options.corsOrigins?.includes(origin))) { res.setHeader('Access-Control-Allow-Origin', origin || '*'); } res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Client-ID'); res.setHeader('Access-Control-Max-Age', '86400'); } /** * Handle MCP requests */ async handleMcpRequest(req, res, startTime) { try { // Check content length const contentLength = parseInt(req.headers['content-length'] || '0'); if (contentLength > this.options.maxRequestSize) { this.sendError(res, 413, 'Request Too Large'); return; } // Rate limiting if (this.options.enableRateLimit) { const clientId = this.getClientId(req); const rateLimit = defaultRateLimiter.checkLimit(clientId); if (!rateLimit.allowed) { this.sendError(res, 429, rateLimit.error || 'Rate limit exceeded'); return; } // Add rate limit headers res.setHeader('X-RateLimit-Remaining', rateLimit.remainingRequests || 0); res.setHeader('X-RateLimit-Reset', rateLimit.resetTime || 0); } // Parse request body const body = await this.parseRequestBody(req); let requestData; try { requestData = JSON.parse(body); } catch (error) { this.sendError(res, 400, 'Invalid JSON'); return; } // Validate request structure if (!this.isValidMcpRequest(requestData)) { this.sendError(res, 400, 'Invalid MCP request structure'); return; } // Additional input validation const validation = InputValidator.validateToolArgs(requestData.method, requestData.params?.arguments); if (!validation.isValid) { this.sendError(res, 400, `Input validation failed: ${validation.error}`); return; } // Update sanitized arguments if (requestData.params?.arguments && validation.sanitizedValue) { requestData.params.arguments = validation.sanitizedValue; } // Process MCP request const response = await this.processMcpRequest(requestData); // Update metrics this.updateMetrics(requestData, startTime, false); // Send response res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify(response)); } catch (error) { console.error('MCP request error:', error); this.updateMetrics(null, startTime, true); this.sendError(res, 500, 'Internal Server Error'); } } /** * Handle health check requests */ handleHealthCheck(req, res) { const health = { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), version: '2.0.0', metrics: this.options.enableMetrics ? { totalRequests: this.metrics.totalRequests, errorCount: this.metrics.errorCount, averageResponseTime: this.metrics.averageResponseTime } : undefined }; res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify(health)); } /** * Handle metrics requests */ handleMetrics(req, res) { if (!this.options.enableMetrics) { this.sendError(res, 404, 'Metrics disabled'); return; } const detailedMetrics = { ...this.metrics, requestsByTool: Object.fromEntries(this.metrics.requestsByTool), memory: process.memoryUsage(), rateLimitStats: defaultRateLimiter.getActiveRecords() }; res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify(detailedMetrics)); } /** * Parse request body */ parseRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', reject); }); } /** * Validate MCP request structure */ isValidMcpRequest(data) { return data && typeof data === 'object' && typeof data.jsonrpc === 'string' && data.jsonrpc === '2.0' && typeof data.method === 'string' && (data.id === undefined || typeof data.id === 'string' || typeof data.id === 'number'); } /** * Process MCP request using the server */ async processMcpRequest(requestData) { // For HTTP transport, we need to manually handle the MCP requests // since the SDK's request method expects different parameters try { switch (requestData.method) { case 'tools/list': // Return list of available tools return { jsonrpc: '2.0', id: requestData.id, result: { tools: [] // This would need to be populated from the server's tool list } }; case 'tools/call': // Execute a tool call return { jsonrpc: '2.0', id: requestData.id, result: { content: [{ type: 'text', text: 'Tool execution not yet implemented in HTTP mode' }] } }; case 'resources/list': // Return list of available resources return { jsonrpc: '2.0', id: requestData.id, result: { resources: [] } }; case 'resources/read': // Read a resource return { jsonrpc: '2.0', id: requestData.id, result: { contents: [{ type: 'text', text: 'Resource reading not yet implemented in HTTP mode' }] } }; default: return { jsonrpc: '2.0', id: requestData.id, error: { code: -32601, message: 'Method not found' } }; } } catch (error) { return { jsonrpc: '2.0', id: requestData.id, error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' } }; } } /** * Get client identifier for rate limiting */ getClientId(req) { // Try to get client ID from headers const clientId = req.headers['x-client-id']; if (clientId && typeof clientId === 'string') { return clientId; } // Fall back to IP address const forwarded = req.headers['x-forwarded-for']; if (forwarded && typeof forwarded === 'string') { return forwarded.split(',')[0].trim(); } return req.socket.remoteAddress || 'unknown'; } /** * Update request metrics */ updateMetrics(requestData, startTime, isError) { if (!this.options.enableMetrics) return; const responseTime = Date.now() - startTime; this.metrics.totalRequests++; this.metrics.lastRequestTime = Date.now(); if (isError) { this.metrics.errorCount++; } if (requestData?.method) { const current = this.metrics.requestsByTool.get(requestData.method) || 0; this.metrics.requestsByTool.set(requestData.method, current + 1); } // Update average response time const totalTime = this.metrics.averageResponseTime * (this.metrics.totalRequests - 1) + responseTime; this.metrics.averageResponseTime = totalTime / this.metrics.totalRequests; } /** * Send error response */ sendError(res, statusCode, message) { res.setHeader('Content-Type', 'application/json'); res.writeHead(statusCode); res.end(JSON.stringify({ error: { code: statusCode, message: message }, timestamp: new Date().toISOString() })); } /** * Start the HTTP server */ async start() { return new Promise((resolve, reject) => { this.server.listen(this.options.port, this.options.host, () => { console.error(`[VineGuard HTTP] Server started on ${this.options.host}:${this.options.port}`); resolve(); }); this.server.on('error', reject); }); } /** * Stop the HTTP server */ async stop() { return new Promise((resolve) => { this.server.close(() => { console.error('[VineGuard HTTP] Server stopped'); resolve(); }); }); } /** * Get server metrics */ getMetrics() { return { ...this.metrics }; } } //# sourceMappingURL=http.js.map