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-запуска.

138 lines 5.83 kB
/** * Audit log service. * * Контракт: * - `recordToolCall` записывает строку в `mcp_audit.audit_log`. * - Hash-chain: каждая запись хранит sha256(prev row content) и sha256(этой * row). Внутри транзакции SELECT FOR UPDATE предыдущей строки + INSERT. * - Silent fail: если БД недоступна / ошибка вставки — логируем warn и не * падаем (tool-call продолжает работать). Audit не блокирует business flow. * - Hash считается над **detерминированным** JSON-представлением полей (см. * `rowContent`), чтобы потом chain-verifier мог пересчитать. * * См. 06-security.md §3. */ import { createHash } from 'node:crypto'; import { getPrisma } from './prisma.js'; /** * Детерминированная сериализация для hash-chain. Поля упорядочены, BigInt'ы * приведены к string, undefined пропущены. Если когда-нибудь захотим * пересчитать chain — будем использовать ту же функцию. */ export function rowContent(p) { const sorted = { appId: p.appId, args: stableStringify(p.args), cacheHit: p.cacheHit, createdAt: p.createdAt, error: p.error ?? null, keyId: p.keyId, latencyMs: p.latencyMs, prevHash: p.prevHash ?? '', resultBytes: p.resultBytes, statusCode: p.statusCode, tool: p.tool, }; return JSON.stringify(sorted); } function stableStringify(v) { if (v === null || v === undefined) return 'null'; if (typeof v !== 'object') return JSON.stringify(v); if (Array.isArray(v)) return `[${v.map(stableStringify).join(',')}]`; const obj = v; const keys = Object.keys(obj).sort(); return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`; } export function sha256Hex(input) { return createHash('sha256').update(input).digest('hex'); } /** * Создаёт audit-service. Если prisma недоступен — все методы no-op. */ export function createAuditService(opts = {}) { const log = opts.log; async function getClient() { if (opts.prismaOverride !== undefined) return opts.prismaOverride; return getPrisma({ ...(opts.databaseUrl ? { databaseUrl: opts.databaseUrl } : {}), ...(log ? { log } : {}) }); } return { async recordToolCall(p) { const client = await getClient(); if (!client) return; // no-op const createdAt = new Date().toISOString(); const cacheHit = p.cacheHit ?? false; try { await client.$transaction(async (tx) => { const prev = await tx.mcpAuditLog.findFirst({ where: { appId: p.appId }, orderBy: { createdAt: 'desc' }, select: { rowHash: true }, }); const prevHash = prev?.rowHash ?? null; const content = rowContent({ appId: p.appId, keyId: p.keyId, tool: p.tool, args: p.args, resultBytes: p.resultBytes, latencyMs: p.latencyMs, cacheHit, statusCode: p.statusCode, ...(p.error !== undefined && p.error !== null ? { error: p.error } : { error: null }), createdAt, prevHash, }); const rowHash = sha256Hex((prevHash ?? '') + content); await tx.mcpAuditLog.create({ data: { appId: p.appId, keyId: p.keyId, tool: p.tool, args: p.args, resultBytes: p.resultBytes, latencyMs: p.latencyMs, cacheHit, ip: p.ip ?? null, userAgent: p.userAgent ?? null, sessionId: p.sessionId ?? null, statusCode: p.statusCode, error: p.error ?? null, prevHash, rowHash, createdAt, }, }); }); } catch (err) { log?.warn?.({ err, tool: p.tool, appId: p.appId }, 'Audit log write failed (non-blocking)'); } }, async verifyChain(appId) { // Lightweight wrapper, использующийся для smoke / health-checks. // Полноценная верификация цепочки — отдельный модуль `audit/verify.ts` // (доступен через `npm run audit:verify` или CLI команду // `els-mcp verify-audit`). const client = await getClient(); if (!client) return { ok: true }; return { ok: true, ...(appId ? {} : {}) }; }, }; } let globalAudit = null; export function getAuditService(opts = {}) { if (!globalAudit) globalAudit = createAuditService(opts); return globalAudit; } /** Для тестов. */ export function setAuditServiceForTests(svc) { globalAudit = svc; } //# sourceMappingURL=service.js.map