Seq MCP Server
by ahmad2x4
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import 'dotenv/config';
// Configuration and constants
const SEQ_BASE_URL = process.env.SEQ_BASE_URL || 'http://localhost:8080';
const SEQ_API_KEY = process.env.SEQ_API_KEY || '';
const MAX_EVENTS = 100;
// Types for SEQ API responses
interface Signal {
id: string;
title: string;
description?: string;
filters: unknown;
ownerId?: string;
shared: boolean;
}
type Event = any;
// Create the MCP server
const server = new McpServer({
name: "seq-server",
version: "1.0.0"
});
// Helper function for SEQ API requests
async function makeSeqRequest<T>(endpoint: string, params: Record<string, string> = {}): Promise<T> {
const url = new URL(`${SEQ_BASE_URL}${endpoint}`);
// Add API key as query parameter
url.searchParams.append('apiKey', SEQ_API_KEY);
// Add additional query parameters
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, value);
}
});
const headers: Record<string, string> = {
'Accept': 'application/json',
'X-Seq-ApiKey': SEQ_API_KEY
};
const response = await fetch(url.toString(), { headers });
if (!response.ok) {
throw new Error(`SEQ API error: ${response.statusText} (${response.status})`);
}
return response.json();
}
// Resource for listing signals
server.resource(
"signals",
"seq://signals",
{
description: "List of saved Seq signals that can be used with the get-events tool to filter log events"
},
async () => {
try {
const signals = await makeSeqRequest<Signal[]>('/api/signals', { shared: 'true' });
const formattedSignals = signals.map(signal => ({
id: signal.id,
title: signal.title,
description: signal.description || 'No description provided',
shared: signal.shared,
ownerId: signal.ownerId
}));
return {
contents: [{
uri: 'seq://signals',
text: JSON.stringify(formattedSignals, null, 2)
}]
};
} catch (error) {
console.error('Error fetching signals:', error);
throw error;
}
}
);
// Tool for fetching signals with filters
server.tool(
"get-signals",
{
ownerId: z.string().optional()
.describe('Owner ID to filter signals by'),
shared: z.boolean().optional()
.describe('Whether to include only shared signals (true) or private signals (false)'),
partial: z.boolean().optional()
.describe('Whether to include partial signal matches')
},
async ({ ownerId, shared, partial }) => {
try {
const params: Record<string, string> = {
// Default to shared=true if no other params provided
shared: shared?.toString() ?? "true"
};
if (ownerId) params.ownerId = ownerId;
if (partial !== undefined) params.partial = partial.toString();
const signals = await makeSeqRequest<Signal[]>('/api/signals', params);
return {
content: [{
type: "text",
text: JSON.stringify(signals, null, 2)
}]
};
} catch (error) {
const err = error as Error;
return {
content: [{
type: "text",
text: `Error fetching signals: ${err.message}`
}],
isError: true
};
}
}
);
// Schema for time range validation
const timeRangeSchema = z.enum(['1m', '15m', '30m', '1h', '2h', '6h', '12h', '1d', '7d', '14d', '30d']);
// Tool for fetching events with enhanced parameters
server.tool(
"get-events",
`Retrieve and analyze a list of event filtered by parameters. Use this tool when you need to:
- Investigate events that are being logged in the SEQ server
- Details of each event is a structured log and can provide usefull information
- Events could be information, error, debug, or critical
- Analyze error patterns and frequencies
`,
{
signal: z.string().optional()
.describe('Comma-separated list of signal IDs'),
filter: z.string().optional()
.describe('Filter expression for events'),
count: z.number().min(1).max(MAX_EVENTS).optional()
.default(MAX_EVENTS)
.describe(`Number of events to return (max ${MAX_EVENTS})`),
fromDateUtc: z.string().optional()
.describe('Start date/time in UTC'),
toDateUtc: z.string().optional()
.describe('End date/time in UTC'),
range: timeRangeSchema.optional()
.describe('Time range (e.g., 1m, 15m, 1h, 1d, 7d)')
},
async ({ signal, filter, count, fromDateUtc, toDateUtc, range }) => {
try {
const params: Record<string, string> = {};
// Handle date range parameters
if (range) {
// If range is provided, it takes precedence over fromDateUtc/toDateUtc
params.range = range;
} else if (fromDateUtc || toDateUtc) {
// Only add date parameters if they're provided
if (fromDateUtc) params.fromDateUtc = fromDateUtc;
if (toDateUtc) params.toDateUtc = toDateUtc;
} else {
// Default to last hour if no time parameters provided
params.range = '1h';
}
// Add other optional parameters
if (signal) params.signal = signal;
if (filter) params.filter = filter;
if (count) params.count = count.toString();
const events = await makeSeqRequest<Event[]>('/api/events', params);
return {
content: [{
type: "text",
text: JSON.stringify(events, null, 2)
}]
};
} catch (error) {
const err = error as Error;
return {
content: [{
type: "text",
text: `Error fetching events: ${err.message}`
}],
isError: true
};
}
}
);
// Tool for fetching alert state
server.tool(
"get-alertstate",
{},
async () => {
try {
const alertState = await makeSeqRequest<any>('/api/alertstate');
return {
content: [{
type: "text",
text: JSON.stringify(alertState, null, 2)
}]
};
} catch (error) {
const err = error as Error;
return {
content: [{
type: "text",
text: `Error fetching alert state: ${err.message}`
}],
isError: true
};
}
}
);
// Start the server with stdio transport
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Always run the server when this file is executed directly
runServer().catch(error => {
console.error('Failed to start server:', error);
process.exit(1);
});
// Handle stdin close gracefully
process.stdin.on("close", () => {
console.error("Seq MCP Server closed");
server.close();
});
export default server;