@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-запуска.
107 lines • 4.58 kB
JavaScript
import { z } from 'zod';
import { ToolError } from '../lib/errors.js';
import { applyResponseFormat } from '../lib/responseFormat.js';
/**
* Tool: query_logs_jql
* Mapping: POST /errors/query → analytics.routes.ts:60
* Upstream Zod: JqlQueryRequestSchema (filter AST, pagination, search).
*
* Поддерживает структурные boolean-условия (AND/OR/NOT/IN/comparison) поверх
* whitelisted полей ErrorLog. AST формат полностью совпадает с upstream
* ExprAst (см. schemas/jql.schema.ts) — мы пробрасываем body как есть.
*
* Limits (upstream): depth ≤ 8, nodes ≤ 100, IN-list ≤ 50, value ≤ 500 chars.
* При превышении ELS вернёт 400 → INVALID_ARGS.
*
* Defaults: limit=20, response_format=compact, sort=receivedAt desc.
*/
const JQL_OP_VALUES = ['=', '!=', '~', '!~', 'in', 'not_in', '>', '<', '>=', '<='];
// Recursive AST schema — зеркало FilterAstSchema из upstream.
// Используем z.ZodType<unknown> чтобы избежать чрезмерной типизации в SDK.
const PrimitiveValueSchema = z.union([z.string().max(500), z.number(), z.boolean()]);
const ExprSchema = z.lazy(() => z.union([
z.object({
type: z.literal('comparison'),
field: z.string().min(1).max(64),
op: z.enum(JQL_OP_VALUES),
value: z.union([PrimitiveValueSchema, z.array(PrimitiveValueSchema).max(50)]),
}),
z.object({ type: z.literal('and'), children: z.array(ExprSchema).min(1).max(50) }),
z.object({ type: z.literal('or'), children: z.array(ExprSchema).min(1).max(50) }),
z.object({ type: z.literal('not'), child: ExprSchema }),
]));
export const queryLogsJqlInputShape = {
filter: ExprSchema.optional().describe('Filter AST (comparison / and / or / not). Fields whitelisted upstream; ops per field-type.'),
search: z.string().max(500).optional(),
from: z.string().optional().describe('ISO timestamp. Default: 1h ago.'),
to: z.string().optional(),
limit: z.number().int().min(1).max(200).default(20),
offset: z.number().int().min(0).default(0),
sort: z.enum(['receivedAt', 'level', 'message']).default('receivedAt'),
order: z.enum(['asc', 'desc']).default('desc'),
// eslint-disable-next-line camelcase
response_format: z.enum(['compact', 'full', 'summary']).default('compact'),
};
export const queryLogsJqlToolDef = {
name: 'query_logs_jql',
title: 'Query logs with JQL filter AST',
description: 'Run a JQL-like structured query with nested AND/OR/NOT groups and IN/NOT_IN operators on whitelisted fields. Use when search_logs cannot express boolean logic.',
inputShape: queryLogsJqlInputShape,
};
function defaultFrom() {
return new Date(Date.now() - 60 * 60 * 1000).toISOString();
}
export async function handleQueryLogsJql(args, client) {
try {
const body = {
pagination: {
from: args.from ?? defaultFrom(),
to: args.to,
limit: args.limit,
offset: args.offset,
sort: args.sort,
order: args.order,
},
};
if (args.filter !== undefined)
body.filter = args.filter;
if (args.search !== undefined)
body.search = args.search;
const { data, elsRequestId } = await client.queryLogsJql(body);
const raw = data;
const { items: formattedItems, truncated } = applyResponseFormat(raw.items ?? [], args.response_format);
const total = Number(raw.total ?? 0);
const hasMore = args.offset + args.limit < total;
const meta = {
elsRequestId,
cached: false,
ttlSec: 15,
redactionApplied: false,
truncated,
};
return {
structuredContent: {
items: formattedItems,
total,
facets: raw.facets ?? {},
histogram: raw.histogram ?? [],
nextCursor: null,
offset: args.offset,
hasMore,
_meta: meta,
},
content: [
{
type: 'text',
text: `Matched ${total} logs via JQL (showing ${formattedItems.length}, offset ${args.offset}).`,
},
],
};
}
catch (err) {
if (err instanceof ToolError)
return err.toToolResult();
throw err;
}
}
//# sourceMappingURL=queryLogsJql.js.map