UNPKG

perplexity-mcp-server

Version:

A Perplexity API Model Context Protocol (MCP) server that unlocks Perplexity's search-augmented AI capabilities for LLM agents. Features robust error handling, secure input validation, and transparent reasoning with the showThinking parameter. Built with

180 lines (179 loc) 6.24 kB
/** * @fileoverview Provides a generic `RateLimiter` class for implementing rate limiting logic. * It supports configurable time windows, request limits, and automatic cleanup of expired entries. * @module src/utils/security/rateLimiter */ import { environment } from "../../config/index.js"; import { BaseErrorCode, McpError } from "../../types-global/errors.js"; import { logger, requestContextService } from "../index.js"; /** * A generic rate limiter class using an in-memory store. * Controls frequency of operations based on unique keys. */ export class RateLimiter { /** * Creates a new `RateLimiter` instance. * @param config - Configuration options, merged with defaults. */ constructor(config) { this.config = config; /** * Timer ID for periodic cleanup. * @private */ this.cleanupTimer = null; this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config }; this.limits = new Map(); this.startCleanupTimer(); } /** * Starts the periodic timer to clean up expired rate limit entries. * @private */ startCleanupTimer() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } const interval = this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval; if (interval && interval > 0) { this.cleanupTimer = setInterval(() => { this.cleanupExpiredEntries(); }, interval); if (this.cleanupTimer.unref) { this.cleanupTimer.unref(); // Allow Node.js process to exit if only timer active } } } /** * Removes expired rate limit entries from the store. * @private */ cleanupExpiredEntries() { const now = Date.now(); let expiredCount = 0; for (const [key, entry] of this.limits.entries()) { if (now >= entry.resetTime) { this.limits.delete(key); expiredCount++; } } if (expiredCount > 0) { const logContext = requestContextService.createRequestContext({ operation: "RateLimiter.cleanupExpiredEntries", cleanedCount: expiredCount, totalRemainingAfterClean: this.limits.size, }); logger.debug(`Cleaned up ${expiredCount} expired rate limit entries`, logContext); } } /** * Updates the configuration of the rate limiter instance. * @param config - New configuration options to merge. */ configure(config) { this.config = { ...this.config, ...config }; if (config.cleanupInterval !== undefined) { this.startCleanupTimer(); } } /** * Retrieves a copy of the current rate limiter configuration. * @returns The current configuration. */ getConfig() { return { ...this.config }; } /** * Resets all rate limits by clearing the internal store. */ reset() { this.limits.clear(); const logContext = requestContextService.createRequestContext({ operation: "RateLimiter.reset", }); logger.debug("Rate limiter reset, all limits cleared", logContext); } /** * Checks if a request exceeds the configured rate limit. * Throws an `McpError` if the limit is exceeded. * * @param key - A unique identifier for the request source. * @param context - Optional request context for custom key generation. * @throws {McpError} If the rate limit is exceeded. */ check(key, context) { if (this.config.skipInDevelopment && environment === "development") { return; } const limitKey = this.config.keyGenerator ? this.config.keyGenerator(key, context) : key; const now = Date.now(); const entry = this.limits.get(limitKey); if (!entry || now >= entry.resetTime) { this.limits.set(limitKey, { count: 1, resetTime: now + this.config.windowMs, }); return; } if (entry.count >= this.config.maxRequests) { const waitTime = Math.ceil((entry.resetTime - now) / 1000); const errorMessage = (this.config.errorMessage || RateLimiter.DEFAULT_CONFIG.errorMessage).replace("{waitTime}", waitTime.toString()); throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, { waitTimeSeconds: waitTime, key: limitKey, limit: this.config.maxRequests, windowMs: this.config.windowMs, }); } entry.count++; } /** * Retrieves the current rate limit status for a specific key. * @param key - The rate limit key. * @returns Status object or `null` if no entry 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, }; } /** * Stops the cleanup timer and clears all rate limit entries. * Call when the rate limiter is no longer needed. */ dispose() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } this.limits.clear(); } } /** * Default configuration values. * @private */ RateLimiter.DEFAULT_CONFIG = { windowMs: 15 * 60 * 1000, // 15 minutes maxRequests: 100, errorMessage: "Rate limit exceeded. Please try again in {waitTime} seconds.", skipInDevelopment: false, cleanupInterval: 5 * 60 * 1000, // 5 minutes }; /** * Default singleton instance of the `RateLimiter`. * Initialized with default configuration. Use `rateLimiter.configure({})` to customize. */ export const rateLimiter = new RateLimiter({ windowMs: 15 * 60 * 1000, // Default: 15 minutes maxRequests: 100, // Default: 100 requests per window });