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
- MCP Server AWS CloudWatch — query metrics, describe alarms, and health probes via CloudWatch
- MCP Server Health Check — wiring /health endpoints for uptime monitoring
- MCP Server Error Handling — structured errors and retry patterns for MCP tools
- MCP Server Webhook Alerts — sending alert payloads to Slack, PagerDuty, and custom endpoints
- Building MCP Tools for DevOps Platforms — patterns for CI/CD, observability, and incident response