Skip to main content
Glama

MCP Bridge Server

ratelimit.js7.25 kB
import { SecurityError, SecurityErrorType } from './types.js'; /** * Memory-based rate limit storage */ class MemoryStorage { constructor() { this.storage = new Map(); } async get(key) { return this.storage.get(key) || null; } async set(key, state) { this.storage.set(key, state); } async delete(key) { this.storage.delete(key); } async cleanup() { const now = new Date(); for (const [key, state] of this.storage.entries()) { if (now > state.resetAt) { this.storage.delete(key); } } } } /** * Rate limiter * Handles request rate limiting based on configured rules */ export class RateLimiter { constructor(options) { this.enabled = options.enabled; this.rules = options.rules; this.storage = this.createStorage(options.storage); } /** * Initialize rate limiter */ async initialize() { if (!this.enabled) { return; } // Start cleanup interval this.cleanupInterval = setInterval(() => this.storage.cleanup(), 60 * 1000 // Every minute ); } /** * Check if request is allowed */ async checkLimit(resource, context) { try { if (!this.enabled) { return { allowed: true, remaining: Infinity, resetAt: new Date(Date.now() + 3600000) }; } // Get applicable rules const rules = this.getApplicableRules(resource, context); if (rules.length === 0) { return { allowed: true, remaining: Infinity, resetAt: new Date(Date.now() + 3600000) }; } // Check each rule const results = await Promise.all(rules.map(rule => this.checkRule(rule, context))); // Find most restrictive result const result = results.reduce((prev, curr) => { if (!prev) return curr; if (!curr) return prev; return curr.remaining < prev.remaining ? curr : prev; }); return { allowed: result.allowed, remaining: result.remaining, resetAt: result.resetAt }; } catch (error) { throw new SecurityError(SecurityErrorType.RATE_LIMIT_FAILED, 'Failed to check rate limit', error); } } /** * Record request */ async recordRequest(resource, context) { try { if (!this.enabled) { return; } // Get applicable rules const rules = this.getApplicableRules(resource, context); if (rules.length === 0) { return; } // Update state for each rule await Promise.all(rules.map(rule => this.updateRuleState(rule, context))); } catch (error) { throw new SecurityError(SecurityErrorType.RATE_LIMIT_FAILED, 'Failed to record request', error); } } /** * Close rate limiter */ async close() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } } /** * Create storage backend */ createStorage(config) { switch (config.type) { case 'memory': return new MemoryStorage(); case 'redis': throw new SecurityError(SecurityErrorType.INVALID_CONFIG, 'Redis storage not yet implemented'); default: throw new SecurityError(SecurityErrorType.INVALID_CONFIG, 'Invalid storage type'); } } /** * Get applicable rules for resource and context */ getApplicableRules(resource, context) { return this.rules.filter(rule => { // Check resource match if (!this.matchResource(resource, rule.resource)) { return false; } // Check identity match if (rule.userId && rule.userId !== context.userId) { return false; } if (rule.clientId && rule.clientId !== context.clientId) { return false; } if (rule.machineId && rule.machineId !== context.machineId) { return false; } return true; }); } /** * Check if request is allowed by rule */ async checkRule(rule, context) { // Get current state const key = this.getStateKey(rule, context); const state = await this.storage.get(key); // If no state exists, request is allowed if (!state) { return { allowed: true, remaining: rule.limit, resetAt: new Date(Date.now() + rule.window) }; } // Check if window has expired if (new Date() > state.resetAt) { await this.storage.delete(key); return { allowed: true, remaining: rule.limit, resetAt: new Date(Date.now() + rule.window) }; } // Check remaining limit return { allowed: state.count < rule.limit, remaining: Math.max(0, rule.limit - state.count), resetAt: state.resetAt }; } /** * Update rule state for request */ async updateRuleState(rule, context) { const key = this.getStateKey(rule, context); const state = await this.storage.get(key); if (!state || new Date() > state.resetAt) { // Create new state await this.storage.set(key, { resource: rule.resource, userId: context.userId, clientId: context.clientId, machineId: context.machineId, count: 1, window: rule.window, resetAt: new Date(Date.now() + rule.window) }); } else { // Update existing state await this.storage.set(key, { ...state, count: state.count + 1 }); } } /** * Get storage key for rule state */ getStateKey(rule, context) { return [ rule.resource, context.userId || '*', context.clientId || '*', context.machineId || '*' ].join(':'); } /** * Match resource against pattern */ matchResource(resource, pattern) { // Convert glob pattern to regex const regex = new RegExp('^' + pattern .replace(/\*/g, '.*') .replace(/\?/g, '.') .replace(/\[!/g, '[^') .replace(/\[/g, '[') .replace(/\]/g, ']') .replace(/\./g, '\\.') + '$'); return regex.test(resource); } } //# sourceMappingURL=ratelimit.js.map

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/glassBead-tc/SubspaceDomain'

If you have feedback or need assistance with the MCP directory API, please join our Discord server