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.

Further reading

Monitor your New Relic health probe and catch API key expiry before your NRQL queries start silently returning empty results

AliveMCP polls your /health/newrelic endpoint and alerts you the moment the NerdGraph user query fails — catching expired User Keys, revoked credentials, and API outages before your dashboards and alerts go dark.

Start monitoring free