Guide · MCP New Relic Integration
MCP Server New Relic — NRQL queries via NerdGraph, alert policy listing, custom event recording, and /health check
New Relic's NerdGraph is a unified GraphQL API that gives MCP tools access to NRQL query execution, alert policy management, entity search, and account metadata — all through a single endpoint. This guide covers building TypeScript MCP tools for NerdGraph: setting the Api-Key header with a User Key, running NRQL queries via GraphQL POST, listing alert policies with incident preference details, recording custom events via the separate Insights Collector API using an Insert Key, and wiring a /health/newrelic endpoint that validates your User Key before AliveMCP catches silent key expiry before your NRQL queries start returning empty results.
TL;DR
New Relic NerdGraph uses a single GraphQL endpoint at POST https://api.newrelic.com/graphql (EU: https://api.eu.newrelic.com/graphql) with the header Api-Key: <USER_KEY>. All requests are JSON with Content-Type: application/json and a body of { query: "...", variables: {...} }. Critical gotcha: NerdGraph always returns HTTP 200 even for authentication errors — you must check the response body for an errors array. A 403 HTTP response means the request format is wrong, not an auth failure. NRQL results live at data.actor.account.nrql.results. The Events API (for recording custom events) is a separate endpoint using an Insert Key, not a User Key.
SDK setup and authentication
New Relic does not publish an official Node.js client for NerdGraph — the API is a standard GraphQL endpoint, so axios with a shared instance is the idiomatic approach. The credential is called a "User Key" in New Relic's UI even when used for programmatic server-to-server access; the header name is Api-Key (not Authorization: Bearer). Your Account ID is a numeric identifier required by most NRQL and alert queries — find it in the New Relic UI under User menu → API Keys → Account ID.
import axios from 'axios';
const nerdgraph = axios.create({
baseURL: 'https://api.newrelic.com',
headers: {
'Api-Key': process.env.NEW_RELIC_USER_KEY!,
'Content-Type': 'application/json',
},
});
const ACCOUNT_ID = parseInt(process.env.NEW_RELIC_ACCOUNT_ID!, 10);
async function nrQuery(query: string, variables: Record<string, unknown> = {}) {
const res = await nerdgraph.post('/graphql', { query, variables });
if (res.data.errors?.length) {
throw new Error(`NerdGraph error: ${res.data.errors[0].message}`);
}
return res.data.data;
}
| Credential | Header | Where to find |
|---|---|---|
| User Key | Api-Key: NRAK-... |
New Relic UI → User menu → API Keys → Create key (type: User) |
| License Key | X-License-Key (APM agents) |
New Relic UI → API Keys → type: License — for agent data ingestion only |
| Insert Key | X-Insert-Key |
New Relic UI → API Keys → type: Insert — for Events API only |
| Account ID | (query variable) | New Relic UI → User menu → API Keys → Account ID column |
For EU data centers, replace api.newrelic.com with api.eu.newrelic.com and the Events collector URL (insights-collector.newrelic.com) with insights-collector.eu01.nr-data.net. The Api-Key header name and authentication mechanism are identical across regions.
Running NRQL queries via NerdGraph
NRQL (New Relic Query Language) queries execute through NerdGraph using a nested GraphQL structure: actor → account(id) → nrql(query). Results come back as an array of plain objects at data.actor.account.nrql.results. NRQL syntax is SQL-like — SELECT, FROM, WHERE, SINCE, UNTIL, FACET, and TIMESERIES are the primary clauses. The rate limit is 25 concurrent NerdGraph requests per API key and 600 requests per minute.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const server = new McpServer({ name: 'newrelic-mcp', version: '1.0.0' });
const NRQL_QUERY = `
query NrqlQuery($accountId: Int!, $nrql: Nrql!) {
actor {
account(id: $accountId) {
nrql(query: $nrql) {
results
}
}
}
}
`;
server.tool('query_nrql', {
nrql_query: z.string()
.describe('NRQL query string. Example: SELECT average(duration) FROM Transaction WHERE appName = \'my-app\' SINCE 1 hour ago LIMIT 100'),
account_id: z.number().int().optional()
.describe('New Relic account ID. Defaults to the account set in NEW_RELIC_ACCOUNT_ID env var.')
}, async ({ nrql_query, account_id }) => {
const accountId = account_id ?? ACCOUNT_ID;
const data = await nrQuery(NRQL_QUERY, {
accountId,
nrql: nrql_query
});
// Results live at data.actor.account.nrql.results — an array of objects
const results = data.actor.account.nrql.results as Record<string, unknown>[];
return {
content: [{
type: 'text',
text: JSON.stringify({
account_id: accountId,
query: nrql_query,
result_count: results.length,
results
}, null, 2)
}]
};
});
| NRQL clause | Example | Notes |
|---|---|---|
SINCE / UNTIL |
SINCE 1 hour ago |
Accepts relative (30 minutes ago) or absolute Unix timestamps |
FACET |
FACET appName |
Groups results by attribute — returns one row per unique value |
TIMESERIES |
TIMESERIES 5 minutes |
Returns time-bucketed data; each result includes a beginTimeSeconds field |
LIMIT |
LIMIT 100 |
Default is 100; max is 2000 for most event types |
WHERE |
WHERE error IS TRUE |
Filters on any attribute; use single quotes for string literals |
Listing alert policies with incident preference
Alert policies in New Relic group alert conditions and control how incidents are opened. The incidentPreference field determines granularity: PER_POLICY opens one incident per policy regardless of how many conditions fire, PER_CONDITION opens one incident per firing condition, and PER_CONDITION_AND_TARGET is the most granular — one incident per condition per monitored entity. Creating or modifying alert conditions via API is complex and error-prone; keep MCP tools read-only for policy inspection.
const LIST_POLICIES_QUERY = `
query ListAlertPolicies($accountId: Int!) {
actor {
account(id: $accountId) {
alerts {
policiesSearch {
policies {
id
name
incidentPreference
}
}
}
}
}
}
`;
server.tool('list_alert_policies', {
account_id: z.number().int().optional()
.describe('New Relic account ID. Defaults to NEW_RELIC_ACCOUNT_ID env var.')
}, async ({ account_id }) => {
const accountId = account_id ?? ACCOUNT_ID;
const data = await nrQuery(LIST_POLICIES_QUERY, { accountId });
const policies = data.actor.account.alerts.policiesSearch.policies as Array<{
id: string;
name: string;
incidentPreference: string;
}>;
return {
content: [{
type: 'text',
text: JSON.stringify({
account_id: accountId,
policy_count: policies.length,
policies: policies.map(p => ({
id: p.id,
name: p.name,
incident_preference: p.incidentPreference,
// incidentPreference meanings:
// PER_POLICY: one incident per policy (all conditions share an incident)
// PER_CONDITION: one incident per firing condition
// PER_CONDITION_AND_TARGET: most granular — one per condition per entity
}))
}, null, 2)
}]
};
});
For accounts using New Relic's older REST v2 alert data, the legacy endpoint GET https://api.newrelic.com/v2/alert_policies.json with the same Api-Key header returns policies in a flat JSON structure. NerdGraph is preferred for new integrations as it includes richer metadata and supports cursor-based pagination via policiesSearch(cursor: $cursor) for accounts with large numbers of policies.
Recording custom events via the Insights Collector API
Custom events use a completely separate API from NerdGraph — the Insights Collector endpoint — and require a different credential: an Insert Key (not a User Key). The Insert Key is write-only and cannot read any data, making it safe to deploy in CI pipelines or agent runtimes. Once recorded, custom events are immediately queryable via NRQL using the eventType value as the FROM table name.
import axios from 'axios';
// Insert Key (separate from User Key) — header is X-Insert-Key
const insightsCollector = axios.create({
baseURL: `https://insights-collector.newrelic.com/v1/accounts/${ACCOUNT_ID}`,
headers: {
'X-Insert-Key': process.env.NEW_RELIC_INSERT_KEY!,
'Content-Type': 'application/json',
},
});
server.tool('record_custom_event', {
event_type: z.string()
.regex(/^[a-zA-Z][a-zA-Z0-9_]{0,254}$/, 'eventType must start with a letter, contain only alphanumeric and underscores, max 255 chars')
.describe('The event type — becomes the table name in NRQL (e.g. "Deployment", "FeatureFlag", "CacheEviction").'),
attributes: z.record(z.union([z.string(), z.number(), z.boolean()]))
.describe('Key-value attributes to attach to the event. Values must be string, number, or boolean.')
}, async ({ event_type, attributes }) => {
// Body is a JSON array — you can batch multiple events in one request
const payload = [{ eventType: event_type, ...attributes }];
const res = await insightsCollector.post('/events', payload);
// Success returns HTTP 200 with { success: true }
return {
content: [{
type: 'text',
text: JSON.stringify({
success: res.data.success,
event_type,
attributes,
// Query immediately after recording:
nrql_to_verify: `SELECT * FROM ${event_type} SINCE 1 hour ago`
}, null, 2)
}]
};
});
| eventType constraint | Rule |
|---|---|
| First character | Must be a letter (a–z, A–Z) — numeric start is rejected |
| Allowed characters | Letters, digits, and underscores — no hyphens, spaces, or dots |
| Max length | 255 characters |
| Reserved names | Cannot start with NR (reserved for New Relic internal event types) |
| Attribute values | String, integer, float, or boolean — nested objects are not supported |
Example deployment event payload and its corresponding NRQL query:
// Record a deployment event
const deploymentEvent = [{
eventType: 'Deployment',
service: 'checkout',
version: '1.2.3',
deployer: 'ci-bot',
environment: 'production',
commit_sha: 'a1b2c3d4'
}];
// After posting, query via NRQL:
// SELECT * FROM Deployment SINCE 1 hour ago
// SELECT count(*) FROM Deployment FACET service SINCE 7 days ago
Wiring /health/newrelic via NerdGraph user query
The NerdGraph health probe uses a minimal query that fetches the authenticated user's name and email. Because NerdGraph always returns HTTP 200 — even for invalid or expired API keys — you must inspect the response body: a populated data.actor.user object means healthy, while an errors array in the response body means the key is invalid or lacks permissions. Never treat HTTP 200 alone as a health signal for NerdGraph.
const HEALTH_QUERY = `
{
actor {
user {
name
email
}
}
}
`;
app.get('/health/newrelic', async (req, res) => {
try {
const response = await nerdgraph.post('/graphql', { query: HEALTH_QUERY });
// NerdGraph always returns HTTP 200 — check the body for errors
if (response.data.errors?.length) {
return res.status(503).json({
status: 'unhealthy',
// Common error: "User authentication failed" when key is expired or revoked
error: response.data.errors[0].message,
type: response.data.errors[0].extensions?.errorClass
});
}
const user = response.data.data?.actor?.user;
if (!user) {
return res.status(503).json({
status: 'unhealthy',
error: 'NerdGraph returned no user data — key may lack permissions'
});
}
return res.json({
status: 'healthy',
user_name: user.name,
user_email: user.email,
account_id: ACCOUNT_ID
});
} catch (err: any) {
// A true HTTP error (network failure, timeout, or HTTP 403 format error)
// HTTP 403 from NerdGraph means the request format is wrong, NOT an auth error
return res.status(503).json({
status: 'unhealthy',
http_status: err.response?.status,
error: err.message
});
}
});
| Response | Meaning | Action |
|---|---|---|
HTTP 200, data.actor.user populated |
Key is valid and active | Return healthy |
HTTP 200, errors array present |
Key is invalid, expired, or revoked | Return 503 unhealthy — rotate the User Key |
| HTTP 403 | Request format error (malformed GraphQL) | Fix the query — this is not an auth error |
| HTTP 429 | Rate limit exceeded (600 req/min or 25 concurrent) | Back off and retry; surface as degraded, not unhealthy |
| Network timeout | New Relic API unreachable | Return 503 with error: "connection timeout" |
Frequently asked questions
Why does NerdGraph always return HTTP 200 even when my API key is invalid?
NerdGraph is a GraphQL API, and GraphQL APIs conventionally return HTTP 200 for all well-formed requests — including requests that result in application-level errors like authentication failures. The HTTP status code reflects whether the GraphQL layer received and processed the request, not whether your query succeeded. Auth errors, permission errors, and query syntax errors all arrive as HTTP 200 responses with an errors array in the JSON body. Your error handling must always check response.data.errors before accessing response.data.data. The only exceptions: HTTP 403 means your request format is malformed (not an auth error), and HTTP 429 means you have hit the rate limit.
What's the difference between User Keys, License Keys, and Insert Keys in New Relic?
New Relic has three distinct API credential types with non-overlapping purposes. User Keys (prefixed NRAK-) are personal credentials tied to a specific user account — they authenticate NerdGraph requests and inherit the user's role-based access control permissions. License Keys are account-level ingest credentials used by APM agents, infrastructure agents, and log forwarders to send telemetry data into New Relic; they cannot query data. Insert Keys are a write-only credential specifically for the Insights Collector API (custom event recording) — they can post events but cannot read any data via NerdGraph or REST. For MCP servers, use a User Key for all NerdGraph operations (queries, alert listing, entity search) and a separate Insert Key for record_custom_event. Never use a License Key in an MCP tool — it provides no query capability and should be treated as an infrastructure secret.
How do I convert NRQL results to time series for visualization?
Add TIMESERIES <bucket> to your NRQL query — for example, SELECT average(duration) FROM Transaction TIMESERIES 5 minutes SINCE 1 hour ago. When TIMESERIES is present, each result object in the array includes a beginTimeSeconds field (Unix epoch) and an endTimeSeconds field alongside the aggregated metric values. To convert to a chart-ready format in TypeScript: results.map(r => ({ timestamp: new Date(r.beginTimeSeconds * 1000), value: r['average.duration'] })). Note that the metric key in results uses dot notation based on your NRQL function and attribute — average(duration) becomes 'average.duration', count(*) becomes 'count'. For faceted time series (FACET appName TIMESERIES), each result also includes a facet field with the group value.
How do I handle multi-account New Relic organizations in MCP tools?
NerdGraph supports querying multiple accounts in a single request using the actor.accounts field or by parameterizing account IDs. For MCP tools serving multi-account organizations, the recommended pattern is to accept an optional account_id parameter (as shown in the query_nrql tool above) rather than hardcoding the account from the environment variable. Users in New Relic organizations with multiple accounts can find all accessible account IDs by querying NerdGraph: { actor { accounts { id name } } }. For cross-account queries (e.g., aggregating metrics across all accounts), use actor.entitySearch which operates across all accounts the User Key has access to, rather than looping through per-account NRQL calls. Rate limits apply per API key regardless of how many accounts are queried.