signoz-mcp-server
Version:
Model Context Protocol server for SigNoz observability platform
780 lines β’ 33.4 kB
JavaScript
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const client_js_1 = require("./client.js");
const types_js_2 = require("./types.js");
const fs_1 = require("fs");
const path_1 = require("path");
const os_1 = require("os");
// Load configuration
function loadConfig() {
// Try environment variables first
const envUrl = process.env.SIGNOZ_URL;
const envToken = process.env.SIGNOZ_TOKEN;
if (envUrl && envToken) {
return {
url: envUrl,
token: envToken,
name: 'env',
description: 'Configuration from environment variables'
};
}
// Try config file
const configFile = (0, path_1.join)((0, os_1.homedir)(), '.signoz-mcp', 'config.json');
if ((0, fs_1.existsSync)(configFile)) {
try {
const configData = JSON.parse((0, fs_1.readFileSync)(configFile, 'utf8'));
return types_js_2.SigNozConfigSchema.parse(configData);
}
catch (error) {
console.error('Error loading config file:', error);
}
}
throw new Error('No SigNoz configuration found. Please run "npx signoz-mcp configure" or set SIGNOZ_URL and SIGNOZ_TOKEN environment variables.');
}
const config = loadConfig();
const client = new client_js_1.SigNozApiClient({
baseURL: config.url,
apiKey: config.token,
timeout: 30000,
retries: 3,
});
const server = new index_js_1.Server({
name: 'signoz-mcp-server',
version: '1.2.0',
}, {
capabilities: {
tools: {},
},
});
// Helper function to format time ranges
function getTimeRange(period = '1h') {
const end = Date.now();
const periodMap = {
'5m': 5 * 60 * 1000,
'15m': 15 * 60 * 1000,
'30m': 30 * 60 * 1000,
'1h': 60 * 60 * 1000,
'3h': 3 * 60 * 60 * 1000,
'6h': 6 * 60 * 60 * 1000,
'12h': 12 * 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000,
'7d': 7 * 24 * 60 * 60 * 1000,
'30d': 30 * 24 * 60 * 60 * 1000,
};
const duration = periodMap[period] || periodMap['1h'];
return {
start: end - duration,
end,
};
}
// Helper function to create metric queries
function createMetricQuery(metricName, filters, groupBy) {
return {
builderQueries: {
A: {
queryName: 'A',
dataSource: 'metrics',
aggregateOperator: 'avg',
aggregateAttribute: {
key: metricName,
dataType: 'float64',
type: 'tag',
},
filters: filters || { op: 'AND', items: [] },
groupBy: groupBy?.map(key => ({
key,
dataType: 'string',
type: 'tag',
})) || [],
expression: 'A',
disabled: false,
},
},
panelType: 'graph',
queryType: 'builder',
};
}
// List available tools
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
return {
tools: [
// System & Health
{
name: 'get_system_health',
description: 'Get SigNoz system health and version information',
inputSchema: {
type: 'object',
properties: {},
},
},
// Services & Monitoring
{
name: 'get_services',
description: 'Get list of services with performance metrics',
inputSchema: {
type: 'object',
properties: {
period: {
type: 'string',
description: 'Time period (5m, 15m, 30m, 1h, 3h, 6h, 12h, 24h, 7d, 30d)',
default: '1h',
},
},
},
},
{
name: 'get_service_details',
description: 'Get detailed information about a specific service',
inputSchema: {
type: 'object',
properties: {
service: {
type: 'string',
description: 'Service name',
},
period: {
type: 'string',
description: 'Time period',
default: '1h',
},
},
required: ['service'],
},
},
// Traces
{
name: 'get_trace',
description: 'Get details of a specific trace',
inputSchema: {
type: 'object',
properties: {
traceId: {
type: 'string',
description: 'Trace ID',
},
},
required: ['traceId'],
},
},
{
name: 'get_dependency_graph',
description: 'Get service dependency graph',
inputSchema: {
type: 'object',
properties: {
period: {
type: 'string',
description: 'Time period',
default: '1h',
},
},
},
},
// Metrics
{
name: 'query_metrics',
description: 'Execute a metric query using PromQL or SigNoz query builder',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'PromQL query or metric name',
},
period: {
type: 'string',
description: 'Time period',
default: '1h',
},
step: {
type: 'string',
description: 'Query step (e.g., 1m, 5m)',
default: '1m',
},
},
required: ['query'],
},
},
// Logs
{
name: 'search_logs',
description: 'Search logs with optional filters',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query string',
},
period: {
type: 'string',
description: 'Time period',
default: '1h',
},
limit: {
type: 'number',
description: 'Maximum number of logs to return',
default: 100,
},
},
},
},
// Dashboards
{
name: 'get_dashboards',
description: 'Get list of all dashboards',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_dashboard',
description: 'Get details of a specific dashboard',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Dashboard ID',
},
},
required: ['id'],
},
},
// Alerts
{
name: 'get_alerts',
description: 'Get active alerts and alert rules',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['active', 'rules'],
description: 'Type of alerts to retrieve',
default: 'active',
},
},
},
},
{
name: 'create_alert_rule',
description: 'Create a new alert rule',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Alert rule name',
},
expression: {
type: 'string',
description: 'Alert expression (PromQL)',
},
duration: {
type: 'string',
description: 'Duration threshold (e.g., 5m)',
default: '5m',
},
severity: {
type: 'string',
enum: ['critical', 'warning', 'info'],
description: 'Alert severity',
default: 'warning',
},
summary: {
type: 'string',
description: 'Alert summary',
},
},
required: ['name', 'expression'],
},
},
// Infrastructure
{
name: 'get_infrastructure',
description: 'Get infrastructure monitoring data (hosts, pods, etc.)',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['hosts', 'pods', 'nodes', 'deployments'],
description: 'Infrastructure type',
default: 'hosts',
},
period: {
type: 'string',
description: 'Time period',
default: '1h',
},
limit: {
type: 'number',
description: 'Maximum number of items',
default: 50,
},
},
},
},
// Live monitoring
{
name: 'start_live_tail',
description: 'Start live tail monitoring for logs (returns setup instructions)',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Optional log filter query',
},
},
},
},
// Enterprise features (if available)
{
name: 'get_license_info',
description: 'Get license and feature information (Enterprise)',
inputSchema: {
type: 'object',
properties: {},
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_system_health': {
const [version, health, features] = await Promise.all([
client.getVersion(),
client.getHealth().catch(() => ({ status: 'unknown' })),
client.getFeatures().catch(() => []),
]);
return {
content: [
{
type: 'text',
text: JSON.stringify({
version: version.version,
enterprise: version.ee,
setupCompleted: version.setupCompleted,
health: health.status,
features: features.length,
availableFeatures: features.filter(f => f.active).map(f => f.name),
}, null, 2),
},
],
};
}
case 'get_services': {
const period = args?.period || '1h';
const timeRange = getTimeRange(period);
const services = await client.getServices(timeRange);
return {
content: [
{
type: 'text',
text: `Services Overview (${period}):\n\n` +
services.map(s => `πΉ ${s.serviceName}\n` +
` P99: ${s.p99.toFixed(2)}ms | Avg: ${s.avgDuration.toFixed(2)}ms\n` +
` Calls: ${s.numCalls.toLocaleString()} | Rate: ${s.callRate.toFixed(2)}/s\n` +
` Errors: ${s.numErrors} (${(s.errorRate * 100).toFixed(2)}%)\n`).join('\n'),
},
],
};
}
case 'get_service_details': {
const service = args?.service;
const period = args?.period || '1h';
const timeRange = getTimeRange(period);
if (!service) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Service name is required');
}
const [operations, services] = await Promise.all([
client.getTopOperations(service, timeRange),
client.getServices(timeRange, { service }),
]);
const serviceData = services.find(s => s.serviceName === service);
return {
content: [
{
type: 'text',
text: serviceData ?
`Service: ${service} (${period})\n\n` +
`π Performance Metrics:\n` +
` P99 Latency: ${serviceData.p99.toFixed(2)}ms\n` +
` Avg Duration: ${serviceData.avgDuration.toFixed(2)}ms\n` +
` Request Rate: ${serviceData.callRate.toFixed(2)}/s\n` +
` Error Rate: ${(serviceData.errorRate * 100).toFixed(2)}%\n` +
` Total Calls: ${serviceData.numCalls.toLocaleString()}\n` +
` Total Errors: ${serviceData.numErrors}\n\n` +
`π Top Operations:\n` +
operations.slice(0, 10).map((op, i) => ` ${i + 1}. ${op.name || 'Unknown'} (${op.p99 || 0}ms)`).join('\n') :
`Service "${service}" not found in the current time period.`,
},
],
};
}
case 'get_trace': {
const traceId = args?.traceId;
if (!traceId) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Trace ID is required');
}
const trace = await client.getTrace(traceId);
return {
content: [
{
type: 'text',
text: `Trace: ${traceId}\n\n` +
`π Trace Details:\n` +
JSON.stringify(trace, null, 2),
},
],
};
}
case 'get_dependency_graph': {
const period = args?.period || '1h';
const timeRange = getTimeRange(period);
const graph = await client.getDependencyGraph(timeRange);
return {
content: [
{
type: 'text',
text: `Service Dependency Graph (${period}):\n\n` +
JSON.stringify(graph, null, 2),
},
],
};
}
case 'query_metrics': {
const query = args?.query;
const period = args?.period || '1h';
const step = args?.step || '1m';
if (!query) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Query is required');
}
const timeRange = getTimeRange(period);
// Try PromQL first
try {
const result = await client.prometheusQueryRange(query, Math.floor(timeRange.start / 1000), Math.floor(timeRange.end / 1000), step);
return {
content: [
{
type: 'text',
text: `Metric Query Results (${period}):\n\n` +
`Query: ${query}\n\n` +
JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
// Fallback to query builder
const compositeQuery = createMetricQuery(query);
const result = await client.queryRange({
start: timeRange.start,
end: timeRange.end,
step: 60,
compositeQuery,
});
return {
content: [
{
type: 'text',
text: `Metric Query Results (${period}):\n\n` +
`Metric: ${query}\n\n` +
JSON.stringify(result, null, 2),
},
],
};
}
}
case 'search_logs': {
const query = args?.query || '';
const period = args?.period || '1h';
const limit = args?.limit || 100;
const timeRange = getTimeRange(period);
try {
const result = await client.searchLogsV3({
start: timeRange.start,
end: timeRange.end,
query: query || undefined,
limit,
orderBy: 'timestamp',
order: 'desc',
});
const logs = result.logs || [];
const total = result.total || 0;
return {
content: [
{
type: 'text',
text: `Log Search Results (${period}):\n\n` +
`Query: "${query}"\n` +
`Found: ${logs.length} logs (Total: ${total})\n\n` +
logs.slice(0, 20).map((log) => `[${new Date(parseInt(log.timestamp) / 1000000).toISOString()}] ${log.body || log.message || JSON.stringify(log)}`).join('\n') +
(logs.length > 20 ? `\n\n... and ${logs.length - 20} more logs` : ''),
},
],
};
}
catch (error) {
// Fallback to old API if V3 fails
const logs = await client.getLogs({
q: query,
timestampStart: timeRange.start * 1000000, // Convert to nanoseconds
timestampEnd: timeRange.end * 1000000,
limit,
orderBy: 'timestamp',
order: 'desc',
});
const logsArray = Array.isArray(logs) ? logs : (logs.results ? logs.results : []);
const safeLogsArray = Array.isArray(logsArray) ? logsArray : [];
return {
content: [
{
type: 'text',
text: `Log Search Results (${period}) - Fallback API:\n\n` +
`Query: "${query}"\n` +
`Found: ${safeLogsArray.length} logs\n\n` +
safeLogsArray.slice(0, 20).map((log) => `[${new Date(parseInt(log.timestamp) / 1000000).toISOString()}] ${log.body}`).join('\n') +
(safeLogsArray.length > 20 ? `\n\n... and ${safeLogsArray.length - 20} more logs` : ''),
},
],
};
}
}
case 'get_dashboards': {
const dashboards = await client.getDashboards();
return {
content: [
{
type: 'text',
text: `Dashboards (${dashboards.length} total):\n\n` +
dashboards.map(d => `π ${d.title || d?.data?.title || 'Untitled Dashboard'}\n` +
` ID: ${d.id}\n` +
` Description: ${d.description || d.data?.description || 'No description'}\n` +
` Tags: ${d.tags?.join(', ') || d.data?.tags?.join(', ') || 'None'}\n`).join('\n'),
},
],
};
}
case 'get_dashboard': {
const id = args?.id;
if (!id) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Dashboard ID is required');
}
const dashboard = await client.getDashboard(id);
return {
content: [
{
type: 'text',
text: `Dashboard: ${dashboard.title || dashboard?.data?.title || 'Untitled Dashboard'}\n\n` +
JSON.stringify(dashboard, null, 2),
},
],
};
}
case 'get_alerts': {
const type = args?.type || 'active';
if (type === 'active') {
const alerts = await client.getActiveAlerts();
return {
content: [
{
type: 'text',
text: `Active Alerts (${alerts.length} total):\n\n` +
(alerts.length > 0 ?
JSON.stringify(alerts, null, 2) :
'β
No active alerts'),
},
],
};
}
else {
const rules = await client.getAlertRules();
const rulesArray = Array.isArray(rules) ? rules : (rules.data?.rules ? rules.data.rules : []);
return {
content: [
{
type: 'text',
text: `Alert Rules (${rulesArray.length} total):\n\n` +
rulesArray.map((rule) => `π¨ ${rule.alert}\n` +
` Expression: ${rule.expr}\n` +
` For: ${rule.for}\n` +
` Labels: ${JSON.stringify(rule.labels || {})}\n`).join('\n'),
},
],
};
}
}
case 'create_alert_rule': {
const name = args?.name;
const expression = args?.expression;
const duration = args?.duration || '5m';
const severity = args?.severity || 'warning';
const summary = args?.summary;
if (!name || !expression) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Name and expression are required');
}
const rule = {
alert: name,
expr: expression,
for: duration,
labels: {
severity: severity,
},
annotations: {
summary: summary || `Alert rule: ${name}`,
},
};
const created = await client.createAlertRule(rule);
return {
content: [
{
type: 'text',
text: `β
Alert rule created successfully!\n\n` +
JSON.stringify(created, null, 2),
},
],
};
}
case 'get_infrastructure': {
const type = args?.type || 'hosts';
const period = args?.period || '1h';
const limit = args?.limit || 50;
const timeRange = getTimeRange(period);
const params = {
start: timeRange.start,
end: timeRange.end,
limit,
offset: 0,
};
let result;
switch (type) {
case 'hosts':
result = await client.getHosts(params);
break;
case 'pods':
result = await client.getPods(params);
break;
default:
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unsupported infrastructure type: ${type}`);
}
return {
content: [
{
type: 'text',
text: `Infrastructure: ${type} (${period})\n\n` +
`Total: ${result.total}\n` +
`Showing: ${result.records.length}\n\n` +
result.records.map((item) => {
if (type === 'hosts') {
return `π₯οΈ ${item.hostName}\n` +
` CPU: ${item.cpu.toFixed(1)}% | Memory: ${item.memory.toFixed(1)}%\n` +
` Load: ${item.load15.toFixed(2)} | OS: ${item.os}\n`;
}
else if (type === 'pods') {
return `βΈοΈ ${item.meta['k8s.pod.name'] || 'Unknown'}\n` +
` CPU: ${(item.podCPU * 1000).toFixed(0)}m | Memory: ${(item.podMemory / 1024 / 1024).toFixed(0)}MB\n` +
` Restarts: ${item.restartCount} | Namespace: ${item.meta['k8s.namespace.name'] || 'Unknown'}\n`;
}
return JSON.stringify(item, null, 2);
}).join('\n'),
},
],
};
}
case 'start_live_tail': {
const query = args?.query || '';
return {
content: [
{
type: 'text',
text: `π΄ Live Tail Setup Instructions\n\n` +
`To start live log monitoring, you can use the SigNoz API directly:\n\n` +
`Endpoint: ${config.url}/api/v3/logs/livetail\n` +
`Method: GET (Server-Sent Events)\n` +
`Headers: SIGNOZ-API-KEY: ${config.token.slice(0, 8)}...\n` +
`Query: ${query || '(no filter)'}\n\n` +
`Example using curl:\n` +
`curl -N -H "SIGNOZ-API-KEY: ${config.token}" "${config.url}/api/v3/logs/livetail${query ? '?q=' + encodeURIComponent(query) : ''}"\n\n` +
`Note: Live tail requires a persistent connection and is not supported in this MCP context.`,
},
],
};
}
case 'get_license_info': {
try {
const [license, features] = await Promise.all([
client.getActiveLicense(),
client.getFeatures(),
]);
return {
content: [
{
type: 'text',
text: `π License Information:\n\n` +
`Plan: ${license.plan.name}\n` +
`Status: ${license.status}\n` +
`State: ${license.state}\n` +
`Platform: ${license.platform}\n` +
`Valid Until: ${new Date(license.valid_until * 1000).toISOString()}\n\n` +
`π― Available Features:\n` +
features.filter(f => f.active).map(f => ` β
${f.name}`).join('\n') +
`\n\nπ« Inactive Features:\n` +
features.filter(f => !f.active).map(f => ` β ${f.name}`).join('\n'),
},
],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text',
text: `β Unable to retrieve license information.\n` +
`This might be a Community Edition instance or the license API is not accessible.\n\n` +
`Error: ${errorMessage}`,
},
],
};
}
}
default:
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Error executing tool ${name}: ${errorMessage}`);
}
});
async function main() {
const transport = new stdio_js_1.StdioServerTransport();
await server.connect(transport);
// Validate connection on startup
try {
await client.getVersion();
console.error('β
SigNoz MCP Server connected successfully');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('β Failed to connect to SigNoz:', errorMessage);
process.exit(1);
}
}
main().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});
//# sourceMappingURL=index.js.map