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

139 lines 5.87 kB
import { z } from 'zod'; import { ToolError } from '../lib/errors.js'; import { encodeCursor, decodeCursor } from '../lib/cursor.js'; import { applyResponseFormat } from '../lib/responseFormat.js'; /** * Tool: search_logs * * Mapping: GET /errors → error-logs-service/src/routes/analytics.routes.ts:33 * Zod schema (upstream): QueryErrorsSchema (page+limit+filters+sort). * * Особенности: * - cursor — transitional wrapper над offset/page (ELS пока offset-based). * - `level`/`serviceName`/`appVersion` upstream — comma-separated strings, * поэтому мы плющим массивы tool-input через `.join(',')`. * - response_format=compact (default) урезает stack/userAgent и message ≤ 200ch. */ const LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL', 'CRITICAL']; const ELS_SORT_BY = ['receivedAt', 'timestamp', 'level', 'message']; export const searchLogsInputShape = { from: z.string().optional().describe('ISO timestamp lower bound. Default: 1h ago.'), to: z.string().optional().describe('ISO timestamp upper bound. Default: now.'), level: z.array(z.enum(LEVELS)).optional().describe('Filter by error level(s).'), serviceName: z.array(z.string().max(255)).optional().describe('Filter by service name(s).'), appVersion: z.array(z.string().max(64)).optional().describe('Filter by app version(s).'), url: z.string().max(2048).optional(), fingerprint: z.string().max(128).optional(), sessionId: z.string().max(128).optional(), search: z.string().max(500).optional().describe('Full-text search in message + stack.'), cursor: z.string().nullable().optional().describe('Opaque seek-cursor. Pass null/omit for first page.'), limit: z.number().int().min(1).max(200).default(20), sortBy: z.enum(ELS_SORT_BY).default('receivedAt'), sortOrder: z.enum(['asc', 'desc']).default('desc'), // eslint-disable-next-line camelcase response_format: z.enum(['compact', 'full', 'summary']).default('compact'), }; export const searchLogsToolDef = { name: 'search_logs', title: 'Search error logs', description: [ 'Search error logs by facet filters (level, time range, service, version, message).', 'Returns paginated items + facets + histogram.', '', 'WHEN TO USE:', ' - After finishing a feature locally - verify no new dev errors', ' (deploymentEnv=DEV, from=last 20 minutes).', ' - After deployment - verify no new prod errors (deploymentEnv=PRODUCTION).', ' - User reports unexpected behavior - search by keyword from their report.', ' - Investigating CI failures - pull prod context for same timeframe.', '', 'DEFAULT: last 1 hour, limit 20, response_format=compact.', ].join('\n'), inputShape: searchLogsInputShape, }; /** * Канонические filters для cursor.filtersHash — должны быть детерминированы * относительно input (без page/cursor/limit/response_format). */ function filtersFromArgs(args) { return { from: args.from, to: args.to, level: args.level?.slice().sort(), serviceName: args.serviceName?.slice().sort(), appVersion: args.appVersion?.slice().sort(), url: args.url, fingerprint: args.fingerprint, sessionId: args.sessionId, search: args.search, sortBy: args.sortBy, sortOrder: args.sortOrder, }; } function defaultFrom() { return new Date(Date.now() - 60 * 60 * 1000).toISOString(); } export async function handleSearchLogs(args, client) { try { const filters = filtersFromArgs(args); let page = 1; if (args.cursor && args.cursor.length > 0) { const decoded = decodeCursor(args.cursor, filters); page = decoded.page ?? 1; } const upstreamParams = { page, limit: args.limit, sortBy: args.sortBy, sortOrder: args.sortOrder, from: args.from ?? defaultFrom(), to: args.to, search: args.search, url: args.url, fingerprint: args.fingerprint, sessionId: args.sessionId, levels: args.level?.join(','), serviceName: args.serviceName?.join(','), appVersion: args.appVersion?.join(','), }; const { data, elsRequestId } = await client.searchLogs(upstreamParams); const raw = data; const { items: formattedItems, truncated } = applyResponseFormat(raw.items, args.response_format); const hasMore = raw.page * raw.limit < raw.total; let nextCursor = null; if (hasMore && raw.items.length > 0) { const lastItem = raw.items[raw.items.length - 1]; nextCursor = encodeCursor({ receivedAt: lastItem.receivedAt, id: lastItem.id }, filters, raw.page + 1, raw.limit); } const meta = { elsRequestId, cached: false, ttlSec: 15, redactionApplied: false, truncated, }; const structured = { items: formattedItems, total: raw.total, facets: raw.facets, histogram: raw.histogram, nextCursor, _meta: meta, }; return { structuredContent: structured, content: [ { type: 'text', text: `Found ${raw.total} matching error logs (showing ${formattedItems.length}, page ${raw.page}/${raw.totalPages}).`, }, ], }; } catch (err) { if (err instanceof ToolError) return err.toToolResult(); throw err; } } //# sourceMappingURL=searchLogs.js.map