Skip to main content
Glama

CTS MCP Server

by EricA1019
index.ts9.78 kB
/** * Observability Module - Structured Logging and Metrics * * Provides: * - Structured logging with severity levels * - Performance metrics tracking * - Tool execution monitoring * - Cache statistics logging * - Error tracking and reporting */ export enum LogLevel { DEBUG = 'debug', INFO = 'info', WARN = 'warn', ERROR = 'error', } export interface LogContext { toolName?: string; operation?: string; duration?: number; cacheHit?: boolean; errorCode?: string; [key: string]: any; } export interface MetricData { name: string; value: number; unit: string; timestamp: number; tags?: Record<string, string>; } export interface ToolMetrics { toolName: string; executionCount: number; totalDuration: number; averageDuration: number; minDuration: number; maxDuration: number; errorCount: number; cacheHitRate: number; lastExecuted: number; } /** * Logger with structured output */ export class Logger { private minLevel: LogLevel; private context: Record<string, any>; constructor(minLevel: LogLevel = LogLevel.INFO, context: Record<string, any> = {}) { this.minLevel = this.getLogLevelFromEnv() || minLevel; this.context = context; } private getLogLevelFromEnv(): LogLevel | null { const level = process.env.LOG_LEVEL?.toLowerCase(); if (level && Object.values(LogLevel).includes(level as LogLevel)) { return level as LogLevel; } return null; } private shouldLog(level: LogLevel): boolean { const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]; return levels.indexOf(level) >= levels.indexOf(this.minLevel); } private formatLog(level: LogLevel, message: string, context?: LogContext): string { const timestamp = new Date().toISOString(); const mergedContext = { ...this.context, ...context }; const logData = { timestamp, level, message, ...mergedContext, }; // JSON format for machine parsing if (process.env.LOG_FORMAT === 'json') { return JSON.stringify(logData); } // Human-readable format with context const hasContext = Object.keys(mergedContext).length > 0; const contextStr = hasContext ? ` ${JSON.stringify(mergedContext)}` : ''; return `[${timestamp}] ${level.toUpperCase()} ${message}${contextStr}`; } debug(message: string, context?: LogContext): void { if (this.shouldLog(LogLevel.DEBUG)) { console.debug(this.formatLog(LogLevel.DEBUG, message, context)); } } info(message: string, context?: LogContext): void { if (this.shouldLog(LogLevel.INFO)) { console.info(this.formatLog(LogLevel.INFO, message, context)); } } warn(message: string, context?: LogContext): void { if (this.shouldLog(LogLevel.WARN)) { console.warn(this.formatLog(LogLevel.WARN, message, context)); } } error(message: string, context?: LogContext): void { if (this.shouldLog(LogLevel.ERROR)) { console.error(this.formatLog(LogLevel.ERROR, message, context)); } } child(context: Record<string, any>): Logger { return new Logger(this.minLevel, { ...this.context, ...context }); } } /** * Metrics collector */ export class MetricsCollector { private metrics: Map<string, MetricData[]> = new Map(); private toolMetrics: Map<string, ToolMetrics> = new Map(); private enabled: boolean; constructor(enabled: boolean = true) { this.enabled = enabled; } recordMetric(name: string, value: number, unit: string = '', tags?: Record<string, string>): void { if (!this.enabled) return; const metric: MetricData = { name, value, unit, timestamp: Date.now(), tags, }; if (!this.metrics.has(name)) { this.metrics.set(name, []); } this.metrics.get(name)!.push(metric); // Keep only last 1000 metrics per name to prevent memory leaks const metrics = this.metrics.get(name)!; if (metrics.length > 1000) { metrics.shift(); } } recordToolExecution( toolName: string, duration: number, success: boolean, cacheHit: boolean = false ): void { if (!this.enabled) return; let metrics = this.toolMetrics.get(toolName); if (!metrics) { metrics = { toolName, executionCount: 0, totalDuration: 0, averageDuration: 0, minDuration: Infinity, maxDuration: 0, errorCount: 0, cacheHitRate: 0, lastExecuted: 0, }; this.toolMetrics.set(toolName, metrics); } metrics.executionCount++; metrics.totalDuration += duration; metrics.averageDuration = metrics.totalDuration / metrics.executionCount; metrics.minDuration = Math.min(metrics.minDuration, duration); metrics.maxDuration = Math.max(metrics.maxDuration, duration); if (!success) metrics.errorCount++; metrics.lastExecuted = Date.now(); // Update cache hit rate const cacheHits = cacheHit ? 1 : 0; const totalRequests = metrics.executionCount; metrics.cacheHitRate = (metrics.cacheHitRate * (totalRequests - 1) + cacheHits) / totalRequests; // Record as time-series metric this.recordMetric(`tool.${toolName}.duration`, duration, 'ms', { success: success.toString(), cacheHit: cacheHit.toString(), }); } getMetrics(name: string): MetricData[] { return this.metrics.get(name) || []; } getToolMetrics(toolName?: string): ToolMetrics | ToolMetrics[] { if (toolName) { return this.toolMetrics.get(toolName) || this.createEmptyToolMetrics(toolName); } return Array.from(this.toolMetrics.values()); } getAllMetrics(): Record<string, MetricData[]> { const result: Record<string, MetricData[]> = {}; this.metrics.forEach((value, key) => { result[key] = value; }); return result; } getSummary(): { totalTools: number; totalExecutions: number; totalErrors: number; averageCacheHitRate: number; toolSummaries: ToolMetrics[]; } { const tools = Array.from(this.toolMetrics.values()); const totalExecutions = tools.reduce((sum, t) => sum + t.executionCount, 0); const totalErrors = tools.reduce((sum, t) => sum + t.errorCount, 0); const avgCacheHitRate = tools.reduce((sum, t) => sum + t.cacheHitRate, 0) / (tools.length || 1); return { totalTools: tools.length, totalExecutions, totalErrors, averageCacheHitRate: avgCacheHitRate, toolSummaries: tools, }; } reset(): void { this.metrics.clear(); this.toolMetrics.clear(); } private createEmptyToolMetrics(toolName: string): ToolMetrics { return { toolName, executionCount: 0, totalDuration: 0, averageDuration: 0, minDuration: 0, maxDuration: 0, errorCount: 0, cacheHitRate: 0, lastExecuted: 0, }; } } /** * Global logger instance */ export const logger = new Logger( process.env.NODE_ENV === 'development' ? LogLevel.DEBUG : LogLevel.INFO, { service: 'cts-mcp-server', version: '3.0.0' } ); /** * Global metrics collector */ export const metrics = new MetricsCollector(true); /** * Performance monitoring decorator */ export function monitored(toolName: string) { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { const startTime = performance.now(); const log = logger.child({ toolName, operation: propertyKey }); log.debug(`Starting ${propertyKey}`, { args: args.length }); try { const result = await originalMethod.apply(this, args); const duration = performance.now() - startTime; metrics.recordToolExecution(toolName, duration, true); log.info(`Completed ${propertyKey}`, { duration }); return result; } catch (error) { const duration = performance.now() - startTime; metrics.recordToolExecution(toolName, duration, false); log.error(`Failed ${propertyKey}`, { duration, error: error instanceof Error ? error.message : String(error), }); throw error; } }; return descriptor; }; } /** * Export metrics in Prometheus format */ export function exportPrometheusMetrics(): string { const summary = metrics.getSummary(); const lines: string[] = []; // Help and type declarations lines.push('# HELP cts_tool_executions_total Total number of tool executions'); lines.push('# TYPE cts_tool_executions_total counter'); lines.push('# HELP cts_tool_duration_seconds Tool execution duration in seconds'); lines.push('# TYPE cts_tool_duration_seconds gauge'); lines.push('# HELP cts_tool_errors_total Total number of tool errors'); lines.push('# TYPE cts_tool_errors_total counter'); lines.push('# HELP cts_tool_cache_hit_rate Cache hit rate for tool'); lines.push('# TYPE cts_tool_cache_hit_rate gauge'); // Metrics for each tool summary.toolSummaries.forEach(tool => { lines.push(`cts_tool_executions_total{tool="${tool.toolName}"} ${tool.executionCount}`); lines.push(`cts_tool_duration_seconds{tool="${tool.toolName}",stat="avg"} ${(tool.averageDuration / 1000).toFixed(6)}`); lines.push(`cts_tool_duration_seconds{tool="${tool.toolName}",stat="min"} ${(tool.minDuration / 1000).toFixed(6)}`); lines.push(`cts_tool_duration_seconds{tool="${tool.toolName}",stat="max"} ${(tool.maxDuration / 1000).toFixed(6)}`); lines.push(`cts_tool_errors_total{tool="${tool.toolName}"} ${tool.errorCount}`); lines.push(`cts_tool_cache_hit_rate{tool="${tool.toolName}"} ${tool.cacheHitRate.toFixed(4)}`); }); return lines.join('\n'); }

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/EricA1019/CTS_MCP'

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