Skip to main content
Glama
MCPServerInitializer.ts11.4 kB
import { Logger } from '../utils/logger.js'; import { StateManager } from '../components/StateManager.js'; import { HealthChecker } from '../components/HealthChecker.js'; import { HealthEndpoint } from '../components/HealthEndpoint.js'; import { ConfigValidator, ServerConfig } from '../config/HealthEndpointConfig.js'; type ServiceStatus = 'pending' | 'initializing' | 'ready' | 'failed'; type ServiceName = 'stateManager' | 'healthChecker' | 'healthEndpoint' | 'projectContext'; /** * MCPツールとその依存サービスのマッピング定義 */ export const SERVICE_DEPENDENCIES = { 'scan_project_dirs': ['projectContext'], 'start_dev_server': ['stateManager'], 'get_dev_status': ['stateManager'], 'get_dev_logs': ['stateManager'], 'stop_dev_server': ['stateManager'], 'restart_dev_server': ['stateManager'], 'get_health_status': ['healthChecker'], 'recover_from_state': ['stateManager'], 'auto_recover': ['stateManager', 'healthChecker'] } as const; /** * MCPサーバーの段階的初期化を管理するクラス * * JSON-RPC通信の確立を最優先とし、重い初期化処理を背景で非同期実行する。 * サービス間の依存関係を管理し、ツール実行時に必要なサービスが初期化済みかを確認する。 */ export class MCPServerInitializer { private serviceStatus = new Map<ServiceName, ServiceStatus>(); private servicePromises = new Map<ServiceName, Promise<void>>(); private dependencyCheckPromises = new Map<string, Promise<void>>(); private logger: Logger; private config: ServerConfig; /** * MCPServerInitializerのコンストラクタ * * 環境変数から設定を読み込み、全サービスを初期状態に設定する。 * @throws {Error} 設定validation失敗時 */ constructor() { this.logger = Logger.getInstance(); try { this.config = ConfigValidator.validateServerConfig(process.env); } catch (error) { this.logger.error('Configuration validation failed', { error }); throw error; } // 全サービスを初期状態に設定 const services: ServiceName[] = ['stateManager', 'healthChecker', 'healthEndpoint', 'projectContext']; services.forEach(service => { this.serviceStatus.set(service, 'pending'); }); } /** * 指定されたサービスを初期化する * * @param name - 初期化するサービス名 * @param initFn - 初期化処理を行う関数 * @returns 初期化完了のPromise * @throws {Error} 初期化処理でエラーが発生した場合 */ async initializeService(name: ServiceName, initFn: () => Promise<void>): Promise<void> { if (this.serviceStatus.get(name) === 'ready') { return; } // 既に初期化中の場合は既存のPromiseを返す if (this.servicePromises.has(name)) { return this.servicePromises.get(name)!; } this.serviceStatus.set(name, 'initializing'); const startTime = Date.now(); const initPromise = (async () => { try { await initFn(); this.serviceStatus.set(name, 'ready'); this.logger.info(`Service initialized: ${name} (${Date.now() - startTime}ms)`); } catch (error) { this.serviceStatus.set(name, 'failed'); this.logger.error(`Service initialization failed: ${name}`, { error, duration: Date.now() - startTime }); throw error; } finally { this.servicePromises.delete(name); } })(); this.servicePromises.set(name, initPromise); return initPromise; } /** * 指定されたサービスの初期化完了を待機する * * @param name - 待機するサービス名 * @param timeoutMs - タイムアウト時間(ミリ秒)、デフォルトは設定値 * @returns サービス初期化完了のPromise * @throws {Error} サービス初期化失敗またはタイムアウト時 */ async waitForService(name: ServiceName, timeoutMs: number = this.config.dependencyTimeout): Promise<void> { const status = this.serviceStatus.get(name); if (status === 'ready') return; if (status === 'failed') { throw new Error(`Service failed: ${name}`); } // 初期化中の場合は既存のPromiseを待機 if (this.servicePromises.has(name)) { const promise = this.servicePromises.get(name)!; return Promise.race([ promise, new Promise<never>((_, reject) => { setTimeout(() => reject(new Error(`Service initialization timeout: ${name}`)), timeoutMs); }) ]); } return new Promise((resolve, reject) => { const abortController = new AbortController(); let pollingTimeout: NodeJS.Timeout | null = null; const timeout = setTimeout(() => { abortController.abort(); if (pollingTimeout) clearTimeout(pollingTimeout); reject(new Error(`Service initialization timeout: ${name}`)); }, timeoutMs); const checkStatus = () => { if (abortController.signal.aborted) return; const currentStatus = this.serviceStatus.get(name); if (currentStatus === 'ready') { clearTimeout(timeout); if (pollingTimeout) clearTimeout(pollingTimeout); resolve(); } else if (currentStatus === 'failed') { clearTimeout(timeout); if (pollingTimeout) clearTimeout(pollingTimeout); reject(new Error(`Service failed: ${name}`)); } else { pollingTimeout = setTimeout(checkStatus, this.config.pollingInterval); } }; checkStatus(); }); } /** * 指定されたサービスが初期化完了しているかチェック * * @param name - チェックするサービス名 * @returns サービスが準備完了の場合true */ isServiceReady(name: ServiceName): boolean { return this.serviceStatus.get(name) === 'ready'; } getServiceStatuses(): Map<ServiceName, ServiceStatus> { return new Map(this.serviceStatus); } /** * 全サービスの背景初期化を開始する * * 各サービスを並行で初期化し、失敗したサービスがあっても他の初期化を継続する。 * @returns 背景初期化完了のPromise */ async startBackgroundInitialization(): Promise<void> { this.logger.info('Starting background service initialization'); const initTasks = [ this.initializeService('stateManager', async () => { const stateManager = StateManager.getInstance(); await stateManager.initialize(); }), this.initializeService('healthChecker', async () => { const healthChecker = HealthChecker.getInstance(); healthChecker.startPeriodicHealthCheck(this.config.healthCheckInterval); }), this.initializeService('healthEndpoint', async () => { const healthEndpoint = HealthEndpoint.getInstance(this.config.healthEndpoint); if (this.config.healthEndpoint.enabled) { try { await healthEndpoint.start(); this.logger.info('Health endpoint started', { url: `http://${this.config.healthEndpoint.host}:${this.config.healthEndpoint.port}${this.config.healthEndpoint.path}` }); } catch (error) { this.logger.warn('Failed to start health endpoint', { error }); throw error; } } }), this.initializeService('projectContext', async () => { // ProjectContextManagerは既にmain()で初期化済み // 追加の初期化処理が必要な場合はここで実行 this.logger.debug('Project context already initialized in main()'); }) ]; // 並行初期化(一つが失敗しても他は続行) const results = await Promise.allSettled(initTasks); // 結果をログ出力 const serviceNames: ServiceName[] = ['stateManager', 'healthChecker', 'healthEndpoint', 'projectContext']; results.forEach((result, index) => { const serviceName = serviceNames[index]; if (result.status === 'rejected') { this.logger.warn(`Background service initialization failed: ${serviceName}`, { error: result.reason }); } else { this.logger.debug(`Background service initialized: ${serviceName}`); } }); const successCount = results.filter(r => r.status === 'fulfilled').length; this.logger.info(`Background initialization completed: ${successCount}/${results.length} services ready`); } /** * ツール実行に必要な依存サービスの初期化完了を確認する * * 競合状態を防ぐため、同じツールの依存チェックが実行中の場合は既存のPromiseを返す。 * @param toolName - 実行するツール名 * @returns 依存サービス準備完了のPromise * @throws {Error} 依存サービスの初期化に失敗した場合 */ async ensureToolDependencies(toolName: keyof typeof SERVICE_DEPENDENCIES): Promise<void> { const cacheKey = `tool:${toolName}`; // 既に同じツールの依存関係チェックが実行中の場合は、そのPromiseを返す if (this.dependencyCheckPromises.has(cacheKey)) { return this.dependencyCheckPromises.get(cacheKey)!; } const dependencies = SERVICE_DEPENDENCIES[toolName] || []; const checkPromise = (async () => { try { // 依存関係を並行でチェック(順序依存がない場合) const dependencyChecks = dependencies.map(dep => this.waitForService(dep as ServiceName).catch(error => { throw new Error(`Tool ${toolName} requires ${dep} service, but initialization failed: ${error instanceof Error ? error.message : String(error)}`); }) ); await Promise.all(dependencyChecks); this.logger.debug(`Tool ${toolName} dependencies satisfied`); } finally { // 完了後はキャッシュから削除 this.dependencyCheckPromises.delete(cacheKey); } })(); this.dependencyCheckPromises.set(cacheKey, checkPromise); return checkPromise; } /** * デバッグ用: 全サービスの現在の初期化状態を取得 * * @returns サービス名と状態のマッピング */ getInitializationStatus(): Record<string, string> { const status: Record<string, string> = {}; this.serviceStatus.forEach((value, key) => { status[key] = value; }); return status; } /** * 将来拡張: サービスヘルスモニタリング機能 * * 初期化状態だけでなく、実行時のサービス健全性を継続的に監視する機能の設計 * * @param name - チェックするサービス名 * @returns サービスが健全な状態かどうか * * 実装予定機能: * - 定期的なヘルスチェック (ping/pong) * - メモリ使用量監視 * - レスポンス時間測定 * - エラー率の監視 * - 自動復旧メカニズム */ // async isServiceHealthy(name: ServiceName): Promise<boolean> { // // 将来の実装: // // 1. サービスの基本ヘルスチェック // // 2. パフォーマンス指標の確認 // // 3. エラー率の監視 // // 4. 必要に応じた自動復旧トリガー // // return Promise<boolean> // } }

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/masamunet/npm-dev-mcp'

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