Skip to main content
Glama

Graylog MCP Server

index.ts10.2 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "graylog-mcp", version: "1.0.0", }); interface GraylogSearchMessageEntry { index: string; message: Record<string, unknown>; highlight?: Record<string, unknown>; } interface GraylogSearchResponse { query: string; took_ms: number; total_results: number; messages: GraylogSearchMessageEntry[]; } type GraylogConfig = { baseUrl: string; username: string; password: string; }; export function getGraylogConfig(): GraylogConfig { const { GRAYLOG_BASE_URL, GRAYLOG_USERNAME, GRAYLOG_PASSWORD } = process.env; if (!GRAYLOG_BASE_URL || !GRAYLOG_USERNAME || !GRAYLOG_PASSWORD) { throw new Error( "Missing Graylog configuration. Please set GRAYLOG_BASE_URL, GRAYLOG_USERNAME, and GRAYLOG_PASSWORD environment variables." ); } return { baseUrl: GRAYLOG_BASE_URL.endsWith("/") ? GRAYLOG_BASE_URL : `${GRAYLOG_BASE_URL}/`, username: GRAYLOG_USERNAME, password: GRAYLOG_PASSWORD, }; } function cleanPayload(payload: Record<string, unknown>): Record<string, unknown> { return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined && value !== null)); } function withFields<T extends { fields?: string[] }>(payload: T): Record<string, unknown> { const { fields, ...rest } = payload; return cleanPayload({ ...rest, ...(fields && fields.length > 0 ? { fields: fields.join(",") } : {}), }); } export async function graylogGet<T>(path: string, params: Record<string, unknown>): Promise<T> { const config = getGraylogConfig(); const normalizedPath = path.replace(/^\//, ""); const url = new URL(normalizedPath, config.baseUrl); for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue; const stringValue = Array.isArray(value) ? value.join(",") : typeof value === "boolean" ? String(value) : String(value); url.searchParams.set(key, stringValue); } const headers = new Headers({ Authorization: `Basic ${Buffer.from(`${config.username}:${config.password}`).toString("base64")}`, "X-Requested-By": "graylog-mcp", Accept: "application/json", }); let response: Response; try { response = await fetch(url, { method: "GET", headers, }); } catch (error) { throw new Error( `Failed to reach Graylog at ${url.toString()}: ${error instanceof Error ? error.message : String(error)}` ); } if (!response.ok) { const errorText = await response.text().catch(() => response.statusText); throw new Error(`Graylog request failed (${response.status} ${response.statusText}): ${errorText}`); } return (await response.json()) as T; } const relativeSearchInputDefinition = { query: z.string().min(1, "Query is required").describe("Graylog search query written in Lucene syntax."), range: z.number().int().positive().describe("Relative timeframe in seconds to search backwards from now."), limit: z .number() .int() .positive() .max(1000) .optional() .default(150) .describe("Maximum number of messages to return (default 150)."), offset: z.number().int().min(0).optional().describe("Pagination offset for the result set."), sort: z.string().optional().describe("Sort expression, e.g., 'timestamp:desc'."), filter: z.string().optional().describe("Graylog stream filter expression, e.g., 'streams:<stream-id>'."), fields: z .array(z.string().min(1)) .nonempty() .optional() .describe("Restrict response to these fields (will be joined as comma separated list)."), decorate: z.boolean().optional().describe("Whether to apply Graylog message decorators."), } satisfies Record<string, z.ZodTypeAny>; const relativeSearchInputSchema = z.object(relativeSearchInputDefinition); type RelativeSearchInput = z.infer<typeof relativeSearchInputSchema>; const absoluteSearchInputDefinition = { query: z.string().min(1, "Query is required").describe("Graylog search query written in Lucene syntax."), from: z .string() .min(1) .describe("Inclusive start timestamp (ISO 8601 or Graylog date format, e.g., '2025-01-01 00:00:00')."), to: z .string() .min(1) .describe("Inclusive end timestamp (ISO 8601 or Graylog date format, e.g., '2025-01-01 23:59:59')."), limit: z .number() .int() .positive() .max(1000) .default(150) .optional() .describe("Maximum number of messages to return (default 150)."), offset: z.number().int().min(0).optional().describe("Pagination offset for the result set."), sort: z.string().optional().describe("Sort expression, e.g., 'timestamp:desc'."), filter: z.string().optional().describe("Graylog stream filter expression, e.g., 'streams:<stream-id>'."), fields: z .array(z.string().min(1)) .nonempty() .optional() .describe("Restrict response to these fields (will be joined as comma separated list)."), decorate: z.boolean().optional().describe("Whether to apply Graylog message decorators."), } satisfies Record<string, z.ZodTypeAny>; const absoluteSearchInputSchema = z.object(absoluteSearchInputDefinition); type AbsoluteSearchInput = z.infer<typeof absoluteSearchInputSchema>; function formatSearchResult(data: GraylogSearchResponse) { return { query: data.query, took_ms: data.took_ms, total_results: data.total_results, messages: data.messages.map((entry) => ({ index: entry.index, message: entry.message, ...(entry.highlight ? { highlight: entry.highlight } : {}), })), }; } server.registerTool( "search_relative_logs", { title: "Search Relative Logs", description: "Search Graylog messages within a relative timeframe (seconds) counted back from now.", inputSchema: relativeSearchInputSchema.shape, }, async (input: RelativeSearchInput) => { try { const payload = withFields({ ...input, limit: input.limit, }); const data = await graylogGet<GraylogSearchResponse>("api/search/universal/relative", payload); return { content: [ { type: "text", text: JSON.stringify(formatSearchResult(data), null, 2), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: error instanceof Error ? error.message : "Unknown error while searching Graylog (relative).", }, ], }; } } ); server.registerTool( "count_relative_logs", { title: "Count Relative Logs", description: "Return only the total number of Graylog messages that match a query within a relative timeframe.", inputSchema: relativeSearchInputDefinition, }, async (input: RelativeSearchInput) => { try { const payload = withFields({ ...input, limit: 0, offset: input.offset ?? 0, }); const data = await graylogGet<GraylogSearchResponse>("api/search/universal/relative", payload); return { content: [ { type: "text", text: JSON.stringify( { query: data.query, took_ms: data.took_ms, total_results: data.total_results, }, null, 2 ), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: error instanceof Error ? error.message : "Unknown error while counting Graylog messages (relative).", }, ], }; } } ); server.registerTool( "search_absolute_logs", { title: "Search Absolute Logs", description: "Search Graylog messages within an absolute time range defined by explicit start and end timestamps.", inputSchema: absoluteSearchInputDefinition, }, async (input: AbsoluteSearchInput) => { try { const payload = withFields({ ...input, limit: input.limit, }); const data = await graylogGet<GraylogSearchResponse>("api/search/universal/absolute", payload); return { content: [ { type: "text", text: JSON.stringify(formatSearchResult(data), null, 2), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: error instanceof Error ? error.message : "Unknown error while searching Graylog (absolute).", }, ], }; } } ); server.registerTool( "count_absolute_logs", { title: "Count Absolute Logs", description: "Return only the total number of Graylog messages that match a query within an absolute timeframe.", inputSchema: absoluteSearchInputDefinition, }, async (input: AbsoluteSearchInput) => { try { const payload = withFields({ ...input, limit: 0, offset: input.offset ?? 0, }); const data = await graylogGet<GraylogSearchResponse>("api/search/universal/absolute", payload); return { content: [ { type: "text", text: JSON.stringify( { query: data.query, took_ms: data.took_ms, total_results: data.total_results, }, null, 2 ), }, ], }; } catch (error) { return { isError: true, content: [ { type: "text", text: error instanceof Error ? error.message : "Unknown error while counting Graylog messages (absolute).", }, ], }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.log("Graylog MCP server started"); } if (import.meta.main) { main(); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PrinceGupta1999/graylog-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server