Skip to main content
Glama

1MCP Server

loadingStateTracker.ts9.1 kB
import { EventEmitter } from 'events'; import logger, { debugIf } from '@src/logger/logger.js'; import { secureLogger } from '@src/logger/secureLogger.js'; /** * Enum representing possible MCP server loading states */ export enum LoadingState { /** Server initialization not started */ Pending = 'pending', /** Server is currently initializing */ Loading = 'loading', /** Server successfully initialized and connected */ Ready = 'ready', /** Server failed to initialize */ Failed = 'failed', /** Server requires OAuth authorization */ AwaitingOAuth = 'awaiting_oauth', /** Server initialization was cancelled */ Cancelled = 'cancelled', } /** * Detailed information about a server's loading progress */ export interface ServerLoadingInfo { readonly name: string; readonly state: LoadingState; startTime?: Date; endTime?: Date; duration?: number; // milliseconds readonly error?: Error; readonly authorizationUrl?: string; oauthStartTime?: Date; readonly retryCount: number; readonly lastRetryTime?: Date; readonly progress?: { readonly phase: string; readonly message?: string; }; } /** * Overall loading statistics and summary */ export interface LoadingSummary { readonly totalServers: number; readonly pending: number; readonly loading: number; readonly ready: number; readonly failed: number; readonly awaitingOAuth: number; readonly cancelled: number; readonly startTime: Date; readonly isComplete: boolean; readonly successRate: number; // percentage readonly averageLoadTime?: number; // milliseconds } /** * Events emitted by LoadingStateTracker */ export const enum LoadingStateEvent { ServerStateChanged = 'server-state-changed', LoadingProgress = 'loading-progress', LoadingComplete = 'loading-complete', ServerReady = 'server-ready', ServerFailed = 'server-failed', OAuthRequired = 'oauth-required', } export interface LoadingStateEvents { [LoadingStateEvent.ServerStateChanged]: (name: string, info: ServerLoadingInfo) => void; [LoadingStateEvent.LoadingProgress]: (summary: LoadingSummary) => void; [LoadingStateEvent.LoadingComplete]: (summary: LoadingSummary) => void; [LoadingStateEvent.ServerReady]: (name: string, info: ServerLoadingInfo) => void; [LoadingStateEvent.ServerFailed]: (name: string, info: ServerLoadingInfo) => void; [LoadingStateEvent.OAuthRequired]: (name: string, info: ServerLoadingInfo) => void; } /** * Tracks the loading state of MCP servers during async initialization * * This class provides real-time visibility into server initialization progress, * enabling the HTTP server to start immediately while MCP servers load in the background. * * @example * ```typescript * const tracker = new LoadingStateTracker(); * tracker.on(LoadingStateEvent.ServerReady, (name) => console.log(`${name} is ready`)); * tracker.on(LoadingStateEvent.LoadingComplete, (summary) => console.log('All done!', summary)); * * tracker.startLoading(['server1', 'server2']); * tracker.updateServerState('server1', LoadingState.Loading, { phase: 'connecting' }); * tracker.updateServerState('server1', LoadingState.Ready); * ``` */ export class LoadingStateTracker extends EventEmitter { private servers: Map<string, ServerLoadingInfo> = new Map(); private globalStartTime: Date = new Date(); private loadingStarted: boolean = false; constructor() { super(); this.setMaxListeners(50); // Allow many listeners for server events } /** * Initialize tracking for a list of servers */ public startLoading(serverNames: string[]): void { this.loadingStarted = true; this.globalStartTime = new Date(); for (const name of serverNames) { this.servers.set(name, { name, state: LoadingState.Pending, retryCount: 0, }); } logger.info(`Started tracking loading for ${serverNames.length} servers`); this.emitProgress(); } /** * Update the state of a specific server */ public updateServerState( name: string, state: LoadingState, updates: Partial<Pick<ServerLoadingInfo, 'error' | 'authorizationUrl' | 'oauthStartTime' | 'progress'>> = {}, ): void { const existing = this.servers.get(name); if (!existing) { logger.warn(`Attempted to update unknown server: ${name}`); return; } const now = new Date(); const info: ServerLoadingInfo = { ...existing, state, ...updates, }; // Handle state-specific updates switch (state) { case LoadingState.Loading: if (!info.startTime) { info.startTime = now; } break; case LoadingState.Ready: case LoadingState.Failed: case LoadingState.Cancelled: info.endTime = now; if (info.startTime) { info.duration = now.getTime() - info.startTime.getTime(); } break; case LoadingState.AwaitingOAuth: if (!info.oauthStartTime) { info.oauthStartTime = now; } break; } this.servers.set(name, info); const stateDescription = state === LoadingState.AwaitingOAuth ? 'requiring authorization' : state; secureLogger.debug( `Server ${name} state changed to ${stateDescription}${updates.progress ? ` (${updates.progress.phase})` : ''}`, ); // Emit specific events this.emit(LoadingStateEvent.ServerStateChanged, name, info); switch (state) { case LoadingState.Ready: this.emit(LoadingStateEvent.ServerReady, name, info); break; case LoadingState.Failed: this.emit(LoadingStateEvent.ServerFailed, name, info); break; case LoadingState.AwaitingOAuth: this.emit(LoadingStateEvent.OAuthRequired, name, info); break; } this.emitProgress(); } /** * Increment retry count for a server */ public incrementRetryCount(name: string): void { const info = this.servers.get(name); if (info) { const updated: ServerLoadingInfo = { ...info, retryCount: info.retryCount + 1, lastRetryTime: new Date(), }; this.servers.set(name, updated); debugIf(() => ({ message: `Server ${name} retry count: ${updated.retryCount}` })); } } /** * Get current state of a specific server */ public getServerState(name: string): ServerLoadingInfo | undefined { return this.servers.get(name); } /** * Get all server states */ public getAllServerStates(): Map<string, ServerLoadingInfo> { return new Map(this.servers); } /** * Get current loading summary */ public getSummary(): LoadingSummary { const states = Array.from(this.servers.values()); const pending = states.filter((s) => s.state === LoadingState.Pending).length; const loading = states.filter((s) => s.state === LoadingState.Loading).length; const ready = states.filter((s) => s.state === LoadingState.Ready).length; const failed = states.filter((s) => s.state === LoadingState.Failed).length; const awaitingOAuth = states.filter((s) => s.state === LoadingState.AwaitingOAuth).length; const cancelled = states.filter((s) => s.state === LoadingState.Cancelled).length; const completedStates = states.filter( (s) => s.state === LoadingState.Ready || s.state === LoadingState.Failed || s.state === LoadingState.Cancelled, ); const isComplete = pending === 0 && loading === 0; const successRate = states.length > 0 ? (ready / states.length) * 100 : 0; const averageLoadTime = completedStates.length > 0 ? completedStates.filter((s) => s.duration !== undefined).reduce((sum, s) => sum + (s.duration || 0), 0) / completedStates.length : undefined; return { totalServers: states.length, pending, loading, ready, failed, awaitingOAuth, cancelled, startTime: this.globalStartTime, isComplete, successRate, averageLoadTime, }; } /** * Check if loading is complete (all servers in final state) */ public isLoadingComplete(): boolean { return this.getSummary().isComplete; } /** * Get servers in a specific state */ public getServersByState(state: LoadingState): ServerLoadingInfo[] { return Array.from(this.servers.values()).filter((s) => s.state === state); } /** * Reset all tracking state */ public reset(): void { this.servers.clear(); this.loadingStarted = false; this.globalStartTime = new Date(); logger.info('Loading state tracker reset'); } /** * Emit progress event with current summary */ private emitProgress(): void { if (!this.loadingStarted) return; const summary = this.getSummary(); this.emit(LoadingStateEvent.LoadingProgress, summary); if (summary.isComplete) { logger.info( `Loading complete: ${summary.ready}/${summary.totalServers} servers ready (${summary.successRate.toFixed(1)}% success rate)`, ); this.emit(LoadingStateEvent.LoadingComplete, summary); } } }

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/1mcp-app/agent'

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