Skip to main content
Glama
server.ts9.31 kB
/** * Core MCP Server implementation */ import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; import { ToolRegistry } from './registry.js'; import { MCPRequest, MCPResponse, MCPError, EnvConfig, ToolCallLog, ImageAttachment } from '../types/core.js'; import { MCPException, wrapError } from './errors.js'; export interface ServerConfig { port: number; host: string; logLevel: 'debug' | 'info' | 'warn' | 'error'; sessionTtlMs: number; enableCors: boolean; maxRequestSizeBytes: number; } export interface ServerEvents { 'request': (request: MCPRequest) => void; 'response': (response: MCPResponse) => void; 'error': (error: Error) => void; 'tool_call': (log: ToolCallLog) => void; } export class MCPServer extends EventEmitter { private registry: ToolRegistry; private config: ServerConfig; private envConfig: EnvConfig; private toolCallLogs: ToolCallLog[] = []; private running = false; constructor(config: Partial<ServerConfig> = {}) { super(); this.config = { port: parseInt(process.env.PORT || '3000'), host: process.env.HOST || 'localhost', logLevel: (process.env.LOG_LEVEL as any) || 'info', sessionTtlMs: 24 * 60 * 60 * 1000, // 24 hours enableCors: true, maxRequestSizeBytes: 50 * 1024 * 1024, // 50MB ...config }; this.envConfig = this.loadEnvConfig(); this.registry = new ToolRegistry(); this.setupRegistryEvents(); this.setupCleanupTimer(); } private loadEnvConfig(): EnvConfig { return { SUPABASE_URL: process.env.SUPABASE_URL, SUPABASE_SERVICE_ROLE: process.env.SUPABASE_SERVICE_ROLE, SUPABASE_ANON: process.env.SUPABASE_ANON, RENDER_API_TOKEN: process.env.RENDER_API_TOKEN, RENDER_ACCOUNT_ID: process.env.RENDER_ACCOUNT_ID, VERCEL_TOKEN: process.env.VERCEL_TOKEN, VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID, POSTHOG_API_HOST: process.env.POSTHOG_API_HOST, POSTHOG_PROJECT_KEY: process.env.POSTHOG_PROJECT_KEY, POSTHOG_PERSONAL_API_KEY: process.env.POSTHOG_PERSONAL_API_KEY, WEB_DRIVER_REMOTE_URL: process.env.WEB_DRIVER_REMOTE_URL, GEMINI_API_KEY: process.env.GEMINI_API_KEY, TRACKING_REDACT_RULES_JSON: process.env.TRACKING_REDACT_RULES_JSON, PORT: process.env.PORT, HOST: process.env.HOST, LOG_LEVEL: process.env.LOG_LEVEL }; } private setupRegistryEvents(): void { this.registry.on('tool_registered', (toolName, sessionId) => { this.log('debug', `Tool registered: ${toolName}${sessionId ? ` (session: ${sessionId})` : ''}`); }); this.registry.on('tool_unregistered', (toolName, sessionId) => { this.log('debug', `Tool unregistered: ${toolName}${sessionId ? ` (session: ${sessionId})` : ''}`); }); this.registry.on('session_created', (sessionId, type) => { this.log('info', `Session created: ${sessionId} (${type})`); }); this.registry.on('session_destroyed', (sessionId, type) => { this.log('info', `Session destroyed: ${sessionId} (${type})`); }); } private setupCleanupTimer(): void { // Clean up expired sessions every hour setInterval(() => { const cleaned = this.registry.cleanupExpiredSessions(this.config.sessionTtlMs); if (cleaned > 0) { this.log('info', `Cleaned up ${cleaned} expired sessions`); } }, 60 * 60 * 1000); } /** * Handle an MCP request */ async handleRequest(request: MCPRequest): Promise<MCPResponse> { const startTime = Date.now(); const requestId = request.x_request_id || uuidv4(); this.emit('request', { ...request, x_request_id: requestId }); try { // Handle built-in methods if (request.method === 'tools/list') { const tools = this.registry.getAllTools(); return this.createResponse(request.id, tools, requestId); } if (request.method === 'tools/call') { const { name, arguments: args } = request.params || {}; if (!name) { throw new MCPException('INVALID_ARG', 'Tool name is required'); } const result = await this.executeTool(name, args || {}, requestId); return this.createResponse(request.id, result, requestId); } // Handle session management if (request.method === 'sessions/list') { const sessions = this.registry.getAllSessions(); return this.createResponse(request.id, { sessions }, requestId); } if (request.method === 'sessions/stats') { const stats = this.registry.getStats(); return this.createResponse(request.id, stats, requestId); } if (request.method === 'logs/recent') { const { limit = 100 } = request.params || {}; const logs = this.toolCallLogs .slice(-limit) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); return this.createResponse(request.id, { logs }, requestId); } throw new MCPException('NOT_FOUND', `Unknown method: ${request.method}`); } catch (error) { const duration = Date.now() - startTime; const mcpError = wrapError(error); this.log('error', `Request failed: ${request.method}`, { error: mcpError, duration }); const response: MCPResponse = { jsonrpc: '2.0', id: request.id, error: mcpError, x_request_id: requestId }; this.emit('response', response); return response; } } private async executeTool(name: string, params: any, requestId: string): Promise<any> { const startTime = Date.now(); let result: any; let error: MCPError | undefined; const images: ImageAttachment[] = []; try { result = await this.registry.executeTool(name, params, requestId); // Extract image attachments from result if present if (result && typeof result === 'object') { this.extractImageAttachments(result, images); } } catch (err) { error = wrapError(err); throw err; } finally { const duration = Date.now() - startTime; const log: ToolCallLog = { id: uuidv4(), tool_name: name, params, result: error ? undefined : result, error, timestamp: new Date().toISOString(), duration_ms: duration, images: images.length > 0 ? images : undefined }; // Store log (keep last 1000 entries) this.toolCallLogs.push(log); if (this.toolCallLogs.length > 1000) { this.toolCallLogs.shift(); } this.emit('tool_call', log); } return result; } private extractImageAttachments(obj: any, images: ImageAttachment[]): void { if (!obj || typeof obj !== 'object') return; for (const [key, value] of Object.entries(obj)) { if (key.endsWith('_image_data') && Buffer.isBuffer(value)) { const name = key.replace('_image_data', ''); const mimeType = obj[`${name}_mime_type`] || 'image/png'; const width = obj[`${name}_width`]; const height = obj[`${name}_height`]; images.push({ name: obj[`${name}_image_name`] || `${name}.png`, data: value, mimeType, width, height }); } } } private createResponse<T>(id: string | number, result: T, requestId: string): MCPResponse<T> { const response: MCPResponse<T> = { jsonrpc: '2.0', id, result, x_request_id: requestId }; this.emit('response', response); return response; } private log(level: string, message: string, meta?: any): void { if (this.shouldLog(level)) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, ...meta }; console.log(JSON.stringify(logEntry)); } } private shouldLog(level: string): boolean { const levels = ['debug', 'info', 'warn', 'error']; const currentLevelIndex = levels.indexOf(this.config.logLevel); const messageLevel = levels.indexOf(level); return messageLevel >= currentLevelIndex; } /** * Get the tool registry */ getRegistry(): ToolRegistry { return this.registry; } /** * Get environment configuration */ getEnvConfig(): EnvConfig { return { ...this.envConfig }; } /** * Check if required env vars are present for a service */ checkRequiredEnv(keys: string[]): void { const missing = keys.filter(key => !this.envConfig[key as keyof EnvConfig]); if (missing.length > 0) { throw new MCPException('CONFIG_MISSING', `Missing required environment variables: ${missing.join(', ')}`); } } /** * Get server configuration */ getConfig(): ServerConfig { return { ...this.config }; } /** * Get recent tool call logs */ getRecentLogs(limit = 100): ToolCallLog[] { return this.toolCallLogs .slice(-limit) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); } /** * Check if server is running */ isRunning(): boolean { return this.running; } /** * Mark server as running/stopped */ setRunning(running: boolean): void { this.running = running; } }

Latest Blog Posts

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/JacobFV/mcp-fullstack'

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