UNPKG

@inso_web/els-mcp

Version:

MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.

151 lines 5.96 kB
#!/usr/bin/env node // OpenTelemetry instrumentation должен импортироваться ПЕРВЫМ — // до любых других модулей, которые SDK инструментирует (http/undici/express/ioredis). import { initInstrumentation } from './instrumentation.js'; import pino from 'pino'; import { loadConfig } from './config.js'; import { createMcpServer } from './server.js'; import { connectStdio } from './transports/stdio.js'; import { startHttpServer } from './transports/http-server.js'; import { readProjectConfig } from './discovery/projectConfig.js'; import { buildInstructions } from './discovery/instructions.js'; /** * CLI entry point. Поддерживает sub-команды: * * els-mcp — запуск сервера * ELS_API_KEY=els_live_... els-mcp # stdio * MCP_TRANSPORT=http MCP_HTTP_PORT=3030 els-mcp # HTTP * * els-mcp verify-audit --app=X [--from=ISO] [--to=ISO] — проверка * hash-chain'а audit log для заданного app. * * Транспорт выбирается через `MCP_TRANSPORT` (stdio | http). * Все логи — в stderr (stdout зарезервирован под JSON-RPC при stdio). */ async function main() { // Sub-команды (verify-audit). Если первым аргументом задано имя // sub-команды, выполняем её и выходим — instrumentation не нужен. const sub = process.argv[2]; if (sub === 'verify-audit') { process.exit(await runVerifyAudit(process.argv.slice(3))); } // 1. OTel — early init (no-op если OTEL_EXPORTER_OTLP_ENDPOINT не задан). const tracing = await initInstrumentation(); let config; try { config = loadConfig(process.env); } catch (err) { process.stderr.write(`[els-mcp] Config error: ${err instanceof Error ? err.message : String(err)}\n`); process.exit(1); } // Pino всегда в stderr — для stdio это обязательно, для HTTP — единый стиль. const log = pino({ level: config.logLevel, name: 'els-mcp' }, pino.destination({ dest: 2, sync: false })); if (tracing.enabled) { log.info('OpenTelemetry tracing enabled'); } if (config.transport === 'http') { await runHttp(config, log, tracing); } else { await runStdio(config, log, tracing); } } function parseFlags(args) { const out = {}; for (const a of args) { const m = /^--([\w-]+)=(.*)$/.exec(a); if (m && m[1]) out[m[1]] = m[2] ?? ''; } return out; } /** * `els-mcp verify-audit --app=X [--from=ISO] [--to=ISO]` * Возвращает exit-code: 0 = chain OK, 1 = broken / config error. */ async function runVerifyAudit(args) { const flags = parseFlags(args); const appId = flags.app ?? flags.appId; if (!appId) { process.stderr.write('Usage: els-mcp verify-audit --app=<appId> [--from=<ISO>] [--to=<ISO>]\n'); return 1; } const log = pino({ level: process.env.MCP_LOG_LEVEL ?? 'info', name: 'els-mcp:verify' }, pino.destination({ dest: 2, sync: false })); const databaseUrl = process.env.MCP_DATABASE_URL?.trim(); if (!databaseUrl) { process.stderr.write('MCP_DATABASE_URL is not set; cannot verify audit chain.\n'); return 1; } const verifyMod = await import('./audit/verify.js'); const result = await verifyMod.verifyChain({ appId, ...(flags.from ? { from: flags.from } : {}), ...(flags.to ? { to: flags.to } : {}), databaseUrl, log, }); process.stdout.write(`${verifyMod.formatVerifyResult(result)}\n`); return result.breakAt ? 1 : 0; } async function runStdio(config, log, tracing) { // Auto-discovery: пытаемся прочитать els.config.json / package.json[inso.els] // из process.cwd() и (опционально) из ELS_PROJECT_CONFIG_DIR. const dirs = [process.cwd()]; const envDir = process.env.ELS_PROJECT_CONFIG_DIR?.trim(); if (envDir) dirs.push(envDir); const projectConfig = readProjectConfig(dirs, { onWarn: (msg, ctx) => log.warn(ctx ?? {}, `[auto-discovery] ${msg}`), }); if (projectConfig) { log.info({ appSlug: projectConfig.appSlug, source: projectConfig.sourcePath }, 'Auto-discovered project config'); } const instructions = buildInstructions({ project: projectConfig }); const { server, client } = createMcpServer({ config, log, projectConfig, instructions, }); const shutdown = async (signal) => { log.info({ signal }, 'Shutting down'); try { await server.close(); await client.close(); await tracing.shutdown(); } catch (err) { log.error({ err }, 'Error during shutdown'); } process.exit(0); }; process.on('SIGINT', () => void shutdown('SIGINT')); process.on('SIGTERM', () => void shutdown('SIGTERM')); try { await connectStdio(server); log.info('MCP stdio transport ready'); } catch (err) { log.error({ err }, 'Failed to start stdio transport'); process.exit(1); } } async function runHttp(config, log, tracing) { const handle = await startHttpServer({ config, log }); const shutdown = async (signal) => { log.info({ signal }, 'Shutting down'); try { await handle.close(); await tracing.shutdown(); } catch (err) { log.error({ err }, 'Error during shutdown'); } process.exit(0); }; process.on('SIGINT', () => void shutdown('SIGINT')); process.on('SIGTERM', () => void shutdown('SIGTERM')); } void main(); //# sourceMappingURL=cli.js.map