@kivanio/mcp-new-relic
Version:
MCP server for querying New Relic errors and metrics
258 lines • 10.5 kB
JavaScript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Configuration
const API_KEY = process.env.NEW_RELIC_API_KEY;
const ACCOUNT_ID = process.env.NEW_RELIC_ACCOUNT_ID;
const REGION = process.env.NEW_RELIC_REGION || 'US';
// API endpoint
const API_URL = REGION === 'EU'
? 'https://api.eu.newrelic.com/graphql'
: 'https://api.newrelic.com/graphql';
// Create server instance
const server = new Server({
name: 'mcp-new-relic',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Helper function to execute NRQL query
async function executeNrqlQuery(nrqlQuery) {
const graphqlQuery = {
query: `
{
actor {
account(id: ${ACCOUNT_ID}) {
nrql(query: "${nrqlQuery}") {
results
}
}
}
}
`,
};
try {
const response = await axios.post(API_URL, graphqlQuery, {
headers: {
'Content-Type': 'application/json',
'API-Key': API_KEY,
},
});
return response.data.data.actor.account.nrql.results || [];
}
catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`New Relic API error: ${error.response?.status} - ${error.response?.statusText}`);
}
throw error;
}
}
// Format error results for display
function formatErrorResults(results, limit) {
if (!results || results.length === 0) {
return 'No errors found! 🎉';
}
// Sort by count descending
const sortedResults = [...results].sort((a, b) => b.count - a.count);
const displayResults = limit ? sortedResults.slice(0, limit) : sortedResults;
let output = `=== New Relic Error Summary ===\n`;
output += `Total unique error types: ${results.length}\n\n`;
output += `Top Errors by Frequency:\n`;
output += '-'.repeat(80) + '\n';
displayResults.forEach((result, index) => {
const errorMessage = result.facet[0] || 'N/A';
const errorClass = result.facet[1] || 'N/A';
output += `\n${index + 1}. Error Count: ${result.count}\n`;
output += ` Class: ${errorClass}\n`;
output += ` Message: ${errorMessage.substring(0, 200)}${errorMessage.length > 200 ? '...' : ''}\n`;
});
if (sortedResults[0]) {
output += '\n' + '='.repeat(80) + '\n';
output += 'MOST FREQUENT ERROR:\n';
output += `Count: ${sortedResults[0].count}\n`;
output += `Class: ${sortedResults[0].facet[1] || 'N/A'}\n`;
output += `Message: ${sortedResults[0].facet[0] || 'N/A'}\n`;
}
return output;
}
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'query_errors',
description: 'Query New Relic for application errors',
inputSchema: {
type: 'object',
properties: {
timeRange: {
type: 'string',
description: 'Time range for the query (e.g., "1 hour ago", "4 hours ago", "24 hours ago", "7 days ago")',
default: '4 hours ago',
},
appName: {
type: 'string',
description: 'Application name pattern to filter (supports LIKE patterns)',
},
limit: {
type: 'number',
description: 'Maximum number of error types to return',
default: 20,
minimum: 1,
maximum: 100,
},
},
},
},
{
name: 'query_custom',
description: 'Execute a custom NRQL query on New Relic',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The NRQL query to execute',
},
},
required: ['query'],
},
},
{
name: 'query_metrics',
description: 'Query New Relic for application performance metrics',
inputSchema: {
type: 'object',
properties: {
timeRange: {
type: 'string',
description: 'Time range for the query (e.g., "1 hour ago", "4 hours ago", "24 hours ago")',
default: '1 hour ago',
},
appName: {
type: 'string',
description: 'Application name pattern to filter',
default: '%kivanio%',
},
metrics: {
type: 'array',
items: {
type: 'string',
enum: ['response_time', 'throughput', 'error_rate', 'apdex'],
},
description: 'Which metrics to query',
default: ['response_time', 'throughput', 'error_rate'],
},
},
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'query_errors': {
const timeRange = args?.timeRange || '4 hours ago';
const appName = args?.appName || '%kivanio%';
const limit = args?.limit || 20;
const nrqlQuery = `SELECT count(*), latest(error.message), latest(error.class) FROM TransactionError WHERE appName LIKE '${appName}' SINCE ${timeRange} FACET error.message, error.class LIMIT ${limit}`;
const results = await executeNrqlQuery(nrqlQuery);
const formattedOutput = formatErrorResults(results, limit);
return {
content: [
{
type: 'text',
text: formattedOutput,
},
],
};
}
case 'query_custom': {
if (!args?.query) {
throw new McpError(ErrorCode.InvalidParams, 'Query parameter is required');
}
const results = await executeNrqlQuery(args.query);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
case 'query_metrics': {
const timeRange = args?.timeRange || '1 hour ago';
const appName = args?.appName || '%kivanio%';
const metrics = args?.metrics || [
'response_time',
'throughput',
'error_rate',
];
const queries = {
response_time: `SELECT average(duration) as 'Response Time (ms)' FROM Transaction WHERE appName LIKE '${appName}' SINCE ${timeRange}`,
throughput: `SELECT rate(count(*), 1 minute) as 'Requests per minute' FROM Transaction WHERE appName LIKE '${appName}' SINCE ${timeRange}`,
error_rate: `SELECT percentage(count(*), WHERE error IS true) as 'Error Rate (%)' FROM Transaction WHERE appName LIKE '${appName}' SINCE ${timeRange}`,
apdex: `SELECT apdex(duration, 0.5) as 'Apdex Score' FROM Transaction WHERE appName LIKE '${appName}' SINCE ${timeRange}`,
};
let output = `=== New Relic Performance Metrics ===\n`;
output += `Application: ${appName}\n`;
output += `Time Range: Since ${timeRange}\n\n`;
for (const metric of metrics) {
if (queries[metric]) {
try {
const results = await executeNrqlQuery(queries[metric]);
if (results.length > 0) {
output += `${metric
.replace('_', ' ')
.toUpperCase()}: ${JSON.stringify(results[0], null, 2)}\n\n`;
}
}
catch (error) {
output += `${metric
.replace('_', ' ')
.toUpperCase()}: Error fetching metric\n\n`;
}
}
}
return {
content: [
{
type: 'text',
text: output,
},
],
};
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Error executing tool: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP New Relic server started');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
//# sourceMappingURL=index.js.map