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