@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
JavaScript
// 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