UNPKG

signoz-mcp-server

Version:

Model Context Protocol server for SigNoz observability platform

780 lines β€’ 33.4 kB
#!/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