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

189 lines 7.18 kB
/** * Rate limiting functionality for VineGuard MCP Server * Prevents abuse and DoS attacks by limiting the number of requests per time window */ export class RateLimiter { requests = new Map(); config; constructor(config) { this.config = { windowMs: 60000, // 1 minute default maxRequests: 100, // 100 requests per minute default toolSpecific: new Map([ // More restrictive limits for resource-intensive tools ['run_tests', { windowMs: 60000, maxRequests: 10 }], ['detect_bugs', { windowMs: 60000, maxRequests: 5 }], ['generate_fixes', { windowMs: 60000, maxRequests: 3 }], ['scan_project', { windowMs: 60000, maxRequests: 20 }], ['analyze_code', { windowMs: 60000, maxRequests: 30 }], // Less restrictive for lightweight tools ['analyze_prd', { windowMs: 60000, maxRequests: 50 }], ['generate_test', { windowMs: 60000, maxRequests: 25 }], ]), ...config }; } /** * Check if a request should be allowed based on rate limits */ checkLimit(identifier, toolName) { const now = Date.now(); // Get tool-specific config or use default const effectiveConfig = toolName && this.config.toolSpecific?.has(toolName) ? this.config.toolSpecific.get(toolName) : { windowMs: this.config.windowMs, maxRequests: this.config.maxRequests }; // Create a unique key for this identifier and tool combination const key = toolName ? `${identifier}:${toolName}` : identifier; // Clean up expired records periodically this.cleanupExpiredRecords(now); // Get or create record for this key let record = this.requests.get(key); if (!record || now >= record.resetTime) { // Create new record or reset expired one record = { count: 1, resetTime: now + effectiveConfig.windowMs }; this.requests.set(key, record); return { allowed: true, remainingRequests: effectiveConfig.maxRequests - 1, resetTime: record.resetTime }; } // Check if limit exceeded if (record.count >= effectiveConfig.maxRequests) { return { allowed: false, remainingRequests: 0, resetTime: record.resetTime, error: `Rate limit exceeded for ${toolName || 'requests'}. Try again in ${Math.ceil((record.resetTime - now) / 1000)} seconds.` }; } // Increment count and allow request record.count++; this.requests.set(key, record); return { allowed: true, remainingRequests: effectiveConfig.maxRequests - record.count, resetTime: record.resetTime }; } /** * Get current usage stats for an identifier */ getUsageStats(identifier, toolName) { const key = toolName ? `${identifier}:${toolName}` : identifier; const record = this.requests.get(key); if (!record) { return null; } const effectiveConfig = toolName && this.config.toolSpecific?.has(toolName) ? this.config.toolSpecific.get(toolName) : { windowMs: this.config.windowMs, maxRequests: this.config.maxRequests }; return { count: record.count, limit: effectiveConfig.maxRequests, resetTime: record.resetTime }; } /** * Reset rate limit for a specific identifier (admin function) */ resetLimit(identifier, toolName) { const key = toolName ? `${identifier}:${toolName}` : identifier; this.requests.delete(key); } /** * Clean up expired records to prevent memory leaks */ cleanupExpiredRecords(now) { for (const [key, record] of this.requests.entries()) { if (now >= record.resetTime) { this.requests.delete(key); } } } /** * Get all active rate limit records (for monitoring) */ getActiveRecords() { const now = Date.now(); const activeRecords = []; for (const [key, record] of this.requests.entries()) { if (now < record.resetTime) { activeRecords.push({ key, count: record.count, resetTime: record.resetTime }); } } return activeRecords; } /** * Update rate limit configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Create identifier from request context * In production, this could use IP address, user ID, API key, etc. */ static createIdentifier(context) { // For MCP server, we might not have traditional request info // This could be enhanced to use client information when available if (context?.clientId) { return context.clientId; } if (context?.userId) { return context.userId; } // Fallback to a default identifier // In a real deployment, you'd want to get actual client identification return 'default-client'; } /** * Create a rate limit middleware for tool calls */ createMiddleware() { return (identifier, toolName) => { return this.checkLimit(identifier, toolName); }; } /** * Get human-readable rate limit info */ getRateLimitInfo(toolName) { const effectiveConfig = toolName && this.config.toolSpecific?.has(toolName) ? this.config.toolSpecific.get(toolName) : { windowMs: this.config.windowMs, maxRequests: this.config.maxRequests }; const windowSeconds = Math.floor(effectiveConfig.windowMs / 1000); const windowMinutes = Math.floor(windowSeconds / 60); if (windowMinutes > 0) { return `${effectiveConfig.maxRequests} requests per ${windowMinutes} minute${windowMinutes > 1 ? 's' : ''}`; } else { return `${effectiveConfig.maxRequests} requests per ${windowSeconds} second${windowSeconds > 1 ? 's' : ''}`; } } } // Default rate limiter instance export const defaultRateLimiter = new RateLimiter(); // Rate limit decorator for tool functions export function rateLimit(toolName) { return function (target, propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args) { const identifier = RateLimiter.createIdentifier(); const result = defaultRateLimiter.checkLimit(identifier, toolName); if (!result.allowed) { throw new Error(result.error || 'Rate limit exceeded'); } return originalMethod.apply(this, args); }; return descriptor; }; } //# sourceMappingURL=rate-limiter.js.map