Guide · MCP Datadog Integration

MCP Server Datadog — query metrics, list monitors, post events, and /health via API key validate

Datadog is the backbone observability platform for thousands of engineering teams — storing metrics, powering monitors, and streaming events from every layer of the stack. This guide covers building TypeScript MCP tools for the Datadog API: two-header authentication with DD-API-KEY and DD-APPLICATION-KEY, querying timeseries metrics with the v1.MetricsApi, listing and muting monitors with state filters, posting deployment events to the event stream, and wiring a /health/datadog endpoint that validates your API key via the /api/v1/validate endpoint before AliveMCP catches silent credential failures.

TL;DR

Datadog API authentication requires two separate headers: DD-API-KEY identifies the caller and can submit metrics and events; DD-APPLICATION-KEY additionally authorizes read APIs like monitor listing and metric queries. The base URL is https://api.datadoghq.com for US1 — EU customers use https://api.datadoghq.eu. Metrics query from/to parameters are Unix epoch seconds, but returned pointlist timestamps are milliseconds. The metrics query endpoint is rate-limited to 300 requests per hour — much lower than other Datadog endpoints. Health: GET /api/v1/validate confirms the API key only; to check the Application key, make a lightweight read like GET /api/v1/monitor?page_size=1.

SDK setup and authentication

The official @datadog/datadog-api-client npm package provides full TypeScript types for every Datadog API version. It handles header injection, response parsing, and server variable substitution for multi-site support. For lightweight integrations, raw axios also works — you just need to set both authentication headers manually on every request.

import { client, v1, v2 } from '@datadog/datadog-api-client';

const configuration = client.createConfiguration({
  authMethods: {
    apiKeyAuth: process.env.DD_API_KEY!,
    appKeyAuth: process.env.DD_APP_KEY!,
  },
  // For EU customers: override the site variable
  // serverVariables: { site: 'datadoghq.eu' }
});

const metricsApi = new v1.MetricsApi(configuration);
const monitorsApi = new v1.MonitorsApi(configuration);
const eventsApi = new v2.EventsApi(configuration);

// If using raw axios instead of the SDK:
import axios from 'axios';

const ddHttp = axios.create({
  baseURL: 'https://api.datadoghq.com',
  headers: {
    'DD-API-KEY': process.env.DD_API_KEY!,
    'DD-APPLICATION-KEY': process.env.DD_APP_KEY!,
    'Content-Type': 'application/json',
  }
});
// EU customers: baseURL: 'https://api.datadoghq.eu'
Credential Header name What it authorizes
API Key DD-API-KEY Submit metrics, events, and service checks — write-only ingest
Application Key DD-APPLICATION-KEY All read APIs: query metrics, list monitors, search logs, dashboards
Both keys required Both headers Any read-after-write pattern — query metrics you just submitted
API Key alone DD-API-KEY only POST to /api/v1/series, /api/v1/events, /api/v1/check_run

Application Keys are scoped to the user who created them — they inherit that user's role permissions in Datadog. If the key owner's permissions change, API reads silently return 403. Use a dedicated service account user and its Application Key for MCP server credentials to decouple permissions from individual team members.

Querying timeseries metrics with query_metrics

The v1.MetricsApi.queryMetrics() method retrieves timeseries data for any Datadog metrics query string. The from and to parameters are Unix epoch seconds, but each point in the returned pointlist array uses millisecond timestamps — a common source of off-by-1000 bugs. The metrics query endpoint is rate-limited to 300 requests per hour, so avoid polling in tight loops.

server.tool('query_metrics', {
  from_time: z.number().int()
    .describe('Start of query window as Unix epoch seconds (e.g. Math.floor(Date.now()/1000) - 3600).'),
  to_time: z.number().int()
    .describe('End of query window as Unix epoch seconds.'),
  query: z.string()
    .describe('Datadog metrics query string, e.g. "avg:system.cpu.user{host:web-01}".')
}, async ({ from_time, to_time, query }) => {
  // queryMetrics takes from/to as seconds, not milliseconds
  const result = await metricsApi.queryMetrics({
    from: from_time,
    to: to_time,
    query,
  });

  // result.series is an array of matched timeseries
  const series = (result.series ?? []).map(s => ({
    metric: s.metric,
    scope: s.scope,
    // pointlist: array of [timestamp_ms, value] — timestamps ARE milliseconds
    points: (s.pointlist ?? []).map(([ts, val]) => ({
      timestamp_ms: ts,
      timestamp_iso: new Date(ts).toISOString(),
      value: val,
    })),
    start: s.start,
    end: s.end,
    interval: s.interval,
  }));

  return {
    content: [{
      type: 'text',
      text: JSON.stringify({ query, from_time, to_time, series }, null, 2)
    }]
  };
});
Field Type Notes
from / to number (seconds) Unix epoch seconds — not milliseconds
pointlist[n][0] number (milliseconds) Timestamp in milliseconds — divide by 1000 for epoch seconds
pointlist[n][1] number | null Metric value; null means no data for that rollup interval
interval number (seconds) Rollup interval Datadog selected based on query window length
scope string Tag filter scope from the query, e.g. host:web-01

Datadog automatically selects a rollup interval based on how long your query window spans. For a 1-hour window you get ~1-minute intervals; for a 7-day window you get ~1-hour intervals. If you need finer resolution, use the rollup() function in your query string: avg:system.cpu.user{*}.rollup(avg, 60) forces 60-second intervals.

Listing and muting monitors with list_monitors and mute_monitor

v1.MonitorsApi.listMonitors() returns all monitors in your account, with optional filtering by name substring or tags. Monitor tags use OR logic when comma-separated — passing env:prod,team:platform returns monitors tagged with either. Muting suppresses notifications without resolving the alert: a muted monitor can still change state, it just won't page anyone. Always add a confirm guard before muting.

server.tool('list_monitors', {
  tags: z.string().optional()
    .describe('Comma-separated monitor tags to filter by (OR logic). E.g. "env:prod,team:platform".'),
  name: z.string().optional()
    .describe('Filter monitors whose name contains this substring.'),
  monitor_states: z.array(
    z.enum(['Alert', 'Warn', 'OK', 'No Data', 'Ignored', 'Skipped', 'Unknown'])
  ).optional().describe('Only return monitors currently in these states.')
}, async ({ tags, name, monitor_states }) => {
  const monitors = await monitorsApi.listMonitors({ tags, name });

  const filtered = monitor_states
    ? monitors.filter(m => monitor_states.includes(m.overallState as any))
    : monitors;

  return {
    content: [{
      type: 'text',
      text: JSON.stringify(
        filtered.map(m => ({
          id: m.id,
          name: m.name,
          type: m.type,
          state: m.overallState,
          tags: m.tags,
          created: m.created,
          modified: m.modified,
        })),
        null, 2
      )
    }]
  };
});

server.tool('mute_monitor', {
  id: z.number().int().describe('Datadog monitor ID to mute.'),
  end: z.number().int().optional()
    .describe('Unix epoch seconds when the mute should lift. Omit to mute indefinitely.'),
  confirm: z.literal(true)
    .describe('Muting suppresses notifications but does not resolve the alert. Pass true to confirm.')
}, async ({ id, end }) => {
  // POST /api/v1/monitor/:id/mute — mutes the monitor
  // omitting end in the body mutes indefinitely
  const body = end !== undefined ? { end } : {};
  const result = await monitorsApi.muteMonitor({ monitorId: id, body });

  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        id: result.id,
        name: result.name,
        muted_until: end ? new Date(end * 1000).toISOString() : 'indefinitely',
        state: result.overallState,
      }, null, 2)
    }]
  };
});
Monitor state Meaning
Alert Monitor condition is met — notifications have fired
Warn Warning threshold crossed but not alert threshold
OK Condition is no longer met — monitor has recovered
No Data No metrics received for the query in the evaluation window
Ignored Monitor is muted — state may still change, notifications suppressed
Skipped Monitor was skipped due to downtimes or group-level muting
Unknown Monitor just created and not yet evaluated

Posting deployment events with post_event

The Datadog event stream is a timestamped log of significant occurrences — deployments, config changes, incidents, and custom annotations. Events can trigger event-based monitors and appear as overlays on metric graphs, making them critical for correlating metric spikes with deploys. The v2 EventsApi is the current interface; the v1 /api/v1/events endpoint also works and only requires an API key (no Application key needed).

server.tool('post_event', {
  title: z.string().max(100).describe('Event title — keep concise, shown in the event stream.'),
  text: z.string().max(4000)
    .describe('Event body. Supports Markdown. Include git SHA, deploy target, and timestamp.'),
  priority: z.enum(['normal', 'low']).default('normal')
    .describe('"normal" events appear in the stream; "low" events are aggregated.'),
  alert_type: z.enum(['info', 'warning', 'error', 'success']).default('info')
    .describe('Sets the event color and icon in the Datadog UI.'),
  tags: z.array(z.string()).optional()
    .describe('Tags to attach, e.g. ["env:prod", "service:api", "version:1.4.2"].')
}, async ({ title, text, priority, alert_type, tags }) => {
  // v1 events endpoint — only requires DD-API-KEY, no Application key needed
  const result = await ddHttp.post('/api/v1/events', {
    title,
    text,
    priority,
    alert_type,
    tags: tags ?? [],
    // source_type_name can help categorize events — e.g. 'my_apps', 'deploy'
  });

  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        id: result.data.event?.id,
        url: result.data.event?.url,
        title,
        alert_type,
        priority,
        tags,
        // Tip: include git SHA in text for deploy correlation:
        // text: `Deploy v1.4.2 (${gitSha}) to prod by ${user}`
      }, null, 2)
    }]
  };
});
Field Values Effect
alert_type info Blue icon — informational, e.g. deploys, config changes
alert_type warning Yellow icon — degraded state, non-critical issue
alert_type error Red icon — failure, outage, or critical state change
alert_type success Green icon — recovery, successful deploy, resolved incident
priority normal Visible individually in the event stream
priority low Collapsed into aggregated groups — use for high-volume automation events

Tagging events with version:<git-sha> and env:prod lets you overlay deploy markers on metric graphs in Datadog dashboards. When you see a CPU spike, you can immediately check whether a deploy event was posted at the same timestamp.

Wiring /health/datadog via the validate endpoint

The Datadog health probe has a subtlety: GET /api/v1/validate only validates the DD-API-KEY header — it does not check the Application key. A server where the API key is valid but the Application key is revoked will pass the validate check and then silently return 403 on every metrics query. The robust approach validates both independently.

app.get('/health/datadog', async (req, res) => {
  const checks: Record<string, unknown> = {};

  // Step 1: validate the API key via /api/v1/validate
  try {
    const validateRes = await ddHttp.get('/api/v1/validate');
    // Returns { valid: true } on success, 403 if invalid
    checks.api_key = validateRes.data.valid === true ? 'valid' : 'invalid';
  } catch (err: any) {
    checks.api_key = `invalid (HTTP ${err.response?.status})`;
    return res.status(503).json({ status: 'unhealthy', checks });
  }

  // Step 2: validate the Application key by making a lightweight read
  // GET /api/v1/monitor?page_size=1 requires the Application key
  try {
    await ddHttp.get('/api/v1/monitor', { params: { page_size: 1 } });
    checks.app_key = 'valid';
  } catch (err: any) {
    const status = err.response?.status;
    checks.app_key = status === 403
      ? 'invalid or insufficient permissions'
      : `error (HTTP ${status})`;
    return res.status(503).json({ status: 'unhealthy', checks });
  }

  return res.json({ status: 'healthy', checks });
});
HTTP status Endpoint Meaning
200 /api/v1/validate API key is valid — { "valid": true }
403 /api/v1/validate API key is invalid or revoked
200 /api/v1/monitor Application key is valid and has monitor read permissions
403 /api/v1/monitor Application key is missing, revoked, or user lacks monitor read role
429 Any endpoint Rate limit hit — back off; metrics queries limited to 300/hr

Register this /health/datadog endpoint with AliveMCP and set a check interval of 60 seconds. When the validate endpoint returns 403, AliveMCP will alert immediately — catching rotated or expired API keys before your metrics pipeline loses ingest and your monitors stop evaluating.

Frequently asked questions

What's the difference between DD-API-KEY and DD-APPLICATION-KEY?

The DD-API-KEY identifies your Datadog organization and authorizes write-only ingest endpoints: submitting metrics via /api/v1/series, posting events, and submitting service checks. It cannot read anything back. The DD-APPLICATION-KEY is tied to a specific Datadog user account and grants read access — querying metrics, listing monitors, searching logs, and reading dashboards. Most MCP tools need both: the API key to submit data and the Application key to read it back. If you find your reads returning 403 but writes succeeding, the Application key is missing or revoked. Application Keys inherit the role permissions of the user who created them, so a key from a read-only user cannot mute monitors even if the key itself is valid.

How do I handle Datadog's rate limits in MCP tools?

Datadog enforces different rate limits per endpoint family. The metrics query endpoint (/api/v1/query) is the most restrictive at 300 requests per hour — roughly one request every 12 seconds at sustained load. The monitors and events endpoints are more permissive at thousands of requests per hour. All Datadog API responses include rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the window resets). In your MCP tools, read X-RateLimit-Remaining on each response and surface a warning when it drops below 10. If you hit a 429, wait for the X-RateLimit-Reset seconds before retrying — do not implement exponential backoff that ignores this header, as the reset time is exact.

Which Datadog site should I use — datadoghq.com or datadoghq.eu?

Datadog operates multiple regional sites and your data only lives in the site where your account was created. The US1 site at https://api.datadoghq.com is the default and most common. EU customers whose data must stay in the European Union use https://api.datadoghq.eu. There are also US3 (https://api.us3.datadoghq.com), US5 (https://api.us5.datadoghq.com), and AP1 (https://api.ap1.datadoghq.com) sites for customers in those regions. If you're using the @datadog/datadog-api-client SDK, set the correct site via serverVariables: { site: 'datadoghq.eu' } in the configuration — the SDK substitutes this into its base URL automatically. If you're using raw axios, just change baseURL. Using the wrong site URL returns 403 or no data because your API keys only exist in your account's home site.

Can I use the Datadog MCP server with both metric queries and log queries?

Yes, but logs use a different API family. Metrics live under the v1.MetricsApi and use the query DSL described in this guide. Logs require the v2.LogsApi — specifically v2.LogsApi.listLogs() or v2.LogsApi.listLogsGet(), which accept a filter object with a Datadog Logs Query Language (DDSQL) string and a time range. Log queries are also subject to a separate rate limit, and you must have the logs_read_data permission on the Application Key's user role. A single MCP server can instantiate both v1.MetricsApi and v2.LogsApi using the same configuration object — there's no need for separate configurations. Just ensure the Application Key's user has both metrics_read and logs_read_data permissions in your Datadog role configuration.

Further reading

Know when your Datadog health probe fails before your metrics pipeline goes silent

AliveMCP polls your /health/datadog endpoint and alerts you the moment the validate check fails — catching rotated API keys, revoked Application keys, and site misconfigurations before your monitors stop evaluating and your ingest goes dark.

Start monitoring free