UNPKG

@cyanheads/git-mcp-server

Version:

An MCP (Model Context Protocol) server enabling LLMs and AI agents to interact with Git repositories. Provides tools for comprehensive Git operations including clone, commit, branch, diff, log, status, push, pull, merge, rebase, worktree, tag management,

177 lines 6.23 kB
import { BaseErrorCode, McpError } from "../../types-global/errors.js"; // Import config and utils import { environment } from "../../config/index.js"; // Import environment from config import { logger } from "../index.js"; /** * Generic rate limiter that can be used across the application */ export class RateLimiter { config; /** Map storing rate limit data */ limits; /** Cleanup interval timer */ cleanupTimer = null; /** Default configuration */ static DEFAULT_CONFIG = { windowMs: 15 * 60 * 1000, // 15 minutes maxRequests: 100, // 100 requests per window errorMessage: "Rate limit exceeded. Please try again in {waitTime} seconds.", skipInDevelopment: false, cleanupInterval: 5 * 60 * 1000, // 5 minutes }; /** * Create a new rate limiter * @param config Rate limiting configuration */ constructor(config) { this.config = config; this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config }; this.limits = new Map(); this.startCleanupTimer(); // Removed logger call from constructor to prevent logging before initialization } /** * Start the cleanup timer to periodically remove expired entries */ startCleanupTimer() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } const interval = this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval; if (interval) { this.cleanupTimer = setInterval(() => { this.cleanupExpiredEntries(); }, interval); // Ensure the timer doesn't prevent the process from exiting if (this.cleanupTimer.unref) { this.cleanupTimer.unref(); } } } /** * Clean up expired rate limit entries to prevent memory leaks */ cleanupExpiredEntries() { const now = Date.now(); let expiredCount = 0; // Use a synchronized approach to avoid race conditions during cleanup for (const [key, entry] of this.limits.entries()) { if (now >= entry.resetTime) { this.limits.delete(key); expiredCount++; } } if (expiredCount > 0) { logger.debug(`Cleaned up ${expiredCount} expired rate limit entries`, { totalRemaining: this.limits.size, }); } } /** * Update rate limiter configuration * @param config New configuration options */ configure(config) { this.config = { ...this.config, ...config }; // Restart cleanup timer if interval changed if (config.cleanupInterval !== undefined) { this.startCleanupTimer(); } } /** * Get current configuration * @returns Current rate limit configuration */ getConfig() { return { ...this.config }; } /** * Reset all rate limits */ reset() { this.limits.clear(); logger.debug("Rate limiter reset, all limits cleared"); } /** * Check if a request exceeds the rate limit * @param key Unique identifier for the request source * @param context Optional request context * @throws {McpError} If rate limit is exceeded */ check(key, context) { // Skip in development if configured, using the validated environment from config if (this.config.skipInDevelopment && environment === "development") { return; } // Generate key using custom generator if provided const limitKey = this.config.keyGenerator ? this.config.keyGenerator(key, context) : key; const now = Date.now(); // Accessing and updating the limit entry within a single function scope // ensures atomicity in Node.js's single-threaded event loop for Map operations. const limit = () => { // Get current entry or create a new one if it doesn't exist or is expired const entry = this.limits.get(limitKey); // Create new entry or reset if expired if (!entry || now >= entry.resetTime) { const newEntry = { count: 1, resetTime: now + this.config.windowMs, }; this.limits.set(limitKey, newEntry); return newEntry; } // Check if limit exceeded if (entry.count >= this.config.maxRequests) { const waitTime = Math.ceil((entry.resetTime - now) / 1000); const errorMessage = this.config.errorMessage?.replace("{waitTime}", waitTime.toString()) || `Rate limit exceeded. Please try again in ${waitTime} seconds.`; throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, { waitTime, key: limitKey, }); } // Increment counter and return updated entry entry.count++; return entry; }; // Execute the rate limiting logic limit(); } /** * Get rate limit information for a key * @param key The rate limit key * @returns Current rate limit status or null if no record exists */ getStatus(key) { const entry = this.limits.get(key); if (!entry) { return null; } return { current: entry.count, limit: this.config.maxRequests, remaining: Math.max(0, this.config.maxRequests - entry.count), resetTime: entry.resetTime, }; } /** * Stop the cleanup timer when the limiter is no longer needed */ dispose() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } // Clear all entries this.limits.clear(); } } /** * Create and export a default rate limiter instance */ export const rateLimiter = new RateLimiter({ windowMs: 15 * 60 * 1000, // 15 minutes maxRequests: 100, // 100 requests per window }); //# sourceMappingURL=rateLimiter.js.map