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