import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { VictoriaLogsClient, handleApiError } from "../services/api.js"; import { CHARACTER_LIMIT, DEFAULT_LIMIT, ResponseFormat } from "../constants.js"; export function registerVictoriaLogsTools(server: McpServer, client: VictoriaLogsClient): void { // Query logs server.registerTool( "victorialogs_query", { title: "Query VictoriaLogs", description: `Execute a LogsQL query against VictoriaLogs and return matching log entries. Args: - query (string): LogsQL query (e.g., 'error', '{app="nginx"} AND error', '_time:5m | stats count()') - start (string, optional): Start time (RFC3339, Unix timestamp, or relative like '1h', '30m') - end (string, optional): End time (RFC3339, Unix timestamp, or relative) - limit (number, optional): Maximum number of log entries to return (default: 100) - offset (number, optional): Number of entries to skip for pagination - timeout (string, optional): Query timeout (e.g., '30s', '1m') Returns: JSON lines of log entries with _time, _msg, and other fields. Examples: - Simple search: query="error" - Stream filter: query='{app="nginx"}' - Time filter: query="_time:1h error" - With stats: query="error | stats count() by (level)"`, inputSchema: z.object({ query: z.string().describe("LogsQL query expression"), start: z.string().optional().describe("Start time (RFC3339, Unix ts, or relative like '1h')"), end: z.string().optional().describe("End time"), limit: z.number().int().min(1).max(10000).default(DEFAULT_LIMIT).describe("Max entries to return"), offset: z.number().int().min(0).optional().describe("Entries to skip"), timeout: z.string().optional().describe("Query timeout (e.g., '30s')") }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.query(params); const lines = result.trim().split("\n").filter(l => l); let output = lines.length > 0 ? `Found ${lines.length} log entries:\n\n${result}` : "No log entries found matching the query."; if (output.length > CHARACTER_LIMIT) { output = output.substring(0, CHARACTER_LIMIT) + "\n\n[Output truncated. Use 'limit' or add filters to reduce results.]"; } return { content: [{ type: "text", text: output }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Hits stats server.registerTool( "victorialogs_hits", { title: "Query Hits Stats", description: `Get hit count statistics over a time range, grouped by time buckets. Args: - query (string): LogsQL query - start (string, optional): Start time - end (string, optional): End time - step (string, optional): Time bucket size (e.g., '1h', '1d'). Default: '1d' - field (string[], optional): Fields to group by - fields_limit (number, optional): Limit unique field groups Returns: JSON with timestamps and hit counts per time bucket. Examples: - Hourly errors: query="error", start="24h", step="1h" - By level: query="*", field=["level"], step="1h"`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), start: z.string().optional().describe("Start time"), end: z.string().optional().describe("End time"), step: z.string().optional().describe("Bucket size (e.g., '1h', '1d')"), field: z.array(z.string()).optional().describe("Fields to group by"), fields_limit: z.number().int().optional().describe("Max field groups") }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.hits(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Facets server.registerTool( "victorialogs_facets", { title: "Query Facets", description: `Get the most frequent values for each field in matching logs. Args: - query (string): LogsQL query - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max values per field (default: 10) - max_values_per_field (number, optional): Skip fields with more unique values - max_value_len (number, optional): Skip values longer than this - keep_const_fields (boolean, optional): Include constant fields Returns: JSON with field names and their most frequent values with hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional().describe("Values per field"), max_values_per_field: z.number().int().optional(), max_value_len: z.number().int().optional(), keep_const_fields: z.boolean().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.facets(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Stats query (instant) server.registerTool( "victorialogs_stats_query", { title: "Query Log Stats (Instant)", description: `Execute a stats query at a specific point in time. Returns Prometheus-compatible format. Args: - query (string): LogsQL query with stats pipe (e.g., '_time:1d | stats by (level) count(*)') - time (string, optional): Evaluation time (default: now) Returns: Prometheus-compatible vector result with metrics and values.`, inputSchema: z.object({ query: z.string().describe("LogsQL query with | stats pipe"), time: z.string().optional().describe("Evaluation timestamp") }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.statsQuery(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Stats query range server.registerTool( "victorialogs_stats_query_range", { title: "Query Log Stats (Range)", description: `Execute a stats query over a time range. Returns Prometheus-compatible format. Args: - query (string): LogsQL query with stats pipe - start (string, optional): Start time - end (string, optional): End time - step (string, optional): Evaluation step (e.g., '1m', '5m') Returns: Prometheus-compatible matrix result with time series data.`, inputSchema: z.object({ query: z.string().describe("LogsQL query with | stats pipe"), start: z.string().optional(), end: z.string().optional(), step: z.string().optional().describe("Evaluation step") }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.statsQueryRange(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Streams server.registerTool( "victorialogs_streams", { title: "Query Log Streams", description: `Get log streams matching the query with their hit counts. Args: - query (string): LogsQL query - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max streams to return Returns: List of streams with stream labels and hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.streams(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Stream IDs server.registerTool( "victorialogs_stream_ids", { title: "Query Stream IDs", description: `Get unique stream IDs for matching logs. Args: - query (string): LogsQL query - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max stream IDs Returns: List of stream IDs with hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.streamIds(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Stream field names server.registerTool( "victorialogs_stream_field_names", { title: "List Stream Field Names", description: `Get available stream field names (fields used in stream labels). Args: - query (string): LogsQL query to scope the search - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max field names Returns: List of stream field names with hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.streamFieldNames(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Stream field values server.registerTool( "victorialogs_stream_field_values", { title: "Get Stream Field Values", description: `Get values for a specific stream field. Args: - query (string): LogsQL query - field (string): Stream field name to get values for - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max values Returns: List of field values with hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), field: z.string().describe("Stream field name"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.streamFieldValues(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Field names server.registerTool( "victorialogs_field_names", { title: "List Log Field Names", description: `Get all field names present in matching logs. Args: - query (string): LogsQL query to scope the search - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max field names Returns: List of field names with hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.fieldNames(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Field values server.registerTool( "victorialogs_field_values", { title: "Get Log Field Values", description: `Get values for a specific log field. Args: - query (string): LogsQL query - field (string): Field name to get values for - start (string, optional): Start time - end (string, optional): End time - limit (number, optional): Max values Returns: List of field values with hit counts.`, inputSchema: z.object({ query: z.string().describe("LogsQL query"), field: z.string().describe("Field name"), start: z.string().optional(), end: z.string().optional(), limit: z.number().int().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.fieldValues(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); // Tenants server.registerTool( "victorialogs_tenants", { title: "List Tenants", description: `List all available tenants (AccountID, ProjectID pairs) in VictoriaLogs. Args: - start (string, optional): Start time to scope the search - end (string, optional): End time Returns: List of tenant IDs with account_id and project_id.`, inputSchema: z.object({ start: z.string().optional(), end: z.string().optional() }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params) => { try { const result = await client.tenants(params); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); }