Skip to main content
Glama
utils.ts•7.98 kB
/** * Utility functions for logging, rate limiting, and other common operations */ // Simple in-memory rate limiter interface RateLimitEntry { count: number; resetTime: number; } /** * Simple rate limiter implementation */ export class RateLimiter { private limits = new Map<string, RateLimitEntry>(); private readonly maxRequests: number; private readonly windowMs: number; constructor(maxRequests = 10, windowMs = 60000) { // 10 requests per minute by default this.maxRequests = maxRequests; this.windowMs = windowMs; } /** * Extract client identifier from request context * @param req - Express request object or similar context * @returns Unique client identifier */ static extractClientId(req?: { headers?: Record<string, string | string[] | undefined>; ip?: string; socket?: { remoteAddress?: string }; }): string { if (!req) { return 'default'; } // Try to get client ID from headers first const clientIdHeader = req.headers?.['x-client-id'] || req.headers?.['mcp-client-id']; if (clientIdHeader && typeof clientIdHeader === 'string') { return clientIdHeader; } // Fall back to IP address const ip = req.ip || req.socket?.remoteAddress || 'unknown'; return `ip:${ip}`; } /** * Check if a client has exceeded the rate limit * @param clientId - Unique identifier for the client * @returns true if request is allowed, false if rate limited */ checkLimit(clientId: string): boolean { const now = Date.now(); const entry = this.limits.get(clientId); if (!entry || now > entry.resetTime) { // First request or window has reset this.limits.set(clientId, { count: 1, resetTime: now + this.windowMs, }); return true; } if (entry.count >= this.maxRequests) { return false; // Rate limited } // Increment count entry.count++; return true; } /** * Get current usage for a client * @param clientId - Unique identifier for the client * @returns Current request count and reset time */ getUsage(clientId: string): { count: number; resetTime: number; remaining: number; } { const entry = this.limits.get(clientId); if (!entry || Date.now() > entry.resetTime) { return { count: 0, resetTime: Date.now() + this.windowMs, remaining: this.maxRequests, }; } return { count: entry.count, resetTime: entry.resetTime, remaining: Math.max(0, this.maxRequests - entry.count), }; } /** * Clear rate limit data for a client * @param clientId - Unique identifier for the client */ clearLimit(clientId: string): void { this.limits.delete(clientId); } /** * Clean up expired entries */ cleanup(): void { const now = Date.now(); for (const [clientId, entry] of this.limits.entries()) { if (now > entry.resetTime) { this.limits.delete(clientId); } } } } // Create a global rate limiter instance export const rateLimiter = new RateLimiter(); // Set up periodic cleanup setInterval(() => { rateLimiter.cleanup(); }, 60000); // Clean up every minute /** * Log levels */ export enum LogLevel { ERROR = 0, WARN = 1, INFO = 2, DEBUG = 3, } /** * Logger interface */ export interface Logger { error(message: string, meta?: unknown): void; warn(message: string, meta?: unknown): void; info(message: string, meta?: unknown): void; debug(message: string, meta?: unknown): void; } /** * Simple structured logger implementation */ class SimpleLogger implements Logger { private readonly name: string; private readonly level: LogLevel; constructor(name: string, level: LogLevel = LogLevel.INFO) { this.name = name; this.level = level; } private log( level: LogLevel, levelName: string, message: string, meta?: unknown, ): void { if (level <= this.level) { const timestamp = new Date().toISOString(); const logEntry: Record<string, unknown> = { timestamp, level: levelName, logger: this.name, message, }; if (meta !== undefined) { logEntry['meta'] = meta; } // Use stderr for error/warn, stdout for info/debug const output = level <= LogLevel.WARN ? console.error : console.log; output(JSON.stringify(logEntry)); } } error(message: string, meta?: unknown): void { this.log(LogLevel.ERROR, 'ERROR', message, meta); } warn(message: string, meta?: unknown): void { this.log(LogLevel.WARN, 'WARN', message, meta); } info(message: string, meta?: unknown): void { this.log(LogLevel.INFO, 'INFO', message, meta); } debug(message: string, meta?: unknown): void { this.log(LogLevel.DEBUG, 'DEBUG', message, meta); } } /** * Create a logger instance * @param name - Logger name * @param level - Log level (defaults to INFO, or DEBUG if NODE_ENV is development) * @returns Logger instance */ export function createLogger(name: string, level?: LogLevel): Logger { // Check for LOG_LEVEL environment variable first let defaultLevel = LogLevel.INFO; if (process.env['LOG_LEVEL']) { const envLevel = parseInt(process.env['LOG_LEVEL'], 10); if ( !isNaN(envLevel) && envLevel >= LogLevel.ERROR && envLevel <= LogLevel.DEBUG ) { defaultLevel = envLevel; } } else if (process.env['NODE_ENV'] === 'development') { defaultLevel = LogLevel.DEBUG; } return new SimpleLogger(name, level ?? defaultLevel); } /** * Utility function to safely stringify objects for logging * @param obj - Object to stringify * @returns Safely stringified object */ export function safeStringify(obj: unknown): string { try { return JSON.stringify(obj, null, 2); } catch (error) { return `[Unstringifiable object: ${error instanceof Error ? error.message : 'Unknown error'}]`; } } /** * Utility function to redact sensitive information from objects * @param obj - Object to redact * @param sensitiveKeys - Array of keys to redact * @returns Object with sensitive values redacted */ export function redactSensitiveInfo( obj: Record<string, unknown>, sensitiveKeys: string[] = ['password', 'secret', 'token', 'key'], ): Record<string, unknown> { const redacted = { ...obj }; for (const key of Object.keys(redacted)) { if ( sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive.toLowerCase()), ) ) { redacted[key] = '[REDACTED]'; } } return redacted; } /** * Utility function to validate and parse environment variables * @param key - Environment variable key * @param defaultValue - Default value if not set * @param required - Whether the variable is required * @returns Parsed value */ export function getEnvVar( key: string, defaultValue?: string, required = false, ): string { const value = process.env[key] || defaultValue; if (required && !value) { throw new Error(`Required environment variable ${key} is not set`); } return value || ''; } /** * Utility function to parse boolean environment variables * @param key - Environment variable key * @param defaultValue - Default value if not set * @returns Boolean value */ export function getEnvBool(key: string, defaultValue = false): boolean { const value = process.env[key]; if (!value) { return defaultValue; } return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()); } /** * Utility function to parse numeric environment variables * @param key - Environment variable key * @param defaultValue - Default value if not set * @returns Numeric value */ export function getEnvNumber(key: string, defaultValue = 0): number { const value = process.env[key]; if (!value) { return defaultValue; } const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed; }

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/rlaput/azure-logs-mcp'

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