@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-запуска.
174 lines (168 loc) • 7.88 kB
JavaScript
import { z } from 'zod';
import { ToolError } from '../lib/errors.js';
import { mistralChatJson, getMistralModel } from '../ai/mistralClient.js';
/**
* Tool: explain_error
*
* Composite: get_log_details + (optional) find_similar_errors + find_correlated_errors.
* Если `MISTRAL_API_KEY` задан — добавляем AI summary / likelyCauses / nextSteps.
* Иначе возвращаем raw context с `aiAvailable: false` — LLM-клиент сам формулирует.
*/
export const explainErrorInputShape = {
traceId: z.string().min(1).max(128),
locale: z.enum(['ru', 'en']).default('ru'),
includeRelated: z.boolean().default(true),
};
export const explainErrorToolDef = {
name: 'explain_error',
title: 'AI-assisted explanation of an error',
description: [
'Fetch error details + related context (similar + correlated) and, if a Mistral key is',
'configured, return a structured explanation: summary, likelyCauses, nextSteps.',
'Falls back to raw context when AI provider is offline.',
'',
'WHEN TO USE:',
' - User asks "what is this error?" with a traceId - one call to get full picture.',
' - Onboarding new engineer - friendly explanation of unfamiliar errors.',
' - Drafting an incident note - get summary + likely causes for write-up.',
].join('\n'),
inputShape: explainErrorInputShape,
};
const SYSTEM_PROMPT_RU = `Ты — SRE-инженер с опытом анализа production-инцидентов в Node.js / TypeScript / Express / Postgres / Redis.
Тебе передадут JSON с одним trace-логом ошибки и контекстом: количество подобных ошибок за сутки и список коррелированных trace'ов в том же временном окне.
Верни СТРОГО валидный JSON со следующей схемой (никакого markdown / лишнего текста):
{
"summary": "1-2 предложения на русском: что именно случилось простыми словами",
"likelyCauses": ["причина 1", "причина 2", "причина 3"],
"nextSteps": ["шаг 1", "шаг 2", "шаг 3"]
}
Правила: 2-4 элемента в каждом массиве; конкретика выше шаблонов; не выдумывай факты которых нет в данных; если данных мало — говори об этом в summary.`;
const SYSTEM_PROMPT_EN = `You are an SRE engineer specialised in Node.js / TypeScript / Express / Postgres / Redis production incidents.
You will receive a JSON payload with a single error trace and context: similar-error count over 24h and a list of correlated traceIds in the same time window.
Return STRICT valid JSON only (no markdown, no extra text):
{
"summary": "1-2 sentences in English: what happened in plain words",
"likelyCauses": ["cause 1", "cause 2", "cause 3"],
"nextSteps": ["step 1", "step 2", "step 3"]
}
Rules: 2-4 items per array; concrete over generic; don't invent facts not in the data; if data is sparse, say so in the summary.`;
function parseAiResponse(raw) {
if (!raw)
return null;
try {
const parsed = JSON.parse(raw);
const summary = typeof parsed.summary === 'string' ? parsed.summary.trim() : '';
const likelyCauses = Array.isArray(parsed.likelyCauses)
? parsed.likelyCauses.filter((x) => typeof x === 'string').slice(0, 6)
: [];
const nextSteps = Array.isArray(parsed.nextSteps)
? parsed.nextSteps.filter((x) => typeof x === 'string').slice(0, 6)
: [];
if (!summary)
return null;
return { summary, likelyCauses, nextSteps };
}
catch {
return null;
}
}
export async function handleExplainError(args, client) {
try {
const { data: details, elsRequestId } = await client.getLogDetails(args.traceId);
let similarCount24h;
let correlatedTraces;
const warnings = [];
if (args.includeRelated) {
try {
const { data: similar } = await client.findSimilarErrors(args.traceId, {});
const sim = (similar ?? {});
if (typeof sim.last24h === 'number')
similarCount24h = sim.last24h;
}
catch (err) {
warnings.push(`find_similar_errors failed: ${err instanceof ToolError ? err.code : 'INTERNAL'}`);
}
try {
const { data: correlated } = await client.findCorrelatedErrors(args.traceId, {});
const corr = (correlated ?? {});
const list = Array.isArray(corr.correlated) ? corr.correlated : [];
correlatedTraces = list
.map((c) => (typeof c.traceId === 'string' ? c.traceId : null))
.filter((t) => t !== null)
.slice(0, 10);
}
catch (err) {
warnings.push(`find_correlated_errors failed: ${err instanceof ToolError ? err.code : 'INTERNAL'}`);
}
}
const aiKey = process.env.MISTRAL_API_KEY;
let ai = null;
let model = null;
if (aiKey && aiKey.length >= 20 && !aiKey.startsWith('your_')) {
const systemPrompt = args.locale === 'en' ? SYSTEM_PROMPT_EN : SYSTEM_PROMPT_RU;
const userPayload = {
details,
relatedContext: {
similarCount24h: similarCount24h ?? null,
correlatedTraces: correlatedTraces ?? [],
},
};
const raw = await mistralChatJson(aiKey, {
systemPrompt,
userContent: JSON.stringify(userPayload),
});
ai = parseAiResponse(raw);
if (ai) {
model = getMistralModel();
}
else if (raw === null) {
warnings.push('mistral_unavailable: upstream timeout or non-2xx response');
}
else {
warnings.push('mistral_parse_failed: response not in expected JSON shape');
}
}
else {
warnings.push('mistral_not_configured: MISTRAL_API_KEY missing — AI summary unavailable');
}
const aiAvailable = ai !== null;
const meta = {
elsRequestId,
cached: false,
ttlSec: 60,
redactionApplied: false,
degraded: !aiAvailable,
...(warnings.length > 0 ? { warnings } : {}),
};
const textSummary = aiAvailable
? `[explain_error] traceId=${args.traceId}: ${ai.summary}`
: `[explain_error] traceId=${args.traceId}: details fetched, AI summary unavailable. The LLM should synthesize an explanation from the returned context.`;
return {
structuredContent: {
traceId: args.traceId,
summary: ai?.summary ?? null,
likelyCauses: ai?.likelyCauses ?? [],
nextSteps: ai?.nextSteps ?? [],
details,
related: {
...(similarCount24h !== undefined ? { similarCount24h } : {}),
...(correlatedTraces !== undefined ? { correlatedTraces } : {}),
},
meta: { model, aiAvailable },
_meta: meta,
},
content: [
{
type: 'text',
text: textSummary,
},
],
};
}
catch (err) {
if (err instanceof ToolError)
return err.toToolResult();
throw err;
}
}
//# sourceMappingURL=explainError.js.map