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

107 lines 4.58 kB
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