datadog-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4579 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config/schema.ts
4
+ import { z } from "zod";
5
+ var ALL_TOOLS = [
6
+ "monitors",
7
+ "dashboards",
8
+ "logs",
9
+ "metrics",
10
+ "traces",
11
+ "events",
12
+ "incidents",
13
+ "slos",
14
+ "synthetics",
15
+ "hosts",
16
+ "downtimes",
17
+ "rum",
18
+ "security",
19
+ "notebooks",
20
+ "users",
21
+ "teams",
22
+ "tags",
23
+ "usage",
24
+ "auth"
25
+ ];
26
+ var configSchema = z.object({
27
+ datadog: z.object({
28
+ apiKey: z.string().min(1, "DD_API_KEY is required"),
29
+ appKey: z.string().min(1, "DD_APP_KEY is required"),
30
+ site: z.string().default("datadoghq.com")
31
+ }),
32
+ server: z.object({
33
+ name: z.string().default("datadog-mcp"),
34
+ version: z.string().default("1.0.0"),
35
+ transport: z.enum(["stdio", "http"]).default("stdio"),
36
+ port: z.number().default(3e3),
37
+ host: z.string().default("localhost")
38
+ }),
39
+ limits: z.object({
40
+ maxResults: z.number().default(100),
41
+ maxLogLines: z.number().default(100),
42
+ // Reduced from 500 for token efficiency
43
+ defaultLimit: z.number().default(25),
44
+ // Default limit for initial queries
45
+ maxMetricDataPoints: z.number().default(1e3),
46
+ defaultTimeRangeHours: z.number().default(24)
47
+ }),
48
+ features: z.object({
49
+ readOnly: z.boolean().default(false),
50
+ disabledTools: z.array(z.string()).default([])
51
+ }).default({})
52
+ });
53
+
54
+ // src/config/index.ts
55
+ function parseArgs() {
56
+ const strings = {};
57
+ const booleans = /* @__PURE__ */ new Set();
58
+ const argv = process.argv.slice(2);
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const arg = argv[i];
61
+ if (!arg) continue;
62
+ if (arg.startsWith("--")) {
63
+ if (arg.includes("=")) {
64
+ const parts = arg.slice(2).split("=");
65
+ const key = parts[0];
66
+ const value = parts.slice(1).join("=");
67
+ if (key && value !== void 0) {
68
+ strings[key] = value;
69
+ }
70
+ } else {
71
+ const argName = arg.slice(2);
72
+ const nextArg = argv[i + 1];
73
+ if (nextArg && !nextArg.startsWith("--")) {
74
+ strings[argName] = nextArg;
75
+ i++;
76
+ } else {
77
+ booleans.add(argName);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return { strings, booleans };
83
+ }
84
+ function parseDisabledTools(value) {
85
+ if (!value) return [];
86
+ const requested = value.split(",").map((s) => s.trim().toLowerCase());
87
+ return requested.filter((t) => ALL_TOOLS.includes(t));
88
+ }
89
+ function loadConfig() {
90
+ const args = parseArgs();
91
+ const raw = {
92
+ datadog: {
93
+ apiKey: process.env.DD_API_KEY ?? "",
94
+ appKey: process.env.DD_APP_KEY ?? "",
95
+ site: args.strings.site ?? process.env.DD_SITE ?? "datadoghq.com"
96
+ },
97
+ server: {
98
+ name: "datadog-mcp",
99
+ version: "1.0.0",
100
+ transport: args.strings.transport ?? process.env.MCP_TRANSPORT ?? "stdio",
101
+ port: parseInt(args.strings.port ?? process.env.MCP_PORT ?? "3000", 10),
102
+ host: args.strings.host ?? process.env.MCP_HOST ?? "localhost"
103
+ },
104
+ limits: {
105
+ maxResults: parseInt(process.env.MCP_MAX_RESULTS ?? "100", 10),
106
+ maxLogLines: parseInt(process.env.MCP_MAX_LOG_LINES ?? "500", 10),
107
+ maxMetricDataPoints: parseInt(process.env.MCP_MAX_METRIC_POINTS ?? "1000", 10),
108
+ defaultTimeRangeHours: parseInt(process.env.MCP_DEFAULT_TIME_RANGE ?? "24", 10)
109
+ },
110
+ features: {
111
+ readOnly: args.booleans.has("read-only") || process.env.MCP_READ_ONLY === "true",
112
+ disabledTools: parseDisabledTools(args.strings["disable-tools"] ?? process.env.MCP_DISABLE_TOOLS)
113
+ }
114
+ };
115
+ return configSchema.parse(raw);
116
+ }
117
+
118
+ // src/server.ts
119
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
120
+
121
+ // src/config/datadog.ts
122
+ import { client, v1, v2 } from "@datadog/datadog-api-client";
123
+ function createDatadogClients(config) {
124
+ const configuration = client.createConfiguration({
125
+ authMethods: {
126
+ apiKeyAuth: config.apiKey,
127
+ appKeyAuth: config.appKey
128
+ }
129
+ });
130
+ if (config.site && config.site !== "datadoghq.com") {
131
+ configuration.setServerVariables({ site: config.site });
132
+ }
133
+ configuration.unstableOperations = {
134
+ "v2.listIncidents": true,
135
+ "v2.getIncident": true,
136
+ "v2.searchIncidents": true,
137
+ "v2.createIncident": true,
138
+ "v2.updateIncident": true,
139
+ "v2.deleteIncident": true
140
+ };
141
+ return {
142
+ monitors: new v1.MonitorsApi(configuration),
143
+ dashboards: new v1.DashboardsApi(configuration),
144
+ dashboardLists: new v1.DashboardListsApi(configuration),
145
+ logs: new v2.LogsApi(configuration),
146
+ metricsV1: new v1.MetricsApi(configuration),
147
+ metricsV2: new v2.MetricsApi(configuration),
148
+ eventsV1: new v1.EventsApi(configuration),
149
+ eventsV2: new v2.EventsApi(configuration),
150
+ incidents: new v2.IncidentsApi(configuration),
151
+ downtimes: new v2.DowntimesApi(configuration),
152
+ hosts: new v1.HostsApi(configuration),
153
+ slo: new v1.ServiceLevelObjectivesApi(configuration),
154
+ synthetics: new v1.SyntheticsApi(configuration),
155
+ rum: new v2.RUMApi(configuration),
156
+ security: new v2.SecurityMonitoringApi(configuration),
157
+ notebooks: new v1.NotebooksApi(configuration),
158
+ users: new v2.UsersApi(configuration),
159
+ teams: new v2.TeamsApi(configuration),
160
+ tags: new v1.TagsApi(configuration),
161
+ usage: new v1.UsageMeteringApi(configuration),
162
+ spans: new v2.SpansApi(configuration),
163
+ services: new v2.ServiceDefinitionApi(configuration),
164
+ auth: new v1.AuthenticationApi(configuration)
165
+ };
166
+ }
167
+
168
+ // src/tools/monitors.ts
169
+ import { z as z2 } from "zod";
170
+
171
+ // src/errors/datadog.ts
172
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
173
+ var DatadogErrorCode = {
174
+ /** 401 - Invalid or missing API/APP key */
175
+ Unauthorized: -32050,
176
+ /** 403 - Valid credentials but insufficient permissions */
177
+ Forbidden: -32051,
178
+ /** 404 - Requested resource does not exist */
179
+ NotFound: -32052,
180
+ /** 429 - Rate limit exceeded, should retry after delay */
181
+ RateLimited: -32053,
182
+ /** 5xx - Datadog service temporarily unavailable */
183
+ ServiceUnavailable: -32054
184
+ };
185
+ function handleDatadogError(error) {
186
+ console.error("[Datadog Error]", error);
187
+ if (error instanceof McpError) {
188
+ throw error;
189
+ }
190
+ const apiError = error;
191
+ if (typeof apiError.code === "number") {
192
+ const message = apiError.body?.errors?.[0] ?? apiError.message ?? "Unknown error";
193
+ switch (apiError.code) {
194
+ case 400:
195
+ throw new McpError(ErrorCode.InvalidRequest, `Invalid request: ${message}`);
196
+ case 401:
197
+ throw new McpError(DatadogErrorCode.Unauthorized, `Authentication failed: Invalid Datadog API key or APP key`);
198
+ case 403:
199
+ throw new McpError(DatadogErrorCode.Forbidden, `Authorization denied: ${message}`);
200
+ case 404:
201
+ throw new McpError(DatadogErrorCode.NotFound, `Resource not found: ${message}`);
202
+ case 429:
203
+ throw new McpError(DatadogErrorCode.RateLimited, "Rate limit exceeded. Retry after a short delay.");
204
+ case 500:
205
+ case 502:
206
+ case 503:
207
+ throw new McpError(DatadogErrorCode.ServiceUnavailable, "Datadog service temporarily unavailable. Retry later.");
208
+ default:
209
+ throw new McpError(ErrorCode.InternalError, `Datadog API error (${apiError.code}): ${message}`);
210
+ }
211
+ }
212
+ throw new McpError(
213
+ ErrorCode.InternalError,
214
+ error instanceof Error ? error.message : String(error)
215
+ );
216
+ }
217
+ function requireParam(value, name, action) {
218
+ if (value === void 0 || value === null || value === "") {
219
+ throw new McpError(
220
+ ErrorCode.InvalidParams,
221
+ `Parameter '${name}' is required for action '${action}'`
222
+ );
223
+ }
224
+ return value;
225
+ }
226
+ var WRITE_ACTIONS = /* @__PURE__ */ new Set([
227
+ "create",
228
+ "update",
229
+ "delete",
230
+ "mute",
231
+ "unmute",
232
+ "cancel",
233
+ "add",
234
+ "trigger"
235
+ ]);
236
+ function checkReadOnly(action, readOnly) {
237
+ if (readOnly && WRITE_ACTIONS.has(action)) {
238
+ throw new McpError(
239
+ ErrorCode.InvalidRequest,
240
+ `Action '${action}' is not allowed in read-only mode. Server started with --read-only flag.`
241
+ );
242
+ }
243
+ }
244
+
245
+ // src/utils/format.ts
246
+ function formatResponse(data) {
247
+ return [
248
+ {
249
+ type: "text",
250
+ text: JSON.stringify(data, null, 2)
251
+ }
252
+ ];
253
+ }
254
+ function toolResult(data) {
255
+ return {
256
+ content: formatResponse(data)
257
+ };
258
+ }
259
+
260
+ // src/utils/urls.ts
261
+ var SITE_TO_APP_URL = {
262
+ "datadoghq.com": "https://app.datadoghq.com",
263
+ "us3.datadoghq.com": "https://us3.datadoghq.com",
264
+ "us5.datadoghq.com": "https://us5.datadoghq.com",
265
+ "datadoghq.eu": "https://app.datadoghq.eu",
266
+ "ap1.datadoghq.com": "https://ap1.datadoghq.com",
267
+ "ddog-gov.com": "https://app.ddog-gov.com"
268
+ };
269
+ function getAppBaseUrl(site = "datadoghq.com") {
270
+ return SITE_TO_APP_URL[site] ?? SITE_TO_APP_URL["datadoghq.com"];
271
+ }
272
+ function toMs(seconds) {
273
+ return seconds * 1e3;
274
+ }
275
+ function buildLogsUrl(query, fromSec, toSec, site = "datadoghq.com") {
276
+ const base = getAppBaseUrl(site);
277
+ const params = new URLSearchParams({
278
+ query,
279
+ from_ts: toMs(fromSec).toString(),
280
+ to_ts: toMs(toSec).toString()
281
+ });
282
+ return `${base}/logs?${params.toString()}`;
283
+ }
284
+ function extractMetricName(query) {
285
+ const match = query.match(/^(?:\w+:)?([a-zA-Z0-9_.]+)/);
286
+ return match?.[1] ?? query;
287
+ }
288
+ function buildMetricsUrl(query, fromSec, toSec, site = "datadoghq.com") {
289
+ const base = getAppBaseUrl(site);
290
+ const metricName = extractMetricName(query);
291
+ const params = new URLSearchParams({
292
+ exp_metric: metricName,
293
+ exp_query: query,
294
+ from_ts: toMs(fromSec).toString(),
295
+ to_ts: toMs(toSec).toString()
296
+ });
297
+ return `${base}/metric/explorer?${params.toString()}`;
298
+ }
299
+ function buildTracesUrl(query, fromSec, toSec, site = "datadoghq.com") {
300
+ const base = getAppBaseUrl(site);
301
+ const params = new URLSearchParams({
302
+ query,
303
+ start: toMs(fromSec).toString(),
304
+ end: toMs(toSec).toString()
305
+ });
306
+ return `${base}/apm/traces?${params.toString()}`;
307
+ }
308
+ function buildEventsUrl(query, fromSec, toSec, site = "datadoghq.com") {
309
+ const base = getAppBaseUrl(site);
310
+ const params = new URLSearchParams({
311
+ query,
312
+ from_ts: toMs(fromSec).toString(),
313
+ to_ts: toMs(toSec).toString()
314
+ });
315
+ return `${base}/event/explorer?${params.toString()}`;
316
+ }
317
+ function buildMonitorUrl(monitorId, site = "datadoghq.com") {
318
+ const base = getAppBaseUrl(site);
319
+ return `${base}/monitors/${monitorId}`;
320
+ }
321
+ function buildMonitorsListUrl(query, site = "datadoghq.com") {
322
+ const base = getAppBaseUrl(site);
323
+ if (query) {
324
+ const params = new URLSearchParams({ query });
325
+ return `${base}/monitors/manage?${params.toString()}`;
326
+ }
327
+ return `${base}/monitors/manage`;
328
+ }
329
+ function buildRumUrl(query, fromSec, toSec, site = "datadoghq.com") {
330
+ const base = getAppBaseUrl(site);
331
+ const params = new URLSearchParams({
332
+ query,
333
+ from_ts: toMs(fromSec).toString(),
334
+ to_ts: toMs(toSec).toString()
335
+ });
336
+ return `${base}/rum/explorer?${params.toString()}`;
337
+ }
338
+ function buildRumSessionUrl(applicationId, sessionId, site = "datadoghq.com") {
339
+ const base = getAppBaseUrl(site);
340
+ return `${base}/rum/replay/sessions/${sessionId}?applicationId=${encodeURIComponent(applicationId)}`;
341
+ }
342
+
343
+ // src/tools/monitors.ts
344
+ var ActionSchema = z2.enum(["list", "get", "search", "create", "update", "delete", "mute", "unmute"]);
345
+ var InputSchema = {
346
+ action: ActionSchema.describe("Action to perform"),
347
+ id: z2.string().optional().describe("Monitor ID (required for get/update/delete/mute/unmute)"),
348
+ query: z2.string().optional().describe("Search query (for search action)"),
349
+ name: z2.string().optional().describe("Filter by name (for list action)"),
350
+ tags: z2.array(z2.string()).optional().describe("Filter by tags"),
351
+ groupStates: z2.array(z2.string()).optional().describe("Filter by group states: alert, warn, no data, ok"),
352
+ limit: z2.number().optional().describe("Maximum number of monitors to return"),
353
+ config: z2.record(z2.unknown()).optional().describe("Monitor configuration (for create/update)"),
354
+ message: z2.string().optional().describe("Mute message (for mute action)"),
355
+ end: z2.number().optional().describe("Mute end timestamp (for mute action)")
356
+ };
357
+ function formatMonitor(m) {
358
+ return {
359
+ id: m.id ?? 0,
360
+ name: m.name ?? "",
361
+ type: String(m.type ?? "unknown"),
362
+ status: String(m.overallState ?? "unknown"),
363
+ message: m.message ?? "",
364
+ tags: m.tags ?? [],
365
+ query: m.query ?? "",
366
+ created: m.created ? new Date(m.created).toISOString() : "",
367
+ modified: m.modified ? new Date(m.modified).toISOString() : ""
368
+ };
369
+ }
370
+ async function listMonitors(api, params, limits, site) {
371
+ const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
372
+ const response = await api.listMonitors({
373
+ name: params.name,
374
+ tags: params.tags?.join(","),
375
+ groupStates: params.groupStates?.join(",")
376
+ });
377
+ const monitors = response.slice(0, effectiveLimit).map(formatMonitor);
378
+ const statusCounts = {
379
+ total: response.length,
380
+ alert: response.filter((m) => m.overallState === "Alert").length,
381
+ warn: response.filter((m) => m.overallState === "Warn").length,
382
+ ok: response.filter((m) => m.overallState === "OK").length,
383
+ noData: response.filter((m) => m.overallState === "No Data").length
384
+ };
385
+ return {
386
+ monitors,
387
+ summary: statusCounts,
388
+ datadog_url: buildMonitorsListUrl(params.name, site)
389
+ };
390
+ }
391
+ async function getMonitor(api, id, site) {
392
+ const monitorId = parseInt(id, 10);
393
+ if (isNaN(monitorId)) {
394
+ throw new Error(`Invalid monitor ID: ${id}`);
395
+ }
396
+ const monitor = await api.getMonitor({ monitorId });
397
+ return {
398
+ monitor: formatMonitor(monitor),
399
+ datadog_url: buildMonitorUrl(monitorId, site)
400
+ };
401
+ }
402
+ async function searchMonitors(api, query, limits, site) {
403
+ const response = await api.searchMonitors({ query });
404
+ const monitors = (response.monitors ?? []).slice(0, limits.maxResults).map((m) => ({
405
+ id: m.id ?? 0,
406
+ name: m.name ?? "",
407
+ status: String(m.status ?? "unknown"),
408
+ type: m.type ?? "",
409
+ tags: m.tags ?? []
410
+ }));
411
+ return {
412
+ monitors,
413
+ metadata: {
414
+ totalCount: response.metadata?.totalCount ?? monitors.length,
415
+ pageCount: response.metadata?.pageCount ?? 1,
416
+ page: response.metadata?.page ?? 0
417
+ },
418
+ datadog_url: buildMonitorsListUrl(query, site)
419
+ };
420
+ }
421
+ function normalizeMonitorConfig(config) {
422
+ const normalized = { ...config };
423
+ if (!normalized.name && !normalized.type && !normalized.query) {
424
+ throw new Error("Monitor config requires at least 'name', 'type', and 'query' fields");
425
+ }
426
+ if (normalized.options && typeof normalized.options === "object") {
427
+ const opts = { ...normalized.options };
428
+ const optionMappings = [
429
+ ["notify_no_data", "notifyNoData"],
430
+ ["no_data_timeframe", "noDataTimeframe"],
431
+ ["new_host_delay", "newHostDelay"],
432
+ ["new_group_delay", "newGroupDelay"],
433
+ ["evaluation_delay", "evaluationDelay"],
434
+ ["renotify_interval", "renotifyInterval"],
435
+ ["renotify_occurrences", "renotifyOccurrences"],
436
+ ["renotify_statuses", "renotifyStatuses"],
437
+ ["timeout_h", "timeoutH"],
438
+ ["notify_audit", "notifyAudit"],
439
+ ["include_tags", "includeTags"],
440
+ ["require_full_window", "requireFullWindow"],
441
+ ["escalation_message", "escalationMessage"],
442
+ ["locked", "locked"],
443
+ ["silenced", "silenced"]
444
+ ];
445
+ for (const [snake, camel] of optionMappings) {
446
+ if (snake in opts && !(camel in opts)) {
447
+ opts[camel] = opts[snake];
448
+ delete opts[snake];
449
+ }
450
+ }
451
+ if (opts.thresholds && typeof opts.thresholds === "object") {
452
+ const thresholds = { ...opts.thresholds };
453
+ const thresholdMappings = [
454
+ ["critical", "critical"],
455
+ ["warning", "warning"],
456
+ ["ok", "ok"],
457
+ ["critical_recovery", "criticalRecovery"],
458
+ ["warning_recovery", "warningRecovery"]
459
+ ];
460
+ for (const [snake, camel] of thresholdMappings) {
461
+ if (snake in thresholds && !(camel in thresholds) && snake !== camel) {
462
+ thresholds[camel] = thresholds[snake];
463
+ delete thresholds[snake];
464
+ }
465
+ }
466
+ opts.thresholds = thresholds;
467
+ }
468
+ normalized.options = opts;
469
+ }
470
+ return normalized;
471
+ }
472
+ async function createMonitor(api, config) {
473
+ const body = normalizeMonitorConfig(config);
474
+ const monitor = await api.createMonitor({ body });
475
+ return {
476
+ success: true,
477
+ monitor: formatMonitor(monitor)
478
+ };
479
+ }
480
+ async function updateMonitor(api, id, config) {
481
+ const monitorId = parseInt(id, 10);
482
+ const body = normalizeMonitorConfig(config);
483
+ const monitor = await api.updateMonitor({ monitorId, body });
484
+ return {
485
+ success: true,
486
+ monitor: formatMonitor(monitor)
487
+ };
488
+ }
489
+ async function deleteMonitor(api, id) {
490
+ const monitorId = parseInt(id, 10);
491
+ await api.deleteMonitor({ monitorId });
492
+ return { success: true, message: `Monitor ${id} deleted` };
493
+ }
494
+ async function muteMonitor(api, id, params) {
495
+ const monitorId = parseInt(id, 10);
496
+ const monitor = await api.getMonitor({ monitorId });
497
+ await api.updateMonitor({
498
+ monitorId,
499
+ body: {
500
+ options: {
501
+ ...monitor.options,
502
+ silenced: { "*": params.end ?? null }
503
+ }
504
+ }
505
+ });
506
+ return { success: true, message: `Monitor ${id} muted` };
507
+ }
508
+ async function unmuteMonitor(api, id) {
509
+ const monitorId = parseInt(id, 10);
510
+ const monitor = await api.getMonitor({ monitorId });
511
+ await api.updateMonitor({
512
+ monitorId,
513
+ body: {
514
+ options: {
515
+ ...monitor.options,
516
+ silenced: {}
517
+ }
518
+ }
519
+ });
520
+ return { success: true, message: `Monitor ${id} unmuted` };
521
+ }
522
+ function registerMonitorsTool(server, api, limits, readOnly = false, site = "datadoghq.com") {
523
+ server.tool(
524
+ "monitors",
525
+ `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute.
526
+ Filters: name, tags, groupStates (alert/warn/ok/no data).
527
+ TIP: For alert HISTORY (which monitors triggered), use the events tool with tags: ["source:alert"].`,
528
+ InputSchema,
529
+ async ({ action, id, query, name, tags, groupStates, limit, config, end }) => {
530
+ try {
531
+ checkReadOnly(action, readOnly);
532
+ switch (action) {
533
+ case "list":
534
+ return toolResult(await listMonitors(api, { name, tags, groupStates, limit }, limits, site));
535
+ case "get": {
536
+ const monitorId = requireParam(id, "id", "get");
537
+ return toolResult(await getMonitor(api, monitorId, site));
538
+ }
539
+ case "search": {
540
+ const searchQuery = requireParam(query, "query", "search");
541
+ return toolResult(await searchMonitors(api, searchQuery, limits, site));
542
+ }
543
+ case "create": {
544
+ const monitorConfig = requireParam(config, "config", "create");
545
+ return toolResult(await createMonitor(api, monitorConfig));
546
+ }
547
+ case "update": {
548
+ const monitorId = requireParam(id, "id", "update");
549
+ const updateConfig = requireParam(config, "config", "update");
550
+ return toolResult(await updateMonitor(api, monitorId, updateConfig));
551
+ }
552
+ case "delete": {
553
+ const monitorId = requireParam(id, "id", "delete");
554
+ return toolResult(await deleteMonitor(api, monitorId));
555
+ }
556
+ case "mute": {
557
+ const monitorId = requireParam(id, "id", "mute");
558
+ return toolResult(await muteMonitor(api, monitorId, { end }));
559
+ }
560
+ case "unmute": {
561
+ const monitorId = requireParam(id, "id", "unmute");
562
+ return toolResult(await unmuteMonitor(api, monitorId));
563
+ }
564
+ default:
565
+ throw new Error(`Unknown action: ${action}`);
566
+ }
567
+ } catch (error) {
568
+ handleDatadogError(error);
569
+ }
570
+ }
571
+ );
572
+ }
573
+
574
+ // src/tools/dashboards.ts
575
+ import { z as z3 } from "zod";
576
+ var ActionSchema2 = z3.enum(["list", "get", "create", "update", "delete"]);
577
+ var InputSchema2 = {
578
+ action: ActionSchema2.describe("Action to perform"),
579
+ id: z3.string().optional().describe("Dashboard ID (required for get/update/delete)"),
580
+ name: z3.string().optional().describe("Filter by name"),
581
+ tags: z3.array(z3.string()).optional().describe("Filter by tags"),
582
+ limit: z3.number().optional().describe("Maximum number of dashboards to return"),
583
+ config: z3.record(z3.unknown()).optional().describe("Dashboard configuration (for create/update)")
584
+ };
585
+ function formatDashboardSummary(d) {
586
+ return {
587
+ id: d.id ?? "",
588
+ title: d.title ?? "",
589
+ description: d.description ?? "",
590
+ url: d.url ?? "",
591
+ layoutType: String(d.layoutType ?? "unknown"),
592
+ created: d.createdAt ? new Date(d.createdAt).toISOString() : "",
593
+ modified: d.modifiedAt ? new Date(d.modifiedAt).toISOString() : "",
594
+ authorHandle: d.authorHandle ?? ""
595
+ };
596
+ }
597
+ async function listDashboards(api, params, limits) {
598
+ const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
599
+ const response = await api.listDashboards({
600
+ filterShared: false
601
+ });
602
+ let dashboards = response.dashboards ?? [];
603
+ if (params.name) {
604
+ const lowerName = params.name.toLowerCase();
605
+ dashboards = dashboards.filter(
606
+ (d) => d.title?.toLowerCase().includes(lowerName)
607
+ );
608
+ }
609
+ const result = dashboards.slice(0, effectiveLimit).map(formatDashboardSummary);
610
+ return {
611
+ dashboards: result,
612
+ total: response.dashboards?.length ?? 0
613
+ };
614
+ }
615
+ async function getDashboard(api, id) {
616
+ const dashboard = await api.getDashboard({ dashboardId: id });
617
+ return {
618
+ dashboard: {
619
+ id: dashboard.id ?? "",
620
+ title: dashboard.title ?? "",
621
+ description: dashboard.description ?? "",
622
+ layoutType: String(dashboard.layoutType ?? "unknown"),
623
+ widgets: dashboard.widgets?.length ?? 0,
624
+ url: dashboard.url ?? "",
625
+ created: dashboard.createdAt ? new Date(dashboard.createdAt).toISOString() : "",
626
+ modified: dashboard.modifiedAt ? new Date(dashboard.modifiedAt).toISOString() : "",
627
+ authorHandle: dashboard.authorHandle ?? ""
628
+ }
629
+ };
630
+ }
631
+ function normalizeDashboardConfig(config) {
632
+ const normalized = { ...config };
633
+ if ("layout_type" in normalized && !("layoutType" in normalized)) {
634
+ normalized.layoutType = normalized.layout_type;
635
+ delete normalized.layout_type;
636
+ }
637
+ if (!normalized.layoutType) {
638
+ throw new Error("Dashboard config requires 'layoutType' (e.g., 'ordered', 'free')");
639
+ }
640
+ return normalized;
641
+ }
642
+ async function createDashboard(api, config) {
643
+ const body = normalizeDashboardConfig(config);
644
+ const dashboard = await api.createDashboard({ body });
645
+ return {
646
+ success: true,
647
+ dashboard: {
648
+ id: dashboard.id ?? "",
649
+ title: dashboard.title ?? "",
650
+ url: dashboard.url ?? ""
651
+ }
652
+ };
653
+ }
654
+ async function updateDashboard(api, id, config) {
655
+ const body = normalizeDashboardConfig(config);
656
+ const dashboard = await api.updateDashboard({ dashboardId: id, body });
657
+ return {
658
+ success: true,
659
+ dashboard: {
660
+ id: dashboard.id ?? "",
661
+ title: dashboard.title ?? "",
662
+ url: dashboard.url ?? ""
663
+ }
664
+ };
665
+ }
666
+ async function deleteDashboard(api, id) {
667
+ await api.deleteDashboard({ dashboardId: id });
668
+ return { success: true, message: `Dashboard ${id} deleted` };
669
+ }
670
+ function registerDashboardsTool(server, api, limits, readOnly = false) {
671
+ server.tool(
672
+ "dashboards",
673
+ "Access Datadog dashboards and visualizations. Actions: list (filter by name/tags), get, create, update, delete. Use for: finding existing views, team dashboards, understanding what is monitored.",
674
+ InputSchema2,
675
+ async ({ action, id, name, tags, limit, config }) => {
676
+ try {
677
+ checkReadOnly(action, readOnly);
678
+ switch (action) {
679
+ case "list":
680
+ return toolResult(await listDashboards(api, { name, tags, limit }, limits));
681
+ case "get": {
682
+ const dashboardId = requireParam(id, "id", "get");
683
+ return toolResult(await getDashboard(api, dashboardId));
684
+ }
685
+ case "create": {
686
+ const dashboardConfig = requireParam(config, "config", "create");
687
+ return toolResult(await createDashboard(api, dashboardConfig));
688
+ }
689
+ case "update": {
690
+ const dashboardId = requireParam(id, "id", "update");
691
+ const updateConfig = requireParam(config, "config", "update");
692
+ return toolResult(await updateDashboard(api, dashboardId, updateConfig));
693
+ }
694
+ case "delete": {
695
+ const dashboardId = requireParam(id, "id", "delete");
696
+ return toolResult(await deleteDashboard(api, dashboardId));
697
+ }
698
+ default:
699
+ throw new Error(`Unknown action: ${action}`);
700
+ }
701
+ } catch (error) {
702
+ handleDatadogError(error);
703
+ }
704
+ }
705
+ );
706
+ }
707
+
708
+ // src/tools/logs.ts
709
+ import { z as z4 } from "zod";
710
+
711
+ // src/utils/time.ts
712
+ function hoursAgo(hours) {
713
+ return Math.floor(Date.now() / 1e3) - hours * 3600;
714
+ }
715
+ function now() {
716
+ return Math.floor(Date.now() / 1e3);
717
+ }
718
+ function startOfDayAgo(days) {
719
+ const date = /* @__PURE__ */ new Date();
720
+ date.setDate(date.getDate() - days);
721
+ date.setHours(0, 0, 0, 0);
722
+ return date;
723
+ }
724
+ function parseTime(input, defaultValue) {
725
+ if (input === void 0) {
726
+ return defaultValue;
727
+ }
728
+ if (typeof input === "number") {
729
+ return input;
730
+ }
731
+ const trimmed = input.trim();
732
+ const simpleRelativeMatch = trimmed.match(/^(\d+)(s|m|h|d)$/);
733
+ if (simpleRelativeMatch) {
734
+ const value = parseInt(simpleRelativeMatch[1] ?? "0", 10);
735
+ const unit = simpleRelativeMatch[2];
736
+ const nowTs = now();
737
+ switch (unit) {
738
+ case "s":
739
+ return nowTs - value;
740
+ case "m":
741
+ return nowTs - value * 60;
742
+ case "h":
743
+ return nowTs - value * 3600;
744
+ case "d":
745
+ return nowTs - value * 86400;
746
+ default:
747
+ return defaultValue;
748
+ }
749
+ }
750
+ const relativeWithTimeMatch = trimmed.match(/^(\d+)(d|h)[@\s](\d{1,2}):(\d{2})(?::(\d{2}))?$/);
751
+ if (relativeWithTimeMatch) {
752
+ const value = parseInt(relativeWithTimeMatch[1] ?? "0", 10);
753
+ const unit = relativeWithTimeMatch[2];
754
+ const hours = parseInt(relativeWithTimeMatch[3] ?? "0", 10);
755
+ const minutes = parseInt(relativeWithTimeMatch[4] ?? "0", 10);
756
+ const seconds = parseInt(relativeWithTimeMatch[5] ?? "0", 10);
757
+ if (unit === "d") {
758
+ const date3 = startOfDayAgo(value);
759
+ date3.setHours(hours, minutes, seconds, 0);
760
+ return Math.floor(date3.getTime() / 1e3);
761
+ }
762
+ const date2 = /* @__PURE__ */ new Date();
763
+ date2.setHours(date2.getHours() - value);
764
+ date2.setMinutes(minutes, seconds, 0);
765
+ return Math.floor(date2.getTime() / 1e3);
766
+ }
767
+ const keywordMatch = trimmed.match(/^(today|yesterday)[@\s](\d{1,2}):(\d{2})(?::(\d{2}))?$/i);
768
+ if (keywordMatch) {
769
+ const keyword = keywordMatch[1]?.toLowerCase();
770
+ const hours = parseInt(keywordMatch[2] ?? "0", 10);
771
+ const minutes = parseInt(keywordMatch[3] ?? "0", 10);
772
+ const seconds = parseInt(keywordMatch[4] ?? "0", 10);
773
+ const daysAgo = keyword === "yesterday" ? 1 : 0;
774
+ const date2 = startOfDayAgo(daysAgo);
775
+ date2.setHours(hours, minutes, seconds, 0);
776
+ return Math.floor(date2.getTime() / 1e3);
777
+ }
778
+ const date = new Date(trimmed);
779
+ if (!isNaN(date.getTime())) {
780
+ return Math.floor(date.getTime() / 1e3);
781
+ }
782
+ const ts = parseInt(trimmed, 10);
783
+ if (!isNaN(ts)) {
784
+ return ts;
785
+ }
786
+ return defaultValue;
787
+ }
788
+ function ensureValidTimeRange(from, to, minRangeSeconds = 60) {
789
+ if (from > to) {
790
+ [from, to] = [to, from];
791
+ }
792
+ if (to - from < minRangeSeconds) {
793
+ to = from + minRangeSeconds;
794
+ }
795
+ return [from, to];
796
+ }
797
+ function parseDurationToNs(input) {
798
+ if (input === void 0) {
799
+ return void 0;
800
+ }
801
+ if (typeof input === "number") {
802
+ return input;
803
+ }
804
+ const trimmed = input.trim().toLowerCase();
805
+ const match = trimmed.match(/^(\d+(?:\.\d+)?)(ns|µs|us|ms|s|m|h|d|w)?$/);
806
+ if (!match) {
807
+ const raw = parseInt(trimmed, 10);
808
+ return isNaN(raw) ? void 0 : raw;
809
+ }
810
+ const value = parseFloat(match[1] ?? "0");
811
+ const unit = match[2] ?? "ns";
812
+ const multipliers = {
813
+ "ns": 1,
814
+ "\xB5s": 1e3,
815
+ "us": 1e3,
816
+ "ms": 1e6,
817
+ "s": 1e9,
818
+ "m": 6e10,
819
+ "h": 36e11,
820
+ "d": 864e11,
821
+ "w": 6048e11
822
+ };
823
+ return Math.floor(value * (multipliers[unit] ?? 1));
824
+ }
825
+ function formatDurationNs(ns) {
826
+ if (ns < 1e3) return `${ns}ns`;
827
+ if (ns < 1e6) return `${(ns / 1e3).toFixed(1)}\xB5s`;
828
+ if (ns < 1e9) return `${(ns / 1e6).toFixed(1)}ms`;
829
+ if (ns < 6e10) return `${(ns / 1e9).toFixed(2)}s`;
830
+ return `${(ns / 6e10).toFixed(2)}m`;
831
+ }
832
+
833
+ // src/tools/logs.ts
834
+ var ActionSchema3 = z4.enum(["search", "aggregate"]);
835
+ var InputSchema3 = {
836
+ action: ActionSchema3.describe("Action to perform"),
837
+ query: z4.string().optional().describe('Log search query (Datadog syntax). Examples: "error", "service:my-service status:error", "error AND timeout"'),
838
+ keyword: z4.string().optional().describe("Simple text search - finds logs containing this text (grep-like). Merged with query using AND"),
839
+ pattern: z4.string().optional().describe('Regex pattern to match in log message (grep -E style). Example: "ERROR.*timeout|connection refused"'),
840
+ from: z4.string().optional().describe("Start time. Formats: ISO 8601, relative (30s, 15m, 2h, 7d), precise (3d@11:45:23, yesterday@14:00)"),
841
+ to: z4.string().optional().describe('End time. Same formats as "from". Example: from="3d@11:45:23" to="3d@12:55:34"'),
842
+ service: z4.string().optional().describe("Filter by service name"),
843
+ host: z4.string().optional().describe("Filter by host"),
844
+ status: z4.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by log status/level"),
845
+ indexes: z4.array(z4.string()).optional().describe("Log indexes to search"),
846
+ limit: z4.number().optional().describe("Maximum number of logs to return"),
847
+ sort: z4.enum(["timestamp", "-timestamp"]).optional().describe("Sort order"),
848
+ sample: z4.enum(["first", "spread", "diverse"]).optional().describe("Sampling mode: first (chronological, default), spread (evenly across time range), diverse (distinct message patterns)"),
849
+ compact: z4.boolean().optional().describe("Strip custom attributes for token efficiency. Keeps: id, timestamp, service, status, message (truncated), dd.trace_id, error info"),
850
+ groupBy: z4.array(z4.string()).optional().describe("Fields to group by (for aggregate)"),
851
+ compute: z4.record(z4.unknown()).optional().describe("Compute operations (for aggregate)")
852
+ };
853
+ function formatLog(log) {
854
+ const attrs = log.attributes ?? {};
855
+ let timestamp = "";
856
+ if (attrs.timestamp) {
857
+ const ts = attrs.timestamp;
858
+ timestamp = ts instanceof Date ? ts.toISOString() : new Date(String(ts)).toISOString();
859
+ }
860
+ return {
861
+ id: log.id ?? "",
862
+ timestamp,
863
+ service: attrs.service ?? "",
864
+ host: attrs.host ?? "",
865
+ status: attrs.status ?? "",
866
+ message: attrs.message ?? "",
867
+ tags: attrs.tags ?? [],
868
+ attributes: attrs.attributes ?? {}
869
+ };
870
+ }
871
+ function formatLogCompact(log) {
872
+ const attrs = log.attributes ?? {};
873
+ const nestedAttrs = attrs.attributes ?? {};
874
+ let timestamp = "";
875
+ if (attrs.timestamp) {
876
+ const ts = attrs.timestamp;
877
+ timestamp = ts instanceof Date ? ts.toISOString() : new Date(String(ts)).toISOString();
878
+ }
879
+ const traceId = nestedAttrs["dd.trace_id"] ?? nestedAttrs["trace_id"] ?? attrs["dd.trace_id"] ?? "";
880
+ const spanId = nestedAttrs["dd.span_id"] ?? nestedAttrs["span_id"] ?? attrs["dd.span_id"] ?? "";
881
+ const errorType = nestedAttrs["error.type"] ?? nestedAttrs["error.kind"] ?? "";
882
+ const errorMessage = nestedAttrs["error.message"] ?? nestedAttrs["error.msg"] ?? "";
883
+ const fullMessage = attrs.message ?? "";
884
+ const message = fullMessage.length > 500 ? fullMessage.slice(0, 500) + "..." : fullMessage;
885
+ const entry = {
886
+ id: log.id ?? "",
887
+ timestamp,
888
+ service: attrs.service ?? "",
889
+ host: attrs.host ?? "",
890
+ status: attrs.status ?? "",
891
+ message,
892
+ traceId,
893
+ spanId
894
+ };
895
+ if (errorType || errorMessage) {
896
+ entry.error = {
897
+ type: errorType,
898
+ message: errorMessage.length > 200 ? errorMessage.slice(0, 200) + "..." : errorMessage
899
+ };
900
+ }
901
+ return entry;
902
+ }
903
+ function normalizeToPattern(message) {
904
+ return message.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "{UUID}").replace(/\b[0-9a-f]{16,}\b/gi, "{HEX}").replace(/\b[0-9a-f]{8,15}\b/gi, "{ID}").replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.\dZ]*/g, "{TS}").replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, "{IP}").replace(/\b\d{4,}\b/g, "{N}").slice(0, 200);
905
+ }
906
+ function spreadSample(items, limit) {
907
+ if (items.length <= limit) return items;
908
+ const step = items.length / limit;
909
+ return Array.from({ length: limit }, (_, i) => items[Math.floor(i * step)]);
910
+ }
911
+ function diverseSample(items, limit) {
912
+ const seen = /* @__PURE__ */ new Map();
913
+ for (const item of items) {
914
+ const pattern = normalizeToPattern(item.message);
915
+ if (!seen.has(pattern)) {
916
+ seen.set(pattern, item);
917
+ if (seen.size >= limit) break;
918
+ }
919
+ }
920
+ return {
921
+ samples: Array.from(seen.values()),
922
+ patterns: seen.size
923
+ };
924
+ }
925
+ function buildLogQuery(params) {
926
+ const parts = [];
927
+ if (params.query) {
928
+ parts.push(params.query);
929
+ }
930
+ if (params.keyword) {
931
+ const escaped = params.keyword.replace(/"/g, '\\"');
932
+ parts.push(`"${escaped}"`);
933
+ }
934
+ if (params.pattern) {
935
+ const escaped = params.pattern.replace(/"/g, '\\"');
936
+ parts.push(`@message:~"${escaped}"`);
937
+ }
938
+ if (params.service) {
939
+ parts.push(`service:${params.service}`);
940
+ }
941
+ if (params.host) {
942
+ parts.push(`host:${params.host}`);
943
+ }
944
+ if (params.status) {
945
+ parts.push(`status:${params.status}`);
946
+ }
947
+ return parts.length > 0 ? parts.join(" ") : "*";
948
+ }
949
+ async function searchLogs(api, params, limits, site) {
950
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
951
+ const defaultTo = now();
952
+ const [validFrom, validTo] = ensureValidTimeRange(
953
+ parseTime(params.from, defaultFrom),
954
+ parseTime(params.to, defaultTo)
955
+ );
956
+ const fromTime = new Date(validFrom * 1e3).toISOString();
957
+ const toTime = new Date(validTo * 1e3).toISOString();
958
+ const fullQuery = buildLogQuery({
959
+ query: params.query,
960
+ keyword: params.keyword,
961
+ pattern: params.pattern,
962
+ service: params.service,
963
+ host: params.host,
964
+ status: params.status
965
+ });
966
+ const requestedLimit = params.limit ?? limits.defaultLimit;
967
+ const sampleMode = params.sample ?? "first";
968
+ const fetchMultiplier = sampleMode === "first" ? 1 : 4;
969
+ const fetchLimit = Math.min(requestedLimit * fetchMultiplier, limits.maxLogLines);
970
+ const body = {
971
+ filter: {
972
+ query: fullQuery,
973
+ from: fromTime,
974
+ to: toTime,
975
+ indexes: params.indexes
976
+ },
977
+ sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
978
+ page: {
979
+ limit: fetchLimit
980
+ }
981
+ };
982
+ const response = await api.listLogs({ body });
983
+ const formattedLogs = params.compact ? (response.data ?? []).map(formatLogCompact) : (response.data ?? []).map(formatLog);
984
+ let logs;
985
+ let distinctPatterns;
986
+ switch (sampleMode) {
987
+ case "spread":
988
+ logs = spreadSample(formattedLogs, requestedLimit);
989
+ break;
990
+ case "diverse": {
991
+ const result = diverseSample(formattedLogs, requestedLimit);
992
+ logs = result.samples;
993
+ distinctPatterns = result.patterns;
994
+ break;
995
+ }
996
+ case "first":
997
+ default:
998
+ logs = formattedLogs.slice(0, requestedLimit);
999
+ }
1000
+ return {
1001
+ logs,
1002
+ meta: {
1003
+ count: logs.length,
1004
+ query: fullQuery,
1005
+ from: fromTime,
1006
+ to: toTime,
1007
+ compact: params.compact ?? false,
1008
+ sample: sampleMode,
1009
+ ...sampleMode !== "first" && { fetched: formattedLogs.length },
1010
+ ...distinctPatterns !== void 0 && { distinctPatterns },
1011
+ datadog_url: buildLogsUrl(fullQuery, validFrom, validTo, site)
1012
+ }
1013
+ };
1014
+ }
1015
+ async function aggregateLogs(api, params, limits, site) {
1016
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1017
+ const defaultTo = now();
1018
+ const [validFrom, validTo] = ensureValidTimeRange(
1019
+ parseTime(params.from, defaultFrom),
1020
+ parseTime(params.to, defaultTo)
1021
+ );
1022
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1023
+ const toTime = new Date(validTo * 1e3).toISOString();
1024
+ const computeOps = params.compute ? [params.compute] : [{ aggregation: "count", type: "total" }];
1025
+ const body = {
1026
+ filter: {
1027
+ query: params.query,
1028
+ from: fromTime,
1029
+ to: toTime
1030
+ },
1031
+ compute: computeOps,
1032
+ groupBy: params.groupBy?.map((field) => ({
1033
+ facet: field,
1034
+ limit: 10
1035
+ }))
1036
+ };
1037
+ const response = await api.aggregateLogs({ body });
1038
+ return {
1039
+ buckets: response.data?.buckets ?? [],
1040
+ meta: {
1041
+ query: params.query,
1042
+ from: fromTime,
1043
+ to: toTime,
1044
+ groupBy: params.groupBy,
1045
+ datadog_url: buildLogsUrl(params.query, validFrom, validTo, site)
1046
+ }
1047
+ };
1048
+ }
1049
+ function registerLogsTool(server, api, limits, site = "datadoghq.com") {
1050
+ server.tool(
1051
+ "logs",
1052
+ `Search Datadog logs with grep-like text filtering. Actions: search (find logs), aggregate (count/group). Key filters: keyword (text grep), pattern (regex), service, host, status (error/warn/info). Time ranges: "1h", "3d@11:45:23".
1053
+ CORRELATION: Logs contain dd.trace_id in attributes for linking to traces and APM metrics.
1054
+ SAMPLING: Use sample:"diverse" for error investigation (dedupes by message pattern), sample:"spread" for time distribution.
1055
+ TOKEN TIP: Use compact:true to reduce payload size (strips heavy fields) when querying large volumes.`,
1056
+ InputSchema3,
1057
+ async ({ action, query, keyword, pattern, service, host, status, from, to, indexes, limit, sort, sample, compact, groupBy, compute }) => {
1058
+ try {
1059
+ switch (action) {
1060
+ case "search": {
1061
+ return toolResult(await searchLogs(api, {
1062
+ query,
1063
+ keyword,
1064
+ pattern,
1065
+ service,
1066
+ host,
1067
+ status,
1068
+ from,
1069
+ to,
1070
+ indexes,
1071
+ limit,
1072
+ sort,
1073
+ sample,
1074
+ compact
1075
+ }, limits, site));
1076
+ }
1077
+ case "aggregate": {
1078
+ const aggregateQuery = requireParam(query, "query", "aggregate");
1079
+ return toolResult(await aggregateLogs(api, {
1080
+ query: aggregateQuery,
1081
+ from,
1082
+ to,
1083
+ groupBy,
1084
+ compute
1085
+ }, limits, site));
1086
+ }
1087
+ default:
1088
+ throw new Error(`Unknown action: ${action}`);
1089
+ }
1090
+ } catch (error) {
1091
+ handleDatadogError(error);
1092
+ }
1093
+ }
1094
+ );
1095
+ }
1096
+
1097
+ // src/tools/metrics.ts
1098
+ import { z as z5 } from "zod";
1099
+ var ActionSchema4 = z5.enum(["query", "search", "list", "metadata"]);
1100
+ var InputSchema4 = {
1101
+ action: ActionSchema4.describe("Action to perform"),
1102
+ query: z5.string().optional().describe('For query: PromQL expression (e.g., "avg:system.cpu.user{*}"). For search: grep-like filter on metric names. For list: tag filter.'),
1103
+ from: z5.string().optional().describe("Start time (ONLY for query action). Formats: ISO 8601, relative (30s, 15m, 2h, 7d), precise (3d@11:45:23)"),
1104
+ to: z5.string().optional().describe('End time (ONLY for query action). Same formats as "from".'),
1105
+ metric: z5.string().optional().describe("Metric name (for metadata action)"),
1106
+ tag: z5.string().optional().describe("Filter by tag"),
1107
+ limit: z5.number().optional().describe("Maximum number of results (for search/list)")
1108
+ };
1109
+ async function queryMetrics(api, params, limits, site) {
1110
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1111
+ const defaultTo = now();
1112
+ const [fromTs, toTs] = ensureValidTimeRange(
1113
+ parseTime(params.from, defaultFrom),
1114
+ parseTime(params.to, defaultTo)
1115
+ );
1116
+ const response = await api.queryMetrics({
1117
+ from: fromTs,
1118
+ to: toTs,
1119
+ query: params.query
1120
+ });
1121
+ const series = (response.series ?? []).map((s) => ({
1122
+ metric: s.metric ?? "",
1123
+ points: (s.pointlist ?? []).slice(0, limits.maxMetricDataPoints).map((p) => ({
1124
+ timestamp: p[0] ?? 0,
1125
+ value: p[1] ?? 0
1126
+ })),
1127
+ scope: s.scope ?? "",
1128
+ tags: s.tagSet ?? []
1129
+ }));
1130
+ return {
1131
+ series,
1132
+ meta: {
1133
+ query: params.query,
1134
+ from: new Date(fromTs * 1e3).toISOString(),
1135
+ to: new Date(toTs * 1e3).toISOString(),
1136
+ seriesCount: series.length,
1137
+ datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site)
1138
+ }
1139
+ };
1140
+ }
1141
+ async function searchMetrics(api, params, limits) {
1142
+ const response = await api.listActiveMetrics({
1143
+ from: hoursAgo(24),
1144
+ host: void 0,
1145
+ tagFilter: void 0
1146
+ // Must match listMetrics exactly
1147
+ });
1148
+ const allMetrics = response.metrics ?? [];
1149
+ const lowerQuery = params.query.toLowerCase();
1150
+ const filtered = allMetrics.filter((name) => name.toLowerCase().includes(lowerQuery)).slice(0, params.limit ?? limits.maxResults);
1151
+ return {
1152
+ metrics: filtered,
1153
+ total: filtered.length,
1154
+ searchedFrom: allMetrics.length
1155
+ };
1156
+ }
1157
+ async function listMetrics(api, params, limits) {
1158
+ const response = await api.listActiveMetrics({
1159
+ from: hoursAgo(24),
1160
+ host: void 0,
1161
+ tagFilter: params.query
1162
+ });
1163
+ const metrics = (response.metrics ?? []).slice(0, limits.maxResults);
1164
+ return {
1165
+ metrics,
1166
+ total: response.metrics?.length ?? 0
1167
+ };
1168
+ }
1169
+ async function getMetricMetadata(api, metricName) {
1170
+ const metadata = await api.getMetricMetadata({ metricName });
1171
+ return {
1172
+ metric: metricName,
1173
+ description: metadata.description ?? "",
1174
+ unit: metadata.unit ?? "",
1175
+ perUnit: metadata.perUnit ?? "",
1176
+ type: metadata.type ?? "",
1177
+ shortName: metadata.shortName ?? "",
1178
+ integration: metadata.integration ?? ""
1179
+ };
1180
+ }
1181
+ function registerMetricsTool(server, metricsV1Api, metricsV2Api, limits, site = "datadoghq.com") {
1182
+ server.tool(
1183
+ "metrics",
1184
+ `Query Datadog metrics. Actions:
1185
+ - query: Get timeseries data (requires from/to time range, PromQL query)
1186
+ - search: Find metrics by name (grep-like, NO time param needed)
1187
+ - list: Get recently active metrics (last 24h, optionally filter by tag)
1188
+ - metadata: Get metric details (unit, type, description)
1189
+
1190
+ APM METRICS (auto-generated from traces):
1191
+ - trace.{service}.hits - Request count
1192
+ - trace.{service}.errors - Error count
1193
+ - trace.{service}.duration - Latency (use avg:, p95:, max:)
1194
+ Example: max:trace.{service}.request.duration{*}`,
1195
+ InputSchema4,
1196
+ async ({ action, query, from, to, metric, limit }) => {
1197
+ try {
1198
+ switch (action) {
1199
+ case "query": {
1200
+ const metricsQuery = requireParam(query, "query", "query");
1201
+ return toolResult(await queryMetrics(metricsV1Api, {
1202
+ query: metricsQuery,
1203
+ from,
1204
+ to
1205
+ }, limits, site));
1206
+ }
1207
+ case "search": {
1208
+ const searchQuery = requireParam(query, "query", "search");
1209
+ return toolResult(await searchMetrics(metricsV1Api, {
1210
+ query: searchQuery,
1211
+ limit
1212
+ }, limits));
1213
+ }
1214
+ case "list":
1215
+ return toolResult(await listMetrics(metricsV1Api, { query }, limits));
1216
+ case "metadata": {
1217
+ const metricName = requireParam(metric, "metric", "metadata");
1218
+ return toolResult(await getMetricMetadata(metricsV1Api, metricName));
1219
+ }
1220
+ default:
1221
+ throw new Error(`Unknown action: ${action}`);
1222
+ }
1223
+ } catch (error) {
1224
+ handleDatadogError(error);
1225
+ }
1226
+ }
1227
+ );
1228
+ }
1229
+
1230
+ // src/tools/traces.ts
1231
+ import { z as z6 } from "zod";
1232
+ var ActionSchema5 = z6.enum(["search", "aggregate", "services"]);
1233
+ var RESERVED_SPAN_FACETS = /* @__PURE__ */ new Set([
1234
+ "service",
1235
+ "resource_name",
1236
+ "operation_name",
1237
+ "span_name",
1238
+ "status",
1239
+ "env",
1240
+ "host",
1241
+ "type",
1242
+ "duration",
1243
+ "trace_id",
1244
+ "span_id"
1245
+ ]);
1246
+ var InputSchema5 = {
1247
+ action: ActionSchema5.describe("Action to perform"),
1248
+ query: z6.string().optional().describe('APM trace search query (Datadog syntax). Example: "@http.status_code:500", "service:my-service status:error"'),
1249
+ from: z6.string().optional().describe("Start time. Formats: ISO 8601, relative (30s, 15m, 2h, 7d), precise (3d@11:45:23, yesterday@14:00)"),
1250
+ to: z6.string().optional().describe('End time. Same formats as "from". Example: from="3d@11:45" to="3d@12:55"'),
1251
+ service: z6.string().optional().describe('Filter by service name. Example: "my-service", "postgres"'),
1252
+ operation: z6.string().optional().describe('Filter by operation name. Example: "express.request", "mongodb.query"'),
1253
+ resource: z6.string().optional().describe('Filter by resource name (endpoint/query). Supports wildcards. Example: "GET /api/*", "*orders*"'),
1254
+ status: z6.enum(["ok", "error"]).optional().describe('Filter by span status - "ok" for successful, "error" for failed spans'),
1255
+ env: z6.string().optional().describe('Filter by environment. Example: "production", "staging"'),
1256
+ minDuration: z6.string().optional().describe('Minimum span duration (find slow spans). Examples: "1s", "500ms", "100ms"'),
1257
+ maxDuration: z6.string().optional().describe('Maximum span duration. Examples: "5s", "1000ms"'),
1258
+ httpStatus: z6.string().optional().describe('HTTP status code filter. Examples: "500", "5xx" (500-599), "4xx" (400-499), ">=400"'),
1259
+ errorType: z6.string().optional().describe('Filter by error type (grep-like). Example: "TimeoutError", "ConnectionRefused"'),
1260
+ errorMessage: z6.string().optional().describe('Filter by error message (grep-like). Example: "timeout", "connection refused"'),
1261
+ limit: z6.number().optional().describe("Maximum number of results"),
1262
+ sort: z6.enum(["timestamp", "-timestamp"]).optional().describe("Sort order"),
1263
+ groupBy: z6.array(z6.string()).optional().describe('Fields to group by (for aggregate). Example: ["resource_name", "status"]')
1264
+ };
1265
+ function formatSpan(span) {
1266
+ const attrs = span.attributes ?? {};
1267
+ const tags = attrs.tags ?? [];
1268
+ const nestedAttrs = attrs.attributes ?? {};
1269
+ const custom = attrs.custom ?? {};
1270
+ const tagMap = {};
1271
+ for (const tag of tags) {
1272
+ const [key, value] = tag.split(":");
1273
+ if (key && value) tagMap[key] = value;
1274
+ }
1275
+ let durationNs = 0;
1276
+ if (attrs.startTimestamp && attrs.endTimestamp) {
1277
+ const startMs = attrs.startTimestamp.getTime();
1278
+ const endMs = attrs.endTimestamp.getTime();
1279
+ durationNs = (endMs - startMs) * 1e6;
1280
+ } else if (typeof nestedAttrs["duration"] === "number") {
1281
+ durationNs = nestedAttrs["duration"];
1282
+ } else if (typeof custom["duration"] === "number") {
1283
+ durationNs = custom["duration"];
1284
+ }
1285
+ const status = nestedAttrs["status"] ?? custom["status"] ?? tagMap["status"] ?? "";
1286
+ return {
1287
+ traceId: attrs.traceId ?? "",
1288
+ spanId: attrs.spanId ?? "",
1289
+ service: attrs.service ?? "",
1290
+ resource: attrs.resourceName ?? "",
1291
+ operation: nestedAttrs["operation_name"] ?? custom["operation_name"] ?? "",
1292
+ type: attrs.type ?? "",
1293
+ status,
1294
+ duration: formatDurationNs(durationNs),
1295
+ durationNs,
1296
+ http: {
1297
+ statusCode: tagMap["http.status_code"] ?? "",
1298
+ method: tagMap["http.method"] ?? "",
1299
+ url: tagMap["http.url"] ?? ""
1300
+ },
1301
+ error: {
1302
+ type: tagMap["error.type"] ?? "",
1303
+ message: tagMap["error.message"] ?? tagMap["error.msg"] ?? ""
1304
+ },
1305
+ env: attrs.env ?? tagMap["env"] ?? "",
1306
+ tags
1307
+ };
1308
+ }
1309
+ function buildTraceQuery(params) {
1310
+ const parts = [];
1311
+ if (params.query) {
1312
+ parts.push(params.query);
1313
+ }
1314
+ if (params.service) {
1315
+ parts.push(`service:${params.service}`);
1316
+ }
1317
+ if (params.operation) {
1318
+ parts.push(`operation_name:${params.operation}`);
1319
+ }
1320
+ if (params.resource) {
1321
+ if (params.resource.includes("*") || params.resource.includes(" ")) {
1322
+ parts.push(`resource_name:${params.resource}`);
1323
+ } else {
1324
+ parts.push(`resource_name:${params.resource}`);
1325
+ }
1326
+ }
1327
+ if (params.status) {
1328
+ parts.push(`status:${params.status}`);
1329
+ }
1330
+ if (params.env) {
1331
+ parts.push(`env:${params.env}`);
1332
+ }
1333
+ if (params.minDuration) {
1334
+ const ns = parseDurationToNs(params.minDuration);
1335
+ if (ns !== void 0) {
1336
+ parts.push(`@duration:>=${ns}`);
1337
+ }
1338
+ }
1339
+ if (params.maxDuration) {
1340
+ const ns = parseDurationToNs(params.maxDuration);
1341
+ if (ns !== void 0) {
1342
+ parts.push(`@duration:<=${ns}`);
1343
+ }
1344
+ }
1345
+ if (params.httpStatus) {
1346
+ const status = params.httpStatus.toLowerCase();
1347
+ if (status.endsWith("xx")) {
1348
+ const base = parseInt(status[0] ?? "0", 10) * 100;
1349
+ parts.push(`@http.status_code:[${base} TO ${base + 99}]`);
1350
+ } else if (status.startsWith(">=")) {
1351
+ parts.push(`@http.status_code:>=${status.slice(2)}`);
1352
+ } else if (status.startsWith(">")) {
1353
+ parts.push(`@http.status_code:>${status.slice(1)}`);
1354
+ } else if (status.startsWith("<=")) {
1355
+ parts.push(`@http.status_code:<=${status.slice(2)}`);
1356
+ } else if (status.startsWith("<")) {
1357
+ parts.push(`@http.status_code:<${status.slice(1)}`);
1358
+ } else {
1359
+ parts.push(`@http.status_code:${params.httpStatus}`);
1360
+ }
1361
+ }
1362
+ if (params.errorType) {
1363
+ const escaped = params.errorType.replace(/"/g, '\\"');
1364
+ parts.push(`error.type:*${escaped}*`);
1365
+ }
1366
+ if (params.errorMessage) {
1367
+ const escaped = params.errorMessage.replace(/"/g, '\\"');
1368
+ parts.push(`error.message:*${escaped}*`);
1369
+ }
1370
+ return parts.length > 0 ? parts.join(" ") : "*";
1371
+ }
1372
+ async function searchTraces(api, params, limits, site) {
1373
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1374
+ const defaultTo = now();
1375
+ const fullQuery = buildTraceQuery({
1376
+ query: params.query,
1377
+ service: params.service,
1378
+ operation: params.operation,
1379
+ resource: params.resource,
1380
+ status: params.status,
1381
+ env: params.env,
1382
+ minDuration: params.minDuration,
1383
+ maxDuration: params.maxDuration,
1384
+ httpStatus: params.httpStatus,
1385
+ errorType: params.errorType,
1386
+ errorMessage: params.errorMessage
1387
+ });
1388
+ const [validFrom, validTo] = ensureValidTimeRange(
1389
+ parseTime(params.from, defaultFrom),
1390
+ parseTime(params.to, defaultTo)
1391
+ );
1392
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1393
+ const toTime = new Date(validTo * 1e3).toISOString();
1394
+ const body = {
1395
+ data: {
1396
+ type: "search_request",
1397
+ attributes: {
1398
+ filter: {
1399
+ query: fullQuery,
1400
+ from: fromTime,
1401
+ to: toTime
1402
+ },
1403
+ sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
1404
+ page: {
1405
+ limit: Math.min(params.limit ?? limits.maxResults, limits.maxResults)
1406
+ }
1407
+ }
1408
+ }
1409
+ };
1410
+ const response = await api.listSpans({ body });
1411
+ const spans = (response.data ?? []).map(formatSpan);
1412
+ return {
1413
+ spans,
1414
+ meta: {
1415
+ count: spans.length,
1416
+ query: fullQuery,
1417
+ from: fromTime,
1418
+ to: toTime,
1419
+ datadog_url: buildTracesUrl(fullQuery, validFrom, validTo, site)
1420
+ }
1421
+ };
1422
+ }
1423
+ async function aggregateTraces(api, params, limits, site) {
1424
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1425
+ const defaultTo = now();
1426
+ const fullQuery = buildTraceQuery({
1427
+ query: params.query,
1428
+ service: params.service,
1429
+ operation: params.operation,
1430
+ resource: params.resource,
1431
+ status: params.status,
1432
+ env: params.env,
1433
+ minDuration: params.minDuration,
1434
+ maxDuration: params.maxDuration,
1435
+ httpStatus: params.httpStatus,
1436
+ errorType: params.errorType,
1437
+ errorMessage: params.errorMessage
1438
+ });
1439
+ const [validFrom, validTo] = ensureValidTimeRange(
1440
+ parseTime(params.from, defaultFrom),
1441
+ parseTime(params.to, defaultTo)
1442
+ );
1443
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1444
+ const toTime = new Date(validTo * 1e3).toISOString();
1445
+ const body = {
1446
+ data: {
1447
+ type: "aggregate_request",
1448
+ attributes: {
1449
+ filter: {
1450
+ query: fullQuery,
1451
+ from: fromTime,
1452
+ to: toTime
1453
+ },
1454
+ compute: [{ aggregation: "count", type: "total" }],
1455
+ groupBy: params.groupBy?.map((field) => ({
1456
+ facet: RESERVED_SPAN_FACETS.has(field) || field.startsWith("@") ? field : `@${field}`,
1457
+ limit: 10
1458
+ }))
1459
+ }
1460
+ }
1461
+ };
1462
+ const response = await api.aggregateSpans({ body });
1463
+ return {
1464
+ data: response.data ?? [],
1465
+ meta: {
1466
+ query: fullQuery,
1467
+ from: fromTime,
1468
+ to: toTime,
1469
+ groupBy: params.groupBy,
1470
+ datadog_url: buildTracesUrl(fullQuery, validFrom, validTo, site)
1471
+ }
1472
+ };
1473
+ }
1474
+ async function listApmServices(api, params, limits) {
1475
+ const defaultFrom = hoursAgo(24);
1476
+ const defaultTo = now();
1477
+ const [validFrom, validTo] = ensureValidTimeRange(
1478
+ parseTime(params.from, defaultFrom),
1479
+ parseTime(params.to, defaultTo)
1480
+ );
1481
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1482
+ const toTime = new Date(validTo * 1e3).toISOString();
1483
+ const query = params.env ? `env:${params.env}` : "*";
1484
+ const body = {
1485
+ data: {
1486
+ type: "aggregate_request",
1487
+ attributes: {
1488
+ filter: {
1489
+ query,
1490
+ from: fromTime,
1491
+ to: toTime
1492
+ },
1493
+ compute: [{ aggregation: "count", type: "total" }],
1494
+ groupBy: [{
1495
+ facet: "service",
1496
+ limit: limits.maxResults
1497
+ }]
1498
+ }
1499
+ }
1500
+ };
1501
+ const response = await api.aggregateSpans({ body });
1502
+ const buckets = response.data ?? [];
1503
+ const services = buckets.map((bucket) => ({
1504
+ name: bucket.attributes?.by?.["service"] ?? "",
1505
+ spanCount: bucket.attributes?.computes?.["c0"] ?? 0
1506
+ })).filter((s) => s.name !== "");
1507
+ return {
1508
+ services,
1509
+ total: services.length,
1510
+ meta: {
1511
+ query,
1512
+ env: params.env ?? "all",
1513
+ from: fromTime,
1514
+ to: toTime
1515
+ }
1516
+ };
1517
+ }
1518
+ function registerTracesTool(server, spansApi, _servicesApi, limits, site = "datadoghq.com") {
1519
+ server.tool(
1520
+ "traces",
1521
+ `Analyze APM traces for request flow and latency debugging. Actions: search (find spans), aggregate (group stats), services (list APM services). Key filters: minDuration/maxDuration ("500ms", "2s"), httpStatus ("5xx", ">=400"), status (ok/error), errorMessage (grep).
1522
+ APM METRICS: Traces auto-generate metrics in trace.{service}.* namespace. Use metrics tool to query: avg:trace.{service}.request.duration{*}`,
1523
+ InputSchema5,
1524
+ async ({ action, query, from, to, service, operation, resource, status, env, minDuration, maxDuration, httpStatus, errorType, errorMessage, limit, sort, groupBy }) => {
1525
+ try {
1526
+ switch (action) {
1527
+ case "search": {
1528
+ return toolResult(await searchTraces(spansApi, {
1529
+ query,
1530
+ from,
1531
+ to,
1532
+ service,
1533
+ operation,
1534
+ resource,
1535
+ status,
1536
+ env,
1537
+ minDuration,
1538
+ maxDuration,
1539
+ httpStatus,
1540
+ errorType,
1541
+ errorMessage,
1542
+ limit,
1543
+ sort
1544
+ }, limits, site));
1545
+ }
1546
+ case "aggregate": {
1547
+ return toolResult(await aggregateTraces(spansApi, {
1548
+ query,
1549
+ from,
1550
+ to,
1551
+ service,
1552
+ operation,
1553
+ resource,
1554
+ status,
1555
+ env,
1556
+ minDuration,
1557
+ maxDuration,
1558
+ httpStatus,
1559
+ errorType,
1560
+ errorMessage,
1561
+ groupBy
1562
+ }, limits, site));
1563
+ }
1564
+ case "services":
1565
+ return toolResult(await listApmServices(spansApi, { env, from, to }, limits));
1566
+ default:
1567
+ throw new Error(`Unknown action: ${action}`);
1568
+ }
1569
+ } catch (error) {
1570
+ handleDatadogError(error);
1571
+ }
1572
+ }
1573
+ );
1574
+ }
1575
+
1576
+ // src/tools/events.ts
1577
+ import { z as z7 } from "zod";
1578
+ var ActionSchema6 = z7.enum(["list", "get", "create", "search", "aggregate", "top", "timeseries", "incidents"]);
1579
+ var InputSchema6 = {
1580
+ action: ActionSchema6.describe("Action to perform"),
1581
+ id: z7.string().optional().describe("Event ID (for get action)"),
1582
+ query: z7.string().optional().describe("Search query"),
1583
+ from: z7.string().optional().describe('Start time (ISO 8601, relative like "1h", or Unix timestamp)'),
1584
+ to: z7.string().optional().describe('End time (ISO 8601, relative like "1h", or Unix timestamp)'),
1585
+ priority: z7.enum(["normal", "low"]).optional().describe("Event priority"),
1586
+ sources: z7.array(z7.string()).optional().describe("Filter by sources"),
1587
+ tags: z7.array(z7.string()).optional().describe("Filter by tags"),
1588
+ limit: z7.number().optional().describe("Maximum number of events to return"),
1589
+ title: z7.string().optional().describe("Event title (for create)"),
1590
+ text: z7.string().optional().describe("Event text (for create)"),
1591
+ alertType: z7.enum(["error", "warning", "info", "success"]).optional().describe("Alert type (for create)"),
1592
+ groupBy: z7.array(z7.string()).optional().describe("Fields to group by: monitor_name, priority, alert_type, source"),
1593
+ cursor: z7.string().optional().describe("Pagination cursor from previous response"),
1594
+ // Phase 2: Timeseries
1595
+ interval: z7.string().optional().describe("Time bucket interval for timeseries: 1h, 4h, 1d (default: 1h)"),
1596
+ // Phase 2: Incidents deduplication
1597
+ dedupeWindow: z7.string().optional().describe("Deduplication window for incidents: 5m, 15m, 1h (default: 5m)"),
1598
+ // Phase 3: Monitor enrichment
1599
+ enrich: z7.boolean().optional().describe("Enrich events with monitor metadata (slower, adds monitor details)")
1600
+ };
1601
+ function extractMonitorInfo(title) {
1602
+ const priorityMatch = title.match(/^\[P(\d+)\]\s*/);
1603
+ const priority = priorityMatch ? `P${priorityMatch[1]}` : void 0;
1604
+ const withoutPriority = title.replace(/^\[P\d+\]\s*/, "");
1605
+ const match = withoutPriority.match(
1606
+ /^\[(Triggered|Recovered|Warn|Alert|OK|No Data|Re-Triggered|Renotify)(?:\s+on\s+\{([^}]+)\})?\]\s*(.+)$/i
1607
+ );
1608
+ if (match) {
1609
+ return {
1610
+ status: match[1] ?? "",
1611
+ scope: match[2] ?? "",
1612
+ name: match[3]?.trim() ?? title,
1613
+ priority
1614
+ };
1615
+ }
1616
+ return { status: "", scope: "", name: title, priority };
1617
+ }
1618
+ function extractTitleFromMessage(message) {
1619
+ if (!message) return "";
1620
+ const content = message.replace(/^%%%\s*\n?/, "").trim();
1621
+ const firstLine = content.split("\n")[0]?.trim() ?? "";
1622
+ return firstLine.replace(/\s+!?\s*$/, "").trim();
1623
+ }
1624
+ function extractMonitorIdFromMessage(message) {
1625
+ if (!message) return void 0;
1626
+ const match = message.match(/\/monitors\/(\d+)/);
1627
+ if (match && match[1]) {
1628
+ const id = parseInt(match[1], 10);
1629
+ return isNaN(id) ? void 0 : id;
1630
+ }
1631
+ return void 0;
1632
+ }
1633
+ function buildGroupKey(event, groupBy) {
1634
+ const parts = [];
1635
+ for (const field of groupBy) {
1636
+ switch (field) {
1637
+ case "monitor_name":
1638
+ parts.push(event.monitorInfo?.name ?? event.title);
1639
+ break;
1640
+ case "monitor_id":
1641
+ parts.push(event.monitorId?.toString() ?? "");
1642
+ break;
1643
+ case "priority":
1644
+ parts.push(event.monitorInfo?.priority ?? event.priority);
1645
+ break;
1646
+ case "source":
1647
+ parts.push(event.source);
1648
+ break;
1649
+ case "alert_type":
1650
+ parts.push(event.alertType);
1651
+ break;
1652
+ case "status":
1653
+ parts.push(event.monitorInfo?.status ?? "");
1654
+ break;
1655
+ case "host":
1656
+ parts.push(event.host);
1657
+ break;
1658
+ default: {
1659
+ const tagValue = event.tags.find((t) => t.startsWith(`${field}:`))?.split(":")[1] ?? "";
1660
+ parts.push(tagValue);
1661
+ }
1662
+ }
1663
+ }
1664
+ return parts.join("|");
1665
+ }
1666
+ function formatEventV1(e) {
1667
+ const event = e;
1668
+ return {
1669
+ id: e.id ?? 0,
1670
+ title: e.title ?? "",
1671
+ text: e.text ?? "",
1672
+ dateHappened: e.dateHappened ? new Date(e.dateHappened * 1e3).toISOString() : "",
1673
+ priority: String(e.priority ?? "normal"),
1674
+ source: event.sourceTypeName ?? "",
1675
+ tags: e.tags ?? [],
1676
+ alertType: String(e.alertType ?? "info"),
1677
+ host: e.host ?? ""
1678
+ };
1679
+ }
1680
+ function formatEventV2(e) {
1681
+ const attrs = e.attributes ?? {};
1682
+ let timestamp = "";
1683
+ if (attrs.timestamp) {
1684
+ const ts = attrs.timestamp;
1685
+ timestamp = ts instanceof Date ? ts.toISOString() : new Date(String(ts)).toISOString();
1686
+ }
1687
+ const message = attrs.message ?? "";
1688
+ let title = attrs.title ?? "";
1689
+ if (!title && message) {
1690
+ title = extractTitleFromMessage(message);
1691
+ }
1692
+ const monitorInfo = extractMonitorInfo(title);
1693
+ const monitorId = extractMonitorIdFromMessage(message);
1694
+ const tags = attrs.tags ?? [];
1695
+ const sourceTag = tags.find((t) => t.startsWith("source:"));
1696
+ const source = sourceTag?.split(":")[1] ?? "";
1697
+ const alertTypeTag = tags.find((t) => t.startsWith("alert_type:"));
1698
+ const alertType = alertTypeTag?.split(":")[1] ?? "";
1699
+ const hostTag = tags.find((t) => t.startsWith("host:"));
1700
+ const host = hostTag?.split(":")[1] ?? "";
1701
+ const priorityTag = tags.find((t) => t.startsWith("priority:"));
1702
+ const priority = priorityTag?.split(":")[1] ?? "normal";
1703
+ return {
1704
+ id: String(e.id ?? ""),
1705
+ title,
1706
+ message,
1707
+ timestamp,
1708
+ priority,
1709
+ source,
1710
+ tags,
1711
+ alertType,
1712
+ host,
1713
+ monitorId,
1714
+ monitorInfo: monitorInfo.name !== title ? {
1715
+ name: monitorInfo.name,
1716
+ status: monitorInfo.status,
1717
+ scope: monitorInfo.scope,
1718
+ priority: monitorInfo.priority
1719
+ } : void 0
1720
+ };
1721
+ }
1722
+ async function listEventsV1(api, params, limits) {
1723
+ const effectiveLimit = Math.min(params.limit ?? limits.defaultLimit, limits.maxResults);
1724
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1725
+ const defaultTo = now();
1726
+ const response = await api.listEvents({
1727
+ start: parseTime(params.from, defaultFrom),
1728
+ end: parseTime(params.to, defaultTo),
1729
+ priority: params.priority === "low" ? "low" : "normal",
1730
+ sources: params.sources?.join(","),
1731
+ tags: params.tags?.join(","),
1732
+ unaggregated: true
1733
+ });
1734
+ let events = response.events ?? [];
1735
+ if (params.query) {
1736
+ const lowerQuery = params.query.toLowerCase();
1737
+ events = events.filter(
1738
+ (e) => e.title?.toLowerCase().includes(lowerQuery) || e.text?.toLowerCase().includes(lowerQuery)
1739
+ );
1740
+ }
1741
+ const result = events.slice(0, effectiveLimit).map(formatEventV1);
1742
+ return {
1743
+ events: result,
1744
+ total: events.length
1745
+ };
1746
+ }
1747
+ async function getEventV1(api, id) {
1748
+ const eventId = parseInt(id, 10);
1749
+ if (isNaN(eventId)) {
1750
+ throw new Error(`Invalid event ID: ${id}`);
1751
+ }
1752
+ const response = await api.getEvent({ eventId });
1753
+ return { event: formatEventV1(response.event ?? {}) };
1754
+ }
1755
+ async function createEventV1(api, params) {
1756
+ const body = {
1757
+ title: params.title,
1758
+ text: params.text,
1759
+ priority: params.priority === "low" ? "low" : "normal",
1760
+ tags: params.tags,
1761
+ alertType: params.alertType ?? "info"
1762
+ };
1763
+ const response = await api.createEvent({ body });
1764
+ return {
1765
+ success: true,
1766
+ event: {
1767
+ id: response.event?.id ?? 0,
1768
+ title: response.event?.title ?? "",
1769
+ status: response.status ?? ""
1770
+ }
1771
+ };
1772
+ }
1773
+ function buildEventQuery(params) {
1774
+ const parts = [];
1775
+ if (params.query) {
1776
+ parts.push(params.query);
1777
+ }
1778
+ if (params.sources && params.sources.length > 0) {
1779
+ const sourceFilter = params.sources.map((s) => `source:${s}`).join(" OR ");
1780
+ parts.push(`(${sourceFilter})`);
1781
+ }
1782
+ if (params.tags && params.tags.length > 0) {
1783
+ for (const tag of params.tags) {
1784
+ parts.push(tag);
1785
+ }
1786
+ }
1787
+ if (params.priority) {
1788
+ parts.push(`priority:${params.priority}`);
1789
+ }
1790
+ return parts.length > 0 ? parts.join(" ") : "*";
1791
+ }
1792
+ async function searchEventsV2(api, params, limits, site) {
1793
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1794
+ const defaultTo = now();
1795
+ const [validFrom, validTo] = ensureValidTimeRange(
1796
+ parseTime(params.from, defaultFrom),
1797
+ parseTime(params.to, defaultTo)
1798
+ );
1799
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1800
+ const toTime = new Date(validTo * 1e3).toISOString();
1801
+ const fullQuery = buildEventQuery({
1802
+ query: params.query,
1803
+ sources: params.sources,
1804
+ tags: params.tags,
1805
+ priority: params.priority
1806
+ });
1807
+ const effectiveLimit = Math.min(params.limit ?? limits.defaultLimit, limits.maxResults);
1808
+ const body = {
1809
+ filter: {
1810
+ query: fullQuery,
1811
+ from: fromTime,
1812
+ to: toTime
1813
+ },
1814
+ sort: "timestamp",
1815
+ page: {
1816
+ limit: effectiveLimit,
1817
+ cursor: params.cursor
1818
+ }
1819
+ };
1820
+ const response = await api.searchEvents({ body });
1821
+ const events = (response.data ?? []).map(formatEventV2);
1822
+ const nextCursor = response.meta?.page?.after;
1823
+ return {
1824
+ events,
1825
+ meta: {
1826
+ count: events.length,
1827
+ query: fullQuery,
1828
+ from: fromTime,
1829
+ to: toTime,
1830
+ nextCursor,
1831
+ datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
1832
+ }
1833
+ };
1834
+ }
1835
+ async function aggregateEventsV2(api, params, limits, site) {
1836
+ const counts = /* @__PURE__ */ new Map();
1837
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1838
+ const defaultTo = now();
1839
+ const [validFrom, validTo] = ensureValidTimeRange(
1840
+ parseTime(params.from, defaultFrom),
1841
+ parseTime(params.to, defaultTo)
1842
+ );
1843
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1844
+ const toTime = new Date(validTo * 1e3).toISOString();
1845
+ const fullQuery = buildEventQuery({
1846
+ query: params.query,
1847
+ sources: params.sources,
1848
+ tags: params.tags
1849
+ });
1850
+ const groupByFields = params.groupBy ?? ["monitor_name"];
1851
+ const maxEventsToAggregate = 1e4;
1852
+ let eventCount = 0;
1853
+ let pageCount = 0;
1854
+ const maxPages = 100;
1855
+ const body = {
1856
+ filter: {
1857
+ query: fullQuery,
1858
+ from: fromTime,
1859
+ to: toTime
1860
+ },
1861
+ sort: "timestamp",
1862
+ page: {
1863
+ limit: 1e3
1864
+ // Max per page
1865
+ }
1866
+ };
1867
+ let cursor;
1868
+ while (pageCount < maxPages && eventCount < maxEventsToAggregate) {
1869
+ const pageBody = { ...body, page: { ...body.page, cursor } };
1870
+ const response = await api.searchEvents({ body: pageBody });
1871
+ const events = response.data ?? [];
1872
+ if (events.length === 0) break;
1873
+ for (const event of events) {
1874
+ const formatted = formatEventV2(event);
1875
+ const groupKey = buildGroupKey(formatted, groupByFields);
1876
+ const existing = counts.get(groupKey);
1877
+ if (existing) {
1878
+ existing.count++;
1879
+ } else {
1880
+ counts.set(groupKey, { count: 1, sample: formatted });
1881
+ }
1882
+ eventCount++;
1883
+ if (eventCount >= maxEventsToAggregate) break;
1884
+ }
1885
+ cursor = response.meta?.page?.after;
1886
+ if (!cursor) break;
1887
+ pageCount++;
1888
+ }
1889
+ const effectiveLimit = Math.min(params.limit ?? 100, 1e3);
1890
+ const sorted = [...counts.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, effectiveLimit);
1891
+ const buckets = sorted.map(([key, data]) => ({
1892
+ key,
1893
+ count: data.count,
1894
+ sample: data.sample
1895
+ }));
1896
+ return {
1897
+ buckets,
1898
+ meta: {
1899
+ query: fullQuery,
1900
+ from: fromTime,
1901
+ to: toTime,
1902
+ groupBy: groupByFields,
1903
+ totalGroups: counts.size,
1904
+ totalEvents: eventCount,
1905
+ truncated: eventCount >= maxEventsToAggregate,
1906
+ datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
1907
+ }
1908
+ };
1909
+ }
1910
+ async function topEventsV2(api, params, limits, site) {
1911
+ const effectiveQuery = params.query ?? "source:alert";
1912
+ const effectiveTags = params.tags ?? ["source:alert"];
1913
+ const result = await aggregateEventsV2(api, {
1914
+ ...params,
1915
+ query: effectiveQuery,
1916
+ tags: effectiveTags,
1917
+ groupBy: params.groupBy ?? ["monitor_name"],
1918
+ limit: params.limit ?? 10
1919
+ }, limits, site);
1920
+ return {
1921
+ top: result.buckets.map((bucket, index) => ({
1922
+ rank: index + 1,
1923
+ name: bucket.key,
1924
+ monitorId: bucket.sample.monitorId,
1925
+ alertCount: bucket.count,
1926
+ lastAlert: bucket.sample.timestamp,
1927
+ sample: {
1928
+ title: bucket.sample.title,
1929
+ source: bucket.sample.source,
1930
+ alertType: bucket.sample.alertType
1931
+ }
1932
+ })),
1933
+ meta: result.meta
1934
+ };
1935
+ }
1936
+ function parseIntervalToMs(interval) {
1937
+ const ns = parseDurationToNs(interval ?? "1h");
1938
+ return ns ? Math.floor(ns / 1e6) : 36e5;
1939
+ }
1940
+ async function timeseriesEventsV2(api, params, limits, site) {
1941
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
1942
+ const defaultTo = now();
1943
+ const [validFrom, validTo] = ensureValidTimeRange(
1944
+ parseTime(params.from, defaultFrom),
1945
+ parseTime(params.to, defaultTo)
1946
+ );
1947
+ const fromTime = new Date(validFrom * 1e3).toISOString();
1948
+ const toTime = new Date(validTo * 1e3).toISOString();
1949
+ const fullQuery = buildEventQuery({
1950
+ query: params.query ?? "source:alert",
1951
+ sources: params.sources,
1952
+ tags: params.tags ?? ["source:alert"]
1953
+ });
1954
+ const intervalMs = parseIntervalToMs(params.interval);
1955
+ const groupByFields = params.groupBy ?? ["monitor_name"];
1956
+ const timeBuckets = /* @__PURE__ */ new Map();
1957
+ const maxEventsToProcess = 1e4;
1958
+ let eventCount = 0;
1959
+ let pageCount = 0;
1960
+ const maxPages = 100;
1961
+ const body = {
1962
+ filter: {
1963
+ query: fullQuery,
1964
+ from: fromTime,
1965
+ to: toTime
1966
+ },
1967
+ sort: "timestamp",
1968
+ page: { limit: 1e3 }
1969
+ };
1970
+ let cursor;
1971
+ while (pageCount < maxPages && eventCount < maxEventsToProcess) {
1972
+ const pageBody = { ...body, page: { ...body.page, cursor } };
1973
+ const response = await api.searchEvents({ body: pageBody });
1974
+ const events = response.data ?? [];
1975
+ if (events.length === 0) break;
1976
+ for (const event of events) {
1977
+ const formatted = formatEventV2(event);
1978
+ const groupKey = buildGroupKey(formatted, groupByFields);
1979
+ const eventTs = new Date(formatted.timestamp).getTime();
1980
+ const bucketTs = Math.floor(eventTs / intervalMs) * intervalMs;
1981
+ if (!timeBuckets.has(bucketTs)) {
1982
+ timeBuckets.set(bucketTs, /* @__PURE__ */ new Map());
1983
+ }
1984
+ const groupCounts = timeBuckets.get(bucketTs);
1985
+ groupCounts.set(groupKey, (groupCounts.get(groupKey) ?? 0) + 1);
1986
+ eventCount++;
1987
+ if (eventCount >= maxEventsToProcess) break;
1988
+ }
1989
+ cursor = response.meta?.page?.after;
1990
+ if (!cursor) break;
1991
+ pageCount++;
1992
+ }
1993
+ const sortedBuckets = [...timeBuckets.entries()].sort((a, b) => a[0] - b[0]).map(([bucketTs, groupCounts]) => {
1994
+ const counts = {};
1995
+ let total = 0;
1996
+ for (const [key, count] of groupCounts) {
1997
+ counts[key] = count;
1998
+ total += count;
1999
+ }
2000
+ return {
2001
+ timestamp: new Date(bucketTs).toISOString(),
2002
+ timestampMs: bucketTs,
2003
+ counts,
2004
+ total
2005
+ };
2006
+ });
2007
+ const effectiveLimit = params.limit ?? 100;
2008
+ const limitedBuckets = sortedBuckets.slice(0, effectiveLimit);
2009
+ return {
2010
+ timeseries: limitedBuckets,
2011
+ meta: {
2012
+ query: fullQuery,
2013
+ from: fromTime,
2014
+ to: toTime,
2015
+ interval: params.interval ?? "1h",
2016
+ intervalMs,
2017
+ groupBy: groupByFields,
2018
+ totalBuckets: sortedBuckets.length,
2019
+ totalEvents: eventCount,
2020
+ truncated: eventCount >= maxEventsToProcess,
2021
+ datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
2022
+ }
2023
+ };
2024
+ }
2025
+ async function incidentsEventsV2(api, params, limits, site) {
2026
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
2027
+ const defaultTo = now();
2028
+ const [validFrom, validTo] = ensureValidTimeRange(
2029
+ parseTime(params.from, defaultFrom),
2030
+ parseTime(params.to, defaultTo)
2031
+ );
2032
+ const fromTime = new Date(validFrom * 1e3).toISOString();
2033
+ const toTime = new Date(validTo * 1e3).toISOString();
2034
+ const fullQuery = buildEventQuery({
2035
+ query: params.query ?? "source:alert",
2036
+ sources: params.sources,
2037
+ tags: params.tags ?? ["source:alert"]
2038
+ });
2039
+ const dedupeWindowNs = parseDurationToNs(params.dedupeWindow ?? "5m");
2040
+ const dedupeWindowMs = dedupeWindowNs ? Math.floor(dedupeWindowNs / 1e6) : 3e5;
2041
+ const incidents = /* @__PURE__ */ new Map();
2042
+ const maxEventsToProcess = 1e4;
2043
+ let eventCount = 0;
2044
+ let pageCount = 0;
2045
+ const maxPages = 100;
2046
+ const body = {
2047
+ filter: {
2048
+ query: fullQuery,
2049
+ from: fromTime,
2050
+ to: toTime
2051
+ },
2052
+ sort: "timestamp",
2053
+ page: { limit: 1e3 }
2054
+ };
2055
+ let cursor;
2056
+ while (pageCount < maxPages && eventCount < maxEventsToProcess) {
2057
+ const pageBody = { ...body, page: { ...body.page, cursor } };
2058
+ const response = await api.searchEvents({ body: pageBody });
2059
+ const events = response.data ?? [];
2060
+ if (events.length === 0) break;
2061
+ for (const event of events) {
2062
+ const formatted = formatEventV2(event);
2063
+ const monitorName = formatted.monitorInfo?.name ?? formatted.title;
2064
+ if (!monitorName) {
2065
+ eventCount++;
2066
+ continue;
2067
+ }
2068
+ const eventTs = new Date(formatted.timestamp);
2069
+ let status = formatted.monitorInfo?.status?.toLowerCase() ?? "";
2070
+ if (!status && formatted.alertType) {
2071
+ const alertType = formatted.alertType.toLowerCase();
2072
+ if (alertType === "error" || alertType === "warning") {
2073
+ status = "triggered";
2074
+ } else if (alertType === "success") {
2075
+ status = "recovered";
2076
+ }
2077
+ }
2078
+ if (!status && formatted.source === "alert") {
2079
+ const msgLower = formatted.message.toLowerCase();
2080
+ if (msgLower.includes("recovered") || msgLower.includes("[ok]") || msgLower.includes("resolved")) {
2081
+ status = "recovered";
2082
+ } else {
2083
+ status = "triggered";
2084
+ }
2085
+ }
2086
+ const existing = incidents.get(monitorName);
2087
+ if (status === "triggered" || status === "alert" || status === "re-triggered" || status === "renotify") {
2088
+ if (existing) {
2089
+ const timeSinceLastTrigger = eventTs.getTime() - existing.lastTrigger.getTime();
2090
+ if (timeSinceLastTrigger <= dedupeWindowMs) {
2091
+ existing.lastTrigger = eventTs;
2092
+ existing.triggerCount++;
2093
+ existing.sample = formatted;
2094
+ } else {
2095
+ const oldKey = `${monitorName}::${existing.firstTrigger.toISOString()}`;
2096
+ incidents.set(oldKey, existing);
2097
+ incidents.set(monitorName, {
2098
+ monitorName,
2099
+ firstTrigger: eventTs,
2100
+ lastTrigger: eventTs,
2101
+ triggerCount: 1,
2102
+ recovered: false,
2103
+ sample: formatted
2104
+ });
2105
+ }
2106
+ } else {
2107
+ incidents.set(monitorName, {
2108
+ monitorName,
2109
+ firstTrigger: eventTs,
2110
+ lastTrigger: eventTs,
2111
+ triggerCount: 1,
2112
+ recovered: false,
2113
+ sample: formatted
2114
+ });
2115
+ }
2116
+ } else if (status === "recovered" || status === "ok") {
2117
+ if (existing && !existing.recovered) {
2118
+ existing.recovered = true;
2119
+ existing.recoveredAt = eventTs;
2120
+ }
2121
+ }
2122
+ eventCount++;
2123
+ if (eventCount >= maxEventsToProcess) break;
2124
+ }
2125
+ cursor = response.meta?.page?.after;
2126
+ if (!cursor) break;
2127
+ pageCount++;
2128
+ }
2129
+ const incidentList = [...incidents.values()].map((inc) => {
2130
+ let duration;
2131
+ if (inc.recoveredAt) {
2132
+ const durationMs = inc.recoveredAt.getTime() - inc.firstTrigger.getTime();
2133
+ if (durationMs < 6e4) {
2134
+ duration = `${Math.round(durationMs / 1e3)}s`;
2135
+ } else if (durationMs < 36e5) {
2136
+ duration = `${Math.round(durationMs / 6e4)}m`;
2137
+ } else {
2138
+ duration = `${(durationMs / 36e5).toFixed(1)}h`;
2139
+ }
2140
+ }
2141
+ return {
2142
+ monitorName: inc.monitorName,
2143
+ firstTrigger: inc.firstTrigger.toISOString(),
2144
+ lastTrigger: inc.lastTrigger.toISOString(),
2145
+ triggerCount: inc.triggerCount,
2146
+ recovered: inc.recovered,
2147
+ recoveredAt: inc.recoveredAt?.toISOString(),
2148
+ duration,
2149
+ sample: inc.sample
2150
+ };
2151
+ });
2152
+ incidentList.sort((a, b) => new Date(b.firstTrigger).getTime() - new Date(a.firstTrigger).getTime());
2153
+ const effectiveLimit = Math.min(params.limit ?? 100, 500);
2154
+ return {
2155
+ incidents: incidentList.slice(0, effectiveLimit),
2156
+ meta: {
2157
+ query: fullQuery,
2158
+ from: fromTime,
2159
+ to: toTime,
2160
+ dedupeWindow: params.dedupeWindow ?? "5m",
2161
+ dedupeWindowMs,
2162
+ totalIncidents: incidentList.length,
2163
+ totalEvents: eventCount,
2164
+ recoveredCount: incidentList.filter((i) => i.recovered).length,
2165
+ activeCount: incidentList.filter((i) => !i.recovered).length,
2166
+ truncated: eventCount >= maxEventsToProcess,
2167
+ datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
2168
+ }
2169
+ };
2170
+ }
2171
+ async function enrichWithMonitorMetadata(events, monitorsApi) {
2172
+ const monitorNames = /* @__PURE__ */ new Set();
2173
+ for (const event of events) {
2174
+ if (event.monitorInfo?.name) {
2175
+ monitorNames.add(event.monitorInfo.name);
2176
+ }
2177
+ }
2178
+ if (monitorNames.size === 0) {
2179
+ return events;
2180
+ }
2181
+ const monitorCache = /* @__PURE__ */ new Map();
2182
+ try {
2183
+ const response = await monitorsApi.listMonitors({
2184
+ pageSize: 1e3
2185
+ });
2186
+ const monitors = response ?? [];
2187
+ for (const monitor of monitors) {
2188
+ if (monitor.name) {
2189
+ monitorCache.set(monitor.name, monitor);
2190
+ }
2191
+ }
2192
+ } catch {
2193
+ return events;
2194
+ }
2195
+ return events.map((event) => {
2196
+ const enriched = { ...event };
2197
+ if (event.monitorInfo?.name) {
2198
+ const monitor = monitorCache.get(event.monitorInfo.name);
2199
+ if (monitor) {
2200
+ enriched.monitorMetadata = {
2201
+ id: monitor.id ?? 0,
2202
+ type: String(monitor.type ?? ""),
2203
+ message: monitor.message ?? "",
2204
+ tags: monitor.tags ?? [],
2205
+ options: {
2206
+ thresholds: monitor.options?.thresholds,
2207
+ notifyNoData: monitor.options?.notifyNoData,
2208
+ escalationMessage: monitor.options?.escalationMessage
2209
+ }
2210
+ };
2211
+ }
2212
+ }
2213
+ return enriched;
2214
+ });
2215
+ }
2216
+ function registerEventsTool(server, apiV1, apiV2, monitorsApi, limits, readOnly = false, site = "datadoghq.com") {
2217
+ server.tool(
2218
+ "events",
2219
+ `Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents.
2220
+ IMPORTANT: For monitor alert history, use tags: ["source:alert"] to find all triggered monitors.
2221
+ Filters: query (text search), sources, tags, priority, time range.
2222
+ Use for: monitor alerts, deployments, incidents, change tracking.
2223
+
2224
+ Use action:"top" with from:"7d" to find the noisiest monitors.
2225
+ Use action:"aggregate" with groupBy:["monitor_name"] for alert counts per monitor.
2226
+ Use action:"timeseries" with interval:"1h" to see alert trends over time.
2227
+ Use action:"incidents" with dedupeWindow:"5m" to deduplicate alerts into incidents.
2228
+ Use enrich:true with search to get monitor metadata (slower).`,
2229
+ InputSchema6,
2230
+ async ({ action, id, query, from, to, priority, sources, tags, limit, title, text, alertType, groupBy, cursor, interval, dedupeWindow, enrich }) => {
2231
+ try {
2232
+ checkReadOnly(action, readOnly);
2233
+ switch (action) {
2234
+ case "list":
2235
+ return toolResult(await listEventsV1(apiV1, {
2236
+ query,
2237
+ from,
2238
+ to,
2239
+ priority,
2240
+ sources,
2241
+ tags,
2242
+ limit
2243
+ }, limits));
2244
+ case "get": {
2245
+ const eventId = requireParam(id, "id", "get");
2246
+ return toolResult(await getEventV1(apiV1, eventId));
2247
+ }
2248
+ case "create": {
2249
+ const eventTitle = requireParam(title, "title", "create");
2250
+ const eventText = requireParam(text, "text", "create");
2251
+ return toolResult(await createEventV1(apiV1, {
2252
+ title: eventTitle,
2253
+ text: eventText,
2254
+ priority,
2255
+ tags,
2256
+ alertType
2257
+ }));
2258
+ }
2259
+ case "search": {
2260
+ const result = await searchEventsV2(apiV2, {
2261
+ query,
2262
+ from,
2263
+ to,
2264
+ sources,
2265
+ tags,
2266
+ priority,
2267
+ limit,
2268
+ cursor
2269
+ }, limits, site);
2270
+ if (enrich && result.events.length > 0) {
2271
+ const enrichedEvents = await enrichWithMonitorMetadata(result.events, monitorsApi);
2272
+ return toolResult({ ...result, events: enrichedEvents });
2273
+ }
2274
+ return toolResult(result);
2275
+ }
2276
+ case "aggregate":
2277
+ return toolResult(await aggregateEventsV2(apiV2, {
2278
+ query,
2279
+ from,
2280
+ to,
2281
+ sources,
2282
+ tags,
2283
+ groupBy,
2284
+ limit
2285
+ }, limits, site));
2286
+ case "top":
2287
+ return toolResult(await topEventsV2(apiV2, {
2288
+ query,
2289
+ from,
2290
+ to,
2291
+ sources,
2292
+ tags,
2293
+ groupBy,
2294
+ limit
2295
+ }, limits, site));
2296
+ case "timeseries":
2297
+ return toolResult(await timeseriesEventsV2(apiV2, {
2298
+ query,
2299
+ from,
2300
+ to,
2301
+ sources,
2302
+ tags,
2303
+ groupBy,
2304
+ interval,
2305
+ limit
2306
+ }, limits, site));
2307
+ case "incidents":
2308
+ return toolResult(await incidentsEventsV2(apiV2, {
2309
+ query,
2310
+ from,
2311
+ to,
2312
+ sources,
2313
+ tags,
2314
+ dedupeWindow,
2315
+ limit
2316
+ }, limits, site));
2317
+ default:
2318
+ throw new Error(`Unknown action: ${action}`);
2319
+ }
2320
+ } catch (error) {
2321
+ handleDatadogError(error);
2322
+ }
2323
+ }
2324
+ );
2325
+ }
2326
+
2327
+ // src/tools/incidents.ts
2328
+ import { z as z8 } from "zod";
2329
+ var ActionSchema7 = z8.enum(["list", "get", "search", "create", "update", "delete"]);
2330
+ var InputSchema7 = {
2331
+ action: ActionSchema7.describe("Action to perform"),
2332
+ id: z8.string().optional().describe("Incident ID (required for get/update/delete)"),
2333
+ query: z8.string().optional().describe("Search query (for search action)"),
2334
+ status: z8.enum(["active", "stable", "resolved"]).optional().describe("Filter by status (for list)"),
2335
+ limit: z8.number().optional().describe("Maximum number of incidents to return"),
2336
+ config: z8.record(z8.unknown()).optional().describe("Incident configuration (for create/update). Create requires: title. Update can modify: title, status, severity, fields.")
2337
+ };
2338
+ function formatIncident(i) {
2339
+ const attrs = i.attributes;
2340
+ const commander = i.relationships?.commanderUser?.data;
2341
+ return {
2342
+ id: i.id ?? "",
2343
+ title: attrs?.title ?? "",
2344
+ status: String(attrs?.state ?? "unknown"),
2345
+ severity: attrs?.severity ? String(attrs.severity) : null,
2346
+ state: attrs?.state ? String(attrs.state) : null,
2347
+ customerImpactScope: attrs?.customerImpactScope ?? null,
2348
+ customerImpacted: attrs?.customerImpacted ?? false,
2349
+ commander: {
2350
+ name: null,
2351
+ // Would need to resolve from relationships
2352
+ email: null,
2353
+ handle: commander?.id ?? null
2354
+ },
2355
+ createdAt: attrs?.created ? new Date(attrs.created).toISOString() : "",
2356
+ modifiedAt: attrs?.modified ? new Date(attrs.modified).toISOString() : "",
2357
+ resolvedAt: attrs?.resolved ? new Date(attrs.resolved).toISOString() : null,
2358
+ timeToDetect: attrs?.timeToDetect ?? null,
2359
+ timeToRepair: attrs?.timeToRepair ?? null
2360
+ };
2361
+ }
2362
+ async function listIncidents(api, params, limits) {
2363
+ const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
2364
+ const response = await api.listIncidents({
2365
+ pageSize: effectiveLimit
2366
+ });
2367
+ let incidents = (response.data ?? []).map(formatIncident);
2368
+ if (params.status) {
2369
+ incidents = incidents.filter((i) => i.state?.toLowerCase() === params.status);
2370
+ }
2371
+ incidents = incidents.slice(0, effectiveLimit);
2372
+ return {
2373
+ incidents,
2374
+ total: incidents.length
2375
+ };
2376
+ }
2377
+ async function getIncident(api, id) {
2378
+ const response = await api.getIncident({ incidentId: id });
2379
+ return {
2380
+ incident: response.data ? formatIncident(response.data) : null
2381
+ };
2382
+ }
2383
+ async function searchIncidents(api, query, limits) {
2384
+ const response = await api.searchIncidents({
2385
+ query,
2386
+ pageSize: limits.maxResults
2387
+ });
2388
+ const incidents = (response.data?.attributes?.incidents ?? []).map((i) => ({
2389
+ id: i.data?.id ?? "",
2390
+ title: i.data?.attributes?.title ?? "",
2391
+ state: i.data?.attributes?.state ?? "unknown"
2392
+ }));
2393
+ return {
2394
+ incidents,
2395
+ total: response.data?.attributes?.total ?? incidents.length
2396
+ };
2397
+ }
2398
+ async function createIncident(api, config) {
2399
+ const body = {
2400
+ data: {
2401
+ type: "incidents",
2402
+ attributes: config
2403
+ }
2404
+ };
2405
+ const response = await api.createIncident({ body });
2406
+ return {
2407
+ success: true,
2408
+ incident: response.data ? formatIncident(response.data) : null
2409
+ };
2410
+ }
2411
+ async function updateIncident(api, id, config) {
2412
+ const body = {
2413
+ data: {
2414
+ type: "incidents",
2415
+ id,
2416
+ attributes: config
2417
+ }
2418
+ };
2419
+ const response = await api.updateIncident({ incidentId: id, body });
2420
+ return {
2421
+ success: true,
2422
+ incident: response.data ? formatIncident(response.data) : null
2423
+ };
2424
+ }
2425
+ async function deleteIncident(api, id) {
2426
+ await api.deleteIncident({ incidentId: id });
2427
+ return {
2428
+ success: true,
2429
+ message: `Incident ${id} deleted`
2430
+ };
2431
+ }
2432
+ function registerIncidentsTool(server, api, limits, readOnly = false) {
2433
+ server.tool(
2434
+ "incidents",
2435
+ "Manage Datadog incidents for incident response. Actions: list, get, search, create, update, delete. Use for: incident management, on-call response, postmortems, tracking MTTR/MTTD.",
2436
+ InputSchema7,
2437
+ async ({ action, id, query, status, limit, config }) => {
2438
+ try {
2439
+ checkReadOnly(action, readOnly);
2440
+ switch (action) {
2441
+ case "list":
2442
+ return toolResult(await listIncidents(api, { status, limit }, limits));
2443
+ case "get": {
2444
+ const incidentId = requireParam(id, "id", "get");
2445
+ return toolResult(await getIncident(api, incidentId));
2446
+ }
2447
+ case "search": {
2448
+ const searchQuery = requireParam(query, "query", "search");
2449
+ return toolResult(await searchIncidents(api, searchQuery, limits));
2450
+ }
2451
+ case "create": {
2452
+ const incidentConfig = requireParam(config, "config", "create");
2453
+ return toolResult(await createIncident(api, incidentConfig));
2454
+ }
2455
+ case "update": {
2456
+ const incidentId = requireParam(id, "id", "update");
2457
+ const incidentConfig = requireParam(config, "config", "update");
2458
+ return toolResult(await updateIncident(api, incidentId, incidentConfig));
2459
+ }
2460
+ case "delete": {
2461
+ const incidentId = requireParam(id, "id", "delete");
2462
+ return toolResult(await deleteIncident(api, incidentId));
2463
+ }
2464
+ default:
2465
+ throw new Error(`Unknown action: ${action}`);
2466
+ }
2467
+ } catch (error) {
2468
+ handleDatadogError(error);
2469
+ }
2470
+ }
2471
+ );
2472
+ }
2473
+
2474
+ // src/tools/slos.ts
2475
+ import { z as z9 } from "zod";
2476
+ var ActionSchema8 = z9.enum(["list", "get", "create", "update", "delete", "history"]);
2477
+ var InputSchema8 = {
2478
+ action: ActionSchema8.describe("Action to perform"),
2479
+ id: z9.string().optional().describe("SLO ID (required for get/update/delete/history)"),
2480
+ ids: z9.array(z9.string()).optional().describe("Multiple SLO IDs (for list with specific IDs)"),
2481
+ query: z9.string().optional().describe("Search query (for list)"),
2482
+ tags: z9.array(z9.string()).optional().describe("Filter by tags (for list)"),
2483
+ limit: z9.number().optional().describe("Maximum number of SLOs to return"),
2484
+ config: z9.record(z9.unknown()).optional().describe("SLO configuration (for create/update). Must include type, name, thresholds."),
2485
+ from: z9.string().optional().describe('Start time for history (ISO 8601 or relative like "7d", "1w")'),
2486
+ to: z9.string().optional().describe("End time for history (ISO 8601 or relative, default: now)")
2487
+ };
2488
+ function formatSlo(s) {
2489
+ const primaryThreshold = s.thresholds?.[0];
2490
+ return {
2491
+ id: s.id ?? "",
2492
+ name: s.name ?? "",
2493
+ description: s.description ?? null,
2494
+ type: String(s.type ?? "unknown"),
2495
+ targetThreshold: primaryThreshold?.target ?? 0,
2496
+ warningThreshold: primaryThreshold?.warning ?? null,
2497
+ timeframe: String(primaryThreshold?.timeframe ?? ""),
2498
+ tags: s.tags ?? [],
2499
+ status: {
2500
+ // Note: SLI status requires a separate API call to getSLOHistory
2501
+ sli: null,
2502
+ errorBudgetRemaining: null,
2503
+ state: "unknown"
2504
+ },
2505
+ createdAt: s.createdAt ? new Date(s.createdAt * 1e3).toISOString() : "",
2506
+ modifiedAt: s.modifiedAt ? new Date(s.modifiedAt * 1e3).toISOString() : ""
2507
+ };
2508
+ }
2509
+ async function listSlos(api, params, limits) {
2510
+ const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
2511
+ const response = await api.listSLOs({
2512
+ ids: params.ids?.join(","),
2513
+ query: params.query,
2514
+ tagsQuery: params.tags?.join(","),
2515
+ limit: effectiveLimit
2516
+ });
2517
+ const slos = (response.data ?? []).map(formatSlo);
2518
+ return {
2519
+ slos,
2520
+ total: response.data?.length ?? 0
2521
+ };
2522
+ }
2523
+ async function getSlo(api, id) {
2524
+ const response = await api.getSLO({ sloId: id });
2525
+ return {
2526
+ slo: response.data ? formatSlo(response.data) : null
2527
+ };
2528
+ }
2529
+ function snakeToCamel(str) {
2530
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
2531
+ }
2532
+ function normalizeConfigKeys(obj) {
2533
+ if (obj === null || obj === void 0) return obj;
2534
+ if (Array.isArray(obj)) return obj.map(normalizeConfigKeys);
2535
+ if (typeof obj !== "object") return obj;
2536
+ const normalized = {};
2537
+ for (const [key, value] of Object.entries(obj)) {
2538
+ const camelKey = snakeToCamel(key);
2539
+ normalized[camelKey] = normalizeConfigKeys(value);
2540
+ }
2541
+ return normalized;
2542
+ }
2543
+ function normalizeSloConfig(config) {
2544
+ const normalized = normalizeConfigKeys(config);
2545
+ if (!normalized.name) {
2546
+ throw new Error("SLO config requires 'name' field");
2547
+ }
2548
+ if (!normalized.type) {
2549
+ throw new Error("SLO config requires 'type' field (e.g., 'metric', 'monitor')");
2550
+ }
2551
+ if (!normalized.thresholds || !Array.isArray(normalized.thresholds)) {
2552
+ throw new Error("SLO config requires 'thresholds' array with at least one threshold");
2553
+ }
2554
+ return normalized;
2555
+ }
2556
+ async function createSlo(api, config) {
2557
+ const body = normalizeSloConfig(config);
2558
+ const response = await api.createSLO({ body });
2559
+ return {
2560
+ success: true,
2561
+ slo: response.data?.[0] ? formatSlo(response.data[0]) : null
2562
+ };
2563
+ }
2564
+ async function updateSlo(api, id, config) {
2565
+ const body = normalizeConfigKeys(config);
2566
+ const response = await api.updateSLO({ sloId: id, body });
2567
+ return {
2568
+ success: true,
2569
+ slo: response.data?.[0] ? formatSlo(response.data[0]) : null
2570
+ };
2571
+ }
2572
+ async function deleteSlo(api, id) {
2573
+ await api.deleteSLO({ sloId: id });
2574
+ return {
2575
+ success: true,
2576
+ message: `SLO ${id} deleted`
2577
+ };
2578
+ }
2579
+ async function getSloHistory(api, id, params) {
2580
+ const nowMs = Date.now();
2581
+ const defaultFromMs = nowMs - 7 * 24 * 60 * 60 * 1e3;
2582
+ const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3)) * 1e3;
2583
+ const toTime = parseTime(params.to, Math.floor(nowMs / 1e3)) * 1e3;
2584
+ const [validFrom, validTo] = ensureValidTimeRange(fromTime, toTime);
2585
+ const response = await api.getSLOHistory({
2586
+ sloId: id,
2587
+ fromTs: Math.floor(validFrom / 1e3),
2588
+ toTs: Math.floor(validTo / 1e3)
2589
+ });
2590
+ const data = response.data;
2591
+ return {
2592
+ history: {
2593
+ overall: {
2594
+ sliValue: data?.overall?.sliValue ?? null,
2595
+ spanPrecision: data?.overall?.spanPrecision ?? null,
2596
+ uptime: data?.overall?.uptime ?? null
2597
+ },
2598
+ series: {
2599
+ numerator: data?.series?.numerator?.values ?? [],
2600
+ denominator: data?.series?.denominator?.values ?? [],
2601
+ times: data?.series?.times ?? []
2602
+ },
2603
+ thresholds: data?.thresholds ?? {},
2604
+ fromTs: new Date(validFrom).toISOString(),
2605
+ toTs: new Date(validTo).toISOString()
2606
+ }
2607
+ };
2608
+ }
2609
+ function registerSlosTool(server, api, limits, readOnly = false) {
2610
+ server.tool(
2611
+ "slos",
2612
+ "Manage Datadog Service Level Objectives. Actions: list, get, create, update, delete, history. SLO types: metric-based, monitor-based. Use for: reliability tracking, error budgets, SLA compliance, performance targets.",
2613
+ InputSchema8,
2614
+ async ({ action, id, ids, query, tags, limit, config, from, to }) => {
2615
+ try {
2616
+ checkReadOnly(action, readOnly);
2617
+ switch (action) {
2618
+ case "list":
2619
+ return toolResult(await listSlos(api, { ids, query, tags, limit }, limits));
2620
+ case "get": {
2621
+ const sloId = requireParam(id, "id", "get");
2622
+ return toolResult(await getSlo(api, sloId));
2623
+ }
2624
+ case "create": {
2625
+ const sloConfig = requireParam(config, "config", "create");
2626
+ return toolResult(await createSlo(api, sloConfig));
2627
+ }
2628
+ case "update": {
2629
+ const sloId = requireParam(id, "id", "update");
2630
+ const sloConfig = requireParam(config, "config", "update");
2631
+ return toolResult(await updateSlo(api, sloId, sloConfig));
2632
+ }
2633
+ case "delete": {
2634
+ const sloId = requireParam(id, "id", "delete");
2635
+ return toolResult(await deleteSlo(api, sloId));
2636
+ }
2637
+ case "history": {
2638
+ const sloId = requireParam(id, "id", "history");
2639
+ return toolResult(await getSloHistory(api, sloId, { from, to }));
2640
+ }
2641
+ default:
2642
+ throw new Error(`Unknown action: ${action}`);
2643
+ }
2644
+ } catch (error) {
2645
+ handleDatadogError(error);
2646
+ }
2647
+ }
2648
+ );
2649
+ }
2650
+
2651
+ // src/tools/synthetics.ts
2652
+ import { z as z10 } from "zod";
2653
+ var ActionSchema9 = z10.enum(["list", "get", "create", "update", "delete", "trigger", "results"]);
2654
+ var InputSchema9 = {
2655
+ action: ActionSchema9.describe("Action to perform"),
2656
+ id: z10.string().optional().describe("Test public ID (required for get/update/delete/trigger/results)"),
2657
+ ids: z10.array(z10.string()).optional().describe("Multiple test IDs (for bulk trigger)"),
2658
+ testType: z10.enum(["api", "browser"]).optional().describe("Test type filter (for list) or type for create"),
2659
+ locations: z10.array(z10.string()).optional().describe("Filter by locations (for list)"),
2660
+ tags: z10.array(z10.string()).optional().describe("Filter by tags (for list)"),
2661
+ limit: z10.number().optional().describe("Maximum number of tests to return"),
2662
+ config: z10.record(z10.unknown()).optional().describe("Test configuration (for create/update). Includes: name, type, config, options, locations, message.")
2663
+ };
2664
+ function formatTest(t) {
2665
+ return {
2666
+ publicId: t.publicId ?? "",
2667
+ name: t.name ?? "",
2668
+ type: String(t.type ?? "unknown"),
2669
+ subtype: t.subtype ? String(t.subtype) : null,
2670
+ status: String(t.status ?? "unknown"),
2671
+ message: t.message ?? "",
2672
+ tags: t.tags ?? [],
2673
+ locations: t.locations ?? [],
2674
+ monitorId: t.monitorId ?? null
2675
+ };
2676
+ }
2677
+ async function listTests(api, params, limits) {
2678
+ const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
2679
+ const response = await api.listTests({
2680
+ pageSize: effectiveLimit
2681
+ });
2682
+ let tests = (response.tests ?? []).map(formatTest);
2683
+ if (params.tags && params.tags.length > 0) {
2684
+ tests = tests.filter((t) => params.tags.some((tag) => t.tags.includes(tag)));
2685
+ }
2686
+ if (params.locations && params.locations.length > 0) {
2687
+ tests = tests.filter((t) => params.locations.some((loc) => t.locations.includes(loc)));
2688
+ }
2689
+ tests = tests.slice(0, effectiveLimit);
2690
+ const summary = {
2691
+ total: response.tests?.length ?? 0,
2692
+ api: tests.filter((t) => t.type === "api").length,
2693
+ browser: tests.filter((t) => t.type === "browser").length,
2694
+ passing: tests.filter((t) => t.status === "OK" || t.status === "live").length,
2695
+ failing: tests.filter((t) => t.status === "Alert").length
2696
+ };
2697
+ return { tests, summary };
2698
+ }
2699
+ async function getTest(api, id) {
2700
+ try {
2701
+ const response = await api.getAPITest({ publicId: id });
2702
+ return { test: formatTest(response) };
2703
+ } catch {
2704
+ const response = await api.getBrowserTest({ publicId: id });
2705
+ return { test: formatTest(response) };
2706
+ }
2707
+ }
2708
+ function snakeToCamel2(str) {
2709
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
2710
+ }
2711
+ function normalizeConfigKeys2(obj) {
2712
+ if (obj === null || obj === void 0) return obj;
2713
+ if (Array.isArray(obj)) return obj.map(normalizeConfigKeys2);
2714
+ if (typeof obj !== "object") return obj;
2715
+ const normalized = {};
2716
+ for (const [key, value] of Object.entries(obj)) {
2717
+ const camelKey = snakeToCamel2(key);
2718
+ normalized[camelKey] = normalizeConfigKeys2(value);
2719
+ }
2720
+ return normalized;
2721
+ }
2722
+ function normalizeSyntheticsConfig(config) {
2723
+ const normalized = normalizeConfigKeys2(config);
2724
+ if (!normalized.name) {
2725
+ throw new Error("Synthetics test config requires 'name' field");
2726
+ }
2727
+ if (!normalized.locations || !Array.isArray(normalized.locations) || normalized.locations.length === 0) {
2728
+ throw new Error("Synthetics test config requires 'locations' array (e.g., ['aws:us-east-1'])");
2729
+ }
2730
+ return normalized;
2731
+ }
2732
+ async function createTest(api, config, testType) {
2733
+ const normalizedConfig = normalizeSyntheticsConfig(config);
2734
+ const type = testType ?? (normalizedConfig.type === "browser" ? "browser" : "api");
2735
+ if (type === "browser") {
2736
+ const body = normalizedConfig;
2737
+ const response = await api.createSyntheticsBrowserTest({ body });
2738
+ return {
2739
+ success: true,
2740
+ test: formatTest(response)
2741
+ };
2742
+ } else {
2743
+ const body = normalizedConfig;
2744
+ const response = await api.createSyntheticsAPITest({ body });
2745
+ return {
2746
+ success: true,
2747
+ test: formatTest(response)
2748
+ };
2749
+ }
2750
+ }
2751
+ async function updateTest(api, id, config) {
2752
+ const normalizedConfig = normalizeConfigKeys2(config);
2753
+ let testType = "api";
2754
+ try {
2755
+ await api.getAPITest({ publicId: id });
2756
+ testType = "api";
2757
+ } catch {
2758
+ testType = "browser";
2759
+ }
2760
+ if (testType === "browser") {
2761
+ const body = normalizedConfig;
2762
+ const response = await api.updateBrowserTest({ publicId: id, body });
2763
+ return {
2764
+ success: true,
2765
+ test: formatTest(response)
2766
+ };
2767
+ } else {
2768
+ const body = normalizedConfig;
2769
+ const response = await api.updateAPITest({ publicId: id, body });
2770
+ return {
2771
+ success: true,
2772
+ test: formatTest(response)
2773
+ };
2774
+ }
2775
+ }
2776
+ async function deleteTests(api, ids) {
2777
+ await api.deleteTests({
2778
+ body: { publicIds: ids }
2779
+ });
2780
+ return {
2781
+ success: true,
2782
+ message: `Deleted ${ids.length} test(s): ${ids.join(", ")}`
2783
+ };
2784
+ }
2785
+ async function triggerTests(api, ids) {
2786
+ const response = await api.triggerTests({
2787
+ body: {
2788
+ tests: ids.map((id) => ({ publicId: id }))
2789
+ }
2790
+ });
2791
+ const results = response.results?.map((r) => ({
2792
+ publicId: r.publicId ?? "",
2793
+ resultId: r.resultId ?? "",
2794
+ triggered: true
2795
+ })) ?? [];
2796
+ return {
2797
+ triggered: results,
2798
+ total: results.length
2799
+ };
2800
+ }
2801
+ async function getTestResults(api, id) {
2802
+ try {
2803
+ const response = await api.getAPITestLatestResults({ publicId: id });
2804
+ const results = (response.results ?? []).map((r) => ({
2805
+ resultId: r.resultId ?? "",
2806
+ status: r.result?.passed ? "passed" : "failed",
2807
+ checkTime: r.checkTime ? new Date(r.checkTime * 1e3).toISOString() : "",
2808
+ responseTime: r.result?.timings?.total ?? null
2809
+ }));
2810
+ return { results, testType: "api" };
2811
+ } catch {
2812
+ const response = await api.getBrowserTestLatestResults({ publicId: id });
2813
+ const results = (response.results ?? []).map((r) => ({
2814
+ resultId: r.resultId ?? "",
2815
+ // Browser tests don't have 'passed' - determine from errorCount
2816
+ status: (r.result?.errorCount ?? 0) === 0 ? "passed" : "failed",
2817
+ checkTime: r.checkTime ? new Date(r.checkTime * 1e3).toISOString() : "",
2818
+ duration: r.result?.duration ?? null
2819
+ }));
2820
+ return { results, testType: "browser" };
2821
+ }
2822
+ }
2823
+ function registerSyntheticsTool(server, api, limits, readOnly = false) {
2824
+ server.tool(
2825
+ "synthetics",
2826
+ "Manage Datadog Synthetic tests (API and Browser). Actions: list, get, create, update, delete, trigger, results. Use for: uptime monitoring, API testing, user journey testing, performance testing, canary deployments.",
2827
+ InputSchema9,
2828
+ async ({ action, id, ids, testType, locations, tags, limit, config }) => {
2829
+ try {
2830
+ checkReadOnly(action, readOnly);
2831
+ switch (action) {
2832
+ case "list":
2833
+ return toolResult(await listTests(api, { locations, tags, limit }, limits));
2834
+ case "get": {
2835
+ const testId = requireParam(id, "id", "get");
2836
+ return toolResult(await getTest(api, testId));
2837
+ }
2838
+ case "create": {
2839
+ const testConfig = requireParam(config, "config", "create");
2840
+ return toolResult(await createTest(api, testConfig, testType));
2841
+ }
2842
+ case "update": {
2843
+ const testId = requireParam(id, "id", "update");
2844
+ const testConfig = requireParam(config, "config", "update");
2845
+ return toolResult(await updateTest(api, testId, testConfig));
2846
+ }
2847
+ case "delete": {
2848
+ const testIds = ids ?? (id ? [id] : void 0);
2849
+ const deleteIds = requireParam(testIds, "id or ids", "delete");
2850
+ return toolResult(await deleteTests(api, deleteIds));
2851
+ }
2852
+ case "trigger": {
2853
+ const testIds = ids ?? (id ? [id] : void 0);
2854
+ const triggerIds = requireParam(testIds, "id or ids", "trigger");
2855
+ return toolResult(await triggerTests(api, triggerIds));
2856
+ }
2857
+ case "results": {
2858
+ const testId = requireParam(id, "id", "results");
2859
+ return toolResult(await getTestResults(api, testId));
2860
+ }
2861
+ default:
2862
+ throw new Error(`Unknown action: ${action}`);
2863
+ }
2864
+ } catch (error) {
2865
+ handleDatadogError(error);
2866
+ }
2867
+ }
2868
+ );
2869
+ }
2870
+
2871
+ // src/tools/hosts.ts
2872
+ import { z as z11 } from "zod";
2873
+ var ActionSchema10 = z11.enum(["list", "totals", "mute", "unmute"]);
2874
+ var InputSchema10 = {
2875
+ action: ActionSchema10.describe("Action to perform"),
2876
+ filter: z11.string().optional().describe('Filter hosts by name, alias, or tag (e.g., "env:prod")'),
2877
+ from: z11.number().optional().describe("Starting offset for pagination"),
2878
+ count: z11.number().optional().describe("Number of hosts to return"),
2879
+ sortField: z11.string().optional().describe('Field to sort by (e.g., "apps", "cpu", "name")'),
2880
+ sortDir: z11.enum(["asc", "desc"]).optional().describe("Sort direction"),
2881
+ hostName: z11.string().optional().describe("Host name (required for mute/unmute)"),
2882
+ message: z11.string().optional().describe("Mute reason message"),
2883
+ end: z11.number().optional().describe("Mute end timestamp (POSIX). Omit for indefinite mute"),
2884
+ override: z11.boolean().optional().describe("If true, replaces existing mute instead of failing")
2885
+ };
2886
+ function formatHost(h) {
2887
+ return {
2888
+ hostName: h.hostName ?? "",
2889
+ aliases: h.aliases ?? [],
2890
+ apps: h.apps ?? [],
2891
+ sources: h.sources ?? [],
2892
+ up: h.up ?? false,
2893
+ isMuted: h.isMuted ?? false,
2894
+ muteTimeout: h.muteTimeout ?? null,
2895
+ lastReportedTime: h.lastReportedTime ? new Date(h.lastReportedTime * 1e3).toISOString() : "",
2896
+ meta: {
2897
+ cpuCores: h.meta?.cpuCores ?? null,
2898
+ platform: h.meta?.platform ?? null,
2899
+ gohai: h.meta?.gohai ?? null
2900
+ }
2901
+ };
2902
+ }
2903
+ async function listHosts(api, params, limits) {
2904
+ const response = await api.listHosts({
2905
+ filter: params.filter,
2906
+ from: params.from,
2907
+ count: Math.min(params.count ?? limits.maxResults, limits.maxResults),
2908
+ sortField: params.sortField,
2909
+ sortDir: params.sortDir
2910
+ });
2911
+ const hosts = (response.hostList ?? []).map(formatHost);
2912
+ return {
2913
+ hosts,
2914
+ totalReturned: response.totalReturned ?? hosts.length,
2915
+ totalMatching: response.totalMatching ?? hosts.length
2916
+ };
2917
+ }
2918
+ async function getHostTotals(api) {
2919
+ const response = await api.getHostTotals({});
2920
+ return {
2921
+ totals: {
2922
+ totalUp: response.totalUp ?? 0,
2923
+ totalActive: response.totalActive ?? 0
2924
+ }
2925
+ };
2926
+ }
2927
+ async function muteHost(api, hostName, params) {
2928
+ await api.muteHost({
2929
+ hostName,
2930
+ body: {
2931
+ message: params.message,
2932
+ end: params.end,
2933
+ override: params.override
2934
+ }
2935
+ });
2936
+ return {
2937
+ success: true,
2938
+ message: `Host ${hostName} muted${params.end ? ` until ${new Date(params.end * 1e3).toISOString()}` : " indefinitely"}`
2939
+ };
2940
+ }
2941
+ async function unmuteHost(api, hostName) {
2942
+ await api.unmuteHost({ hostName });
2943
+ return {
2944
+ success: true,
2945
+ message: `Host ${hostName} unmuted`
2946
+ };
2947
+ }
2948
+ function registerHostsTool(server, api, limits, readOnly = false) {
2949
+ server.tool(
2950
+ "hosts",
2951
+ "Manage Datadog infrastructure hosts. Actions: list (with filters), totals (counts), mute (silence alerts), unmute. Use for: infrastructure inventory, host health, silencing noisy hosts during maintenance.",
2952
+ InputSchema10,
2953
+ async ({ action, filter, from, count, sortField, sortDir, hostName, message, end, override }) => {
2954
+ try {
2955
+ checkReadOnly(action, readOnly);
2956
+ switch (action) {
2957
+ case "list":
2958
+ return toolResult(await listHosts(api, { filter, from, count, sortField, sortDir }, limits));
2959
+ case "totals":
2960
+ return toolResult(await getHostTotals(api));
2961
+ case "mute": {
2962
+ const host = requireParam(hostName, "hostName", "mute");
2963
+ return toolResult(await muteHost(api, host, { message, end, override }));
2964
+ }
2965
+ case "unmute": {
2966
+ const host = requireParam(hostName, "hostName", "unmute");
2967
+ return toolResult(await unmuteHost(api, host));
2968
+ }
2969
+ default:
2970
+ throw new Error(`Unknown action: ${action}`);
2971
+ }
2972
+ } catch (error) {
2973
+ handleDatadogError(error);
2974
+ }
2975
+ }
2976
+ );
2977
+ }
2978
+
2979
+ // src/tools/downtimes.ts
2980
+ import { z as z12 } from "zod";
2981
+ var ActionSchema11 = z12.enum(["list", "get", "create", "update", "cancel", "listByMonitor"]);
2982
+ var InputSchema11 = {
2983
+ action: ActionSchema11.describe("Action to perform"),
2984
+ id: z12.string().optional().describe("Downtime ID (required for get/update/cancel)"),
2985
+ monitorId: z12.number().optional().describe("Monitor ID (required for listByMonitor)"),
2986
+ currentOnly: z12.boolean().optional().describe("Only return active downtimes (for list)"),
2987
+ limit: z12.number().optional().describe("Maximum number of downtimes to return"),
2988
+ config: z12.record(z12.unknown()).optional().describe("Downtime configuration (for create/update). Must include scope and schedule.")
2989
+ };
2990
+ function extractMonitorIdentifier(mi) {
2991
+ if (!mi) return { monitorId: null, monitorTags: [] };
2992
+ if ("monitorId" in mi && typeof mi.monitorId === "number") {
2993
+ return { monitorId: mi.monitorId, monitorTags: [] };
2994
+ }
2995
+ if ("monitorTags" in mi && Array.isArray(mi.monitorTags)) {
2996
+ return { monitorId: null, monitorTags: mi.monitorTags };
2997
+ }
2998
+ return { monitorId: null, monitorTags: [] };
2999
+ }
3000
+ function formatDowntime(d) {
3001
+ const attrs = d.attributes;
3002
+ const status = attrs?.status;
3003
+ return {
3004
+ id: d.id ?? "",
3005
+ displayTimezone: attrs?.displayTimezone ?? "UTC",
3006
+ message: attrs?.message ?? null,
3007
+ monitorIdentifier: extractMonitorIdentifier(attrs?.monitorIdentifier),
3008
+ scope: attrs?.scope ?? "",
3009
+ status: typeof status === "string" ? status : "unknown",
3010
+ schedule: attrs?.schedule ?? null,
3011
+ createdAt: attrs?.created ? new Date(attrs.created).toISOString() : "",
3012
+ modifiedAt: attrs?.modified ? new Date(attrs.modified).toISOString() : ""
3013
+ };
3014
+ }
3015
+ async function listDowntimes(api, params, limits) {
3016
+ const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
3017
+ const response = await api.listDowntimes({
3018
+ currentOnly: params.currentOnly
3019
+ });
3020
+ const downtimes = (response.data ?? []).slice(0, effectiveLimit).map(formatDowntime);
3021
+ return {
3022
+ downtimes,
3023
+ total: response.data?.length ?? 0
3024
+ };
3025
+ }
3026
+ async function getDowntime(api, id) {
3027
+ const response = await api.getDowntime({ downtimeId: id });
3028
+ return {
3029
+ downtime: response.data ? formatDowntime(response.data) : null
3030
+ };
3031
+ }
3032
+ async function createDowntime(api, config) {
3033
+ const body = {
3034
+ data: {
3035
+ type: "downtime",
3036
+ attributes: config
3037
+ }
3038
+ };
3039
+ const response = await api.createDowntime({ body });
3040
+ return {
3041
+ success: true,
3042
+ downtime: response.data ? formatDowntime(response.data) : null
3043
+ };
3044
+ }
3045
+ async function updateDowntime(api, id, config) {
3046
+ const body = {
3047
+ data: {
3048
+ type: "downtime",
3049
+ id,
3050
+ attributes: config
3051
+ }
3052
+ };
3053
+ const response = await api.updateDowntime({ downtimeId: id, body });
3054
+ return {
3055
+ success: true,
3056
+ downtime: response.data ? formatDowntime(response.data) : null
3057
+ };
3058
+ }
3059
+ async function cancelDowntime(api, id) {
3060
+ await api.cancelDowntime({ downtimeId: id });
3061
+ return {
3062
+ success: true,
3063
+ message: `Downtime ${id} cancelled`
3064
+ };
3065
+ }
3066
+ function formatMonitorDowntime(d) {
3067
+ const attrs = d.attributes;
3068
+ return {
3069
+ id: d.id ?? "",
3070
+ scope: attrs?.scope ?? null,
3071
+ start: attrs?.start ? new Date(attrs.start).toISOString() : null,
3072
+ end: attrs?.end ? new Date(attrs.end).toISOString() : null
3073
+ };
3074
+ }
3075
+ async function listMonitorDowntimes(api, monitorId, limits) {
3076
+ const response = await api.listMonitorDowntimes({ monitorId });
3077
+ const downtimes = (response.data ?? []).slice(0, limits.maxResults).map(formatMonitorDowntime);
3078
+ return {
3079
+ downtimes,
3080
+ monitorId,
3081
+ total: response.data?.length ?? 0
3082
+ };
3083
+ }
3084
+ function registerDowntimesTool(server, api, limits, readOnly = false) {
3085
+ server.tool(
3086
+ "downtimes",
3087
+ "Manage Datadog scheduled downtimes for maintenance windows. Actions: list, get, create, update, cancel, listByMonitor. Use for: scheduling maintenance, preventing false alerts during deployments, managing recurring maintenance windows.",
3088
+ InputSchema11,
3089
+ async ({ action, id, monitorId, currentOnly, limit, config }) => {
3090
+ try {
3091
+ checkReadOnly(action, readOnly);
3092
+ switch (action) {
3093
+ case "list":
3094
+ return toolResult(await listDowntimes(api, { currentOnly, limit }, limits));
3095
+ case "get": {
3096
+ const downtimeId = requireParam(id, "id", "get");
3097
+ return toolResult(await getDowntime(api, downtimeId));
3098
+ }
3099
+ case "create": {
3100
+ const downtimeConfig = requireParam(config, "config", "create");
3101
+ return toolResult(await createDowntime(api, downtimeConfig));
3102
+ }
3103
+ case "update": {
3104
+ const downtimeId = requireParam(id, "id", "update");
3105
+ const downtimeConfig = requireParam(config, "config", "update");
3106
+ return toolResult(await updateDowntime(api, downtimeId, downtimeConfig));
3107
+ }
3108
+ case "cancel": {
3109
+ const downtimeId = requireParam(id, "id", "cancel");
3110
+ return toolResult(await cancelDowntime(api, downtimeId));
3111
+ }
3112
+ case "listByMonitor": {
3113
+ const monitor = requireParam(monitorId, "monitorId", "listByMonitor");
3114
+ return toolResult(await listMonitorDowntimes(api, monitor, limits));
3115
+ }
3116
+ default:
3117
+ throw new Error(`Unknown action: ${action}`);
3118
+ }
3119
+ } catch (error) {
3120
+ handleDatadogError(error);
3121
+ }
3122
+ }
3123
+ );
3124
+ }
3125
+
3126
+ // src/tools/rum.ts
3127
+ import { z as z13 } from "zod";
3128
+ var ActionSchema12 = z13.enum(["applications", "events", "aggregate", "performance", "waterfall"]);
3129
+ var InputSchema12 = {
3130
+ action: ActionSchema12.describe("Action to perform"),
3131
+ query: z13.string().optional().describe('RUM query string (e.g., "@type:view @application.id:abc")'),
3132
+ from: z13.string().optional().describe('Start time (ISO 8601, relative like "1h", "7d", or precise like "1d@10:00")'),
3133
+ to: z13.string().optional().describe('End time (ISO 8601, relative like "now", or precise timestamp)'),
3134
+ type: z13.enum(["all", "view", "action", "error", "long_task", "resource"]).optional().describe("RUM event type filter"),
3135
+ sort: z13.enum(["timestamp", "-timestamp"]).optional().describe("Sort order for events"),
3136
+ limit: z13.number().optional().describe("Maximum number of events to return"),
3137
+ groupBy: z13.array(z13.string()).optional().describe('Fields to group by for aggregation (e.g., ["@view.url_path", "@session.type"])'),
3138
+ compute: z13.object({
3139
+ aggregation: z13.enum(["count", "cardinality", "avg", "sum", "min", "max", "percentile"]).optional(),
3140
+ metric: z13.string().optional(),
3141
+ interval: z13.string().optional()
3142
+ }).optional().describe("Compute configuration for aggregation"),
3143
+ // Performance action parameters
3144
+ metrics: z13.array(z13.enum(["lcp", "fcp", "cls", "fid", "inp", "loading_time"])).optional().describe("Core Web Vitals metrics to retrieve (default: all). lcp=Largest Contentful Paint, fcp=First Contentful Paint, cls=Cumulative Layout Shift, fid=First Input Delay, inp=Interaction to Next Paint, loading_time=View loading time"),
3145
+ // Waterfall action parameters
3146
+ applicationId: z13.string().optional().describe("Application ID for waterfall action"),
3147
+ sessionId: z13.string().optional().describe("Session ID for waterfall action"),
3148
+ viewId: z13.string().optional().describe("View ID for waterfall action (optional, filters to specific view)")
3149
+ };
3150
+ function formatApplication(app) {
3151
+ const attrs = app.attributes ?? {};
3152
+ return {
3153
+ id: app.id ?? "",
3154
+ name: attrs.name ?? "",
3155
+ type: String(attrs.type ?? ""),
3156
+ orgId: attrs.orgId ?? 0,
3157
+ hash: attrs.hash ?? null,
3158
+ createdAt: attrs.createdAt ? new Date(attrs.createdAt).toISOString() : "",
3159
+ updatedAt: attrs.updatedAt ? new Date(attrs.updatedAt).toISOString() : ""
3160
+ };
3161
+ }
3162
+ function formatEvent(event) {
3163
+ const attrs = event.attributes ?? {};
3164
+ const appAttrs = attrs.attributes ?? {};
3165
+ const application = appAttrs["application"] ?? {};
3166
+ const session = appAttrs["session"] ?? {};
3167
+ const view = appAttrs["view"] ?? {};
3168
+ const usr = appAttrs["usr"] ?? {};
3169
+ const action = appAttrs["action"] ?? {};
3170
+ const error = appAttrs["error"] ?? {};
3171
+ const resource = appAttrs["resource"] ?? {};
3172
+ return {
3173
+ id: event.id ?? "",
3174
+ type: String(event.type ?? ""),
3175
+ timestamp: attrs.timestamp?.toISOString() ?? "",
3176
+ attributes: {
3177
+ application: {
3178
+ id: application["id"] ?? null,
3179
+ name: application["name"] ?? null
3180
+ },
3181
+ session: {
3182
+ id: session["id"] ?? null,
3183
+ type: session["type"] ?? null
3184
+ },
3185
+ view: {
3186
+ id: view["id"] ?? null,
3187
+ url: view["url"] ?? null,
3188
+ urlPath: view["url_path"] ?? null,
3189
+ name: view["name"] ?? null
3190
+ },
3191
+ user: {
3192
+ id: usr["id"] ?? null,
3193
+ email: usr["email"] ?? null,
3194
+ name: usr["name"] ?? null
3195
+ },
3196
+ action: action["id"] ? {
3197
+ id: action["id"] ?? null,
3198
+ type: action["type"] ?? null,
3199
+ name: action["name"] ?? null
3200
+ } : void 0,
3201
+ error: error["message"] ? {
3202
+ message: error["message"] ?? null,
3203
+ source: error["source"] ?? null,
3204
+ stack: error["stack"] ?? null
3205
+ } : void 0,
3206
+ resource: resource["url"] ? {
3207
+ url: resource["url"] ?? null,
3208
+ type: resource["type"] ?? null,
3209
+ duration: resource["duration"] ?? null
3210
+ } : void 0
3211
+ }
3212
+ };
3213
+ }
3214
+ async function listApplications(api) {
3215
+ const response = await api.getRUMApplications();
3216
+ const applications = (response.data ?? []).map(formatApplication);
3217
+ return {
3218
+ applications,
3219
+ totalCount: applications.length
3220
+ };
3221
+ }
3222
+ async function searchEvents(api, params, limits, site) {
3223
+ let queryString = params.query ?? "*";
3224
+ if (params.type && params.type !== "all") {
3225
+ queryString = `@type:${params.type} ${queryString}`.trim();
3226
+ }
3227
+ const nowMs = Date.now();
3228
+ const defaultFromMs = nowMs - 15 * 60 * 1e3;
3229
+ const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
3230
+ const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
3231
+ const response = await api.listRUMEvents({
3232
+ filterQuery: queryString,
3233
+ filterFrom: new Date(fromTime * 1e3),
3234
+ filterTo: new Date(toTime * 1e3),
3235
+ sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
3236
+ pageLimit: Math.min(params.limit ?? limits.maxResults, limits.maxResults)
3237
+ });
3238
+ const events = (response.data ?? []).map(formatEvent);
3239
+ return {
3240
+ events,
3241
+ meta: {
3242
+ totalCount: events.length,
3243
+ timeRange: {
3244
+ from: new Date(fromTime * 1e3).toISOString(),
3245
+ to: new Date(toTime * 1e3).toISOString()
3246
+ },
3247
+ datadog_url: buildRumUrl(queryString, fromTime, toTime, site)
3248
+ }
3249
+ };
3250
+ }
3251
+ async function aggregateEvents(api, params, _limits, site) {
3252
+ const nowMs = Date.now();
3253
+ const defaultFromMs = nowMs - 60 * 60 * 1e3;
3254
+ const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
3255
+ const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
3256
+ const groupByConfigs = (params.groupBy ?? []).map((field) => ({
3257
+ facet: field,
3258
+ limit: 10,
3259
+ sort: {
3260
+ type: "measure",
3261
+ aggregation: "count",
3262
+ order: "desc"
3263
+ }
3264
+ }));
3265
+ const computeConfig = {
3266
+ aggregation: params.compute?.aggregation ?? "count"
3267
+ };
3268
+ if (params.compute?.metric) {
3269
+ computeConfig.metric = params.compute.metric;
3270
+ computeConfig.type = "total";
3271
+ }
3272
+ if (params.compute?.interval) {
3273
+ computeConfig.interval = params.compute.interval;
3274
+ computeConfig.type = "timeseries";
3275
+ }
3276
+ const computeConfigs = [computeConfig];
3277
+ const queryString = params.query ?? "*";
3278
+ const response = await api.aggregateRUMEvents({
3279
+ body: {
3280
+ filter: {
3281
+ query: queryString,
3282
+ from: new Date(fromTime * 1e3).toISOString(),
3283
+ to: new Date(toTime * 1e3).toISOString()
3284
+ },
3285
+ groupBy: groupByConfigs.length > 0 ? groupByConfigs : void 0,
3286
+ compute: computeConfigs
3287
+ }
3288
+ });
3289
+ const buckets = (response.data?.buckets ?? []).map((bucket) => ({
3290
+ by: bucket.by ?? {},
3291
+ computes: bucket.computes ?? {}
3292
+ }));
3293
+ return {
3294
+ buckets,
3295
+ meta: {
3296
+ elapsed: response.meta?.elapsed ?? 0,
3297
+ timeRange: {
3298
+ from: new Date(fromTime * 1e3).toISOString(),
3299
+ to: new Date(toTime * 1e3).toISOString()
3300
+ },
3301
+ datadog_url: buildRumUrl(queryString, fromTime, toTime, site)
3302
+ }
3303
+ };
3304
+ }
3305
+ var METRIC_CONFIGS = {
3306
+ lcp: {
3307
+ field: "@view.largest_contentful_paint",
3308
+ aggregations: ["avg", "pc75", "pc90"]
3309
+ },
3310
+ fcp: {
3311
+ field: "@view.first_contentful_paint",
3312
+ aggregations: ["avg", "pc75", "pc90"]
3313
+ },
3314
+ cls: {
3315
+ field: "@view.cumulative_layout_shift",
3316
+ aggregations: ["avg", "pc75"]
3317
+ },
3318
+ fid: {
3319
+ field: "@view.first_input_delay",
3320
+ aggregations: ["avg", "pc75", "pc90"]
3321
+ },
3322
+ inp: {
3323
+ field: "@view.interaction_to_next_paint",
3324
+ aggregations: ["avg", "pc75", "pc90"]
3325
+ },
3326
+ loading_time: {
3327
+ field: "@view.loading_time",
3328
+ aggregations: ["avg", "pc75", "pc90"]
3329
+ }
3330
+ };
3331
+ async function getPerformanceMetrics(api, params, _limits, site) {
3332
+ const nowMs = Date.now();
3333
+ const defaultFromMs = nowMs - 60 * 60 * 1e3;
3334
+ const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
3335
+ const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
3336
+ const requestedMetrics = params.metrics ?? ["lcp", "fcp", "cls"];
3337
+ const computeConfigs = [];
3338
+ for (const metricName of requestedMetrics) {
3339
+ const config = METRIC_CONFIGS[metricName];
3340
+ if (!config) continue;
3341
+ for (const aggregation of config.aggregations) {
3342
+ computeConfigs.push({
3343
+ aggregation,
3344
+ metric: config.field,
3345
+ type: "total"
3346
+ });
3347
+ }
3348
+ }
3349
+ const groupByConfigs = (params.groupBy ?? []).map((field) => ({
3350
+ facet: field,
3351
+ limit: 10,
3352
+ sort: {
3353
+ type: "measure",
3354
+ aggregation: "count",
3355
+ order: "desc"
3356
+ }
3357
+ }));
3358
+ const viewQuery = params.query ? `@type:view ${params.query}` : "@type:view";
3359
+ const response = await api.aggregateRUMEvents({
3360
+ body: {
3361
+ filter: {
3362
+ query: viewQuery,
3363
+ from: new Date(fromTime * 1e3).toISOString(),
3364
+ to: new Date(toTime * 1e3).toISOString()
3365
+ },
3366
+ groupBy: groupByConfigs.length > 0 ? groupByConfigs : void 0,
3367
+ compute: computeConfigs
3368
+ }
3369
+ });
3370
+ const buckets = (response.data?.buckets ?? []).map((bucket) => {
3371
+ const computes = bucket.computes ?? {};
3372
+ const metrics = {};
3373
+ for (const metricName of requestedMetrics) {
3374
+ const config = METRIC_CONFIGS[metricName];
3375
+ if (!config) continue;
3376
+ metrics[metricName] = {};
3377
+ for (const aggregation of config.aggregations) {
3378
+ const computeIndex = computeConfigs.findIndex(
3379
+ (c) => c.metric === config.field && c.aggregation === aggregation
3380
+ );
3381
+ const key = `c${computeIndex}`;
3382
+ const value = computes[key]?.value;
3383
+ metrics[metricName][String(aggregation)] = value ?? null;
3384
+ }
3385
+ }
3386
+ return {
3387
+ by: bucket.by ?? {},
3388
+ metrics
3389
+ };
3390
+ });
3391
+ return {
3392
+ buckets,
3393
+ meta: {
3394
+ metrics: requestedMetrics,
3395
+ timeRange: {
3396
+ from: new Date(fromTime * 1e3).toISOString(),
3397
+ to: new Date(toTime * 1e3).toISOString()
3398
+ },
3399
+ datadog_url: buildRumUrl(viewQuery, fromTime, toTime, site)
3400
+ }
3401
+ };
3402
+ }
3403
+ function formatWaterfallEvent(event) {
3404
+ const attrs = event.attributes ?? {};
3405
+ const appAttrs = attrs.attributes ?? {};
3406
+ const view = appAttrs["view"] ?? {};
3407
+ const resource = appAttrs["resource"] ?? {};
3408
+ const action = appAttrs["action"] ?? {};
3409
+ const error = appAttrs["error"] ?? {};
3410
+ const longTask = appAttrs["long_task"] ?? {};
3411
+ const eventType = appAttrs["type"] ?? "unknown";
3412
+ return {
3413
+ id: event.id ?? "",
3414
+ type: eventType,
3415
+ timestamp: attrs.timestamp?.toISOString() ?? "",
3416
+ duration: view["loading_time"] ?? resource["duration"] ?? null,
3417
+ view: {
3418
+ id: view["id"] ?? null,
3419
+ url: view["url"] ?? null,
3420
+ name: view["name"] ?? null
3421
+ },
3422
+ resource: resource["url"] ? {
3423
+ url: resource["url"] ?? null,
3424
+ type: resource["type"] ?? null,
3425
+ duration: resource["duration"] ?? null,
3426
+ size: resource["size"] ?? null,
3427
+ statusCode: resource["status_code"] ?? null
3428
+ } : void 0,
3429
+ action: action["id"] ? {
3430
+ id: action["id"] ?? null,
3431
+ type: action["type"] ?? null,
3432
+ name: action["name"] ?? null,
3433
+ target: action["target"] ?? null
3434
+ } : void 0,
3435
+ error: error["message"] ? {
3436
+ message: error["message"] ?? null,
3437
+ source: error["source"] ?? null,
3438
+ type: error["type"] ?? null
3439
+ } : void 0,
3440
+ longTask: longTask["duration"] ? {
3441
+ duration: longTask["duration"] ?? null
3442
+ } : void 0
3443
+ };
3444
+ }
3445
+ async function getSessionWaterfall(api, params, limits, site) {
3446
+ const queryParts = [
3447
+ `@application.id:${params.applicationId}`,
3448
+ `@session.id:${params.sessionId}`
3449
+ ];
3450
+ if (params.viewId) {
3451
+ queryParts.push(`@view.id:${params.viewId}`);
3452
+ }
3453
+ const response = await api.listRUMEvents({
3454
+ filterQuery: queryParts.join(" "),
3455
+ sort: "timestamp",
3456
+ pageLimit: Math.min(limits.maxResults, 1e3)
3457
+ });
3458
+ const events = (response.data ?? []).map(formatWaterfallEvent);
3459
+ const summary = {
3460
+ views: events.filter((e) => e.type === "view").length,
3461
+ resources: events.filter((e) => e.type === "resource").length,
3462
+ actions: events.filter((e) => e.type === "action").length,
3463
+ errors: events.filter((e) => e.type === "error").length,
3464
+ longTasks: events.filter((e) => e.type === "long_task").length
3465
+ };
3466
+ return {
3467
+ events,
3468
+ summary,
3469
+ meta: {
3470
+ totalCount: events.length,
3471
+ applicationId: params.applicationId,
3472
+ sessionId: params.sessionId,
3473
+ viewId: params.viewId ?? null,
3474
+ datadog_url: buildRumSessionUrl(params.applicationId, params.sessionId, site)
3475
+ }
3476
+ };
3477
+ }
3478
+ function registerRumTool(server, api, limits, site = "datadoghq.com") {
3479
+ server.tool(
3480
+ "rum",
3481
+ "Query Datadog Real User Monitoring (RUM) data. Actions: applications (list RUM apps), events (search RUM events), aggregate (group and count events), performance (Core Web Vitals: LCP, FCP, CLS, FID, INP), waterfall (session timeline with resources/actions/errors). Use for: frontend performance, user sessions, page views, errors, resource loading.",
3482
+ InputSchema12,
3483
+ async ({ action, query, from, to, type, sort, limit, groupBy, compute, metrics, applicationId, sessionId, viewId }) => {
3484
+ try {
3485
+ switch (action) {
3486
+ case "applications":
3487
+ return toolResult(await listApplications(api));
3488
+ case "events":
3489
+ return toolResult(await searchEvents(api, { query, from, to, type, sort, limit }, limits, site));
3490
+ case "aggregate":
3491
+ return toolResult(await aggregateEvents(api, { query, from, to, groupBy, compute }, limits, site));
3492
+ case "performance":
3493
+ return toolResult(await getPerformanceMetrics(api, { query, from, to, groupBy, metrics }, limits, site));
3494
+ case "waterfall":
3495
+ if (!applicationId || !sessionId) {
3496
+ throw new Error("waterfall action requires applicationId and sessionId parameters");
3497
+ }
3498
+ return toolResult(await getSessionWaterfall(api, { applicationId, sessionId, viewId }, limits, site));
3499
+ default:
3500
+ throw new Error(`Unknown action: ${action}`);
3501
+ }
3502
+ } catch (error) {
3503
+ handleDatadogError(error);
3504
+ }
3505
+ }
3506
+ );
3507
+ }
3508
+
3509
+ // src/tools/security.ts
3510
+ import { z as z14 } from "zod";
3511
+ var ActionSchema13 = z14.enum(["rules", "signals", "findings"]);
3512
+ var InputSchema13 = {
3513
+ action: ActionSchema13.describe("Action to perform"),
3514
+ id: z14.string().optional().describe("Rule or signal ID (for specific lookups)"),
3515
+ query: z14.string().optional().describe("Search query for signals or findings"),
3516
+ from: z14.string().optional().describe('Start time (ISO 8601, relative like "1h", "7d")'),
3517
+ to: z14.string().optional().describe('End time (ISO 8601, relative like "now")'),
3518
+ severity: z14.enum(["info", "low", "medium", "high", "critical"]).optional().describe("Filter by severity"),
3519
+ status: z14.enum(["open", "under_review", "archived"]).optional().describe("Filter signals by status"),
3520
+ pageSize: z14.number().optional().describe("Number of results to return"),
3521
+ pageCursor: z14.string().optional().describe("Cursor for pagination")
3522
+ };
3523
+ function formatRule(rule) {
3524
+ const ruleData = rule;
3525
+ return {
3526
+ id: ruleData["id"] ?? "",
3527
+ name: ruleData["name"] ?? "",
3528
+ type: ruleData["type"] ?? "",
3529
+ isEnabled: ruleData["isEnabled"] ?? false,
3530
+ hasExtendedTitle: ruleData["hasExtendedTitle"] ?? false,
3531
+ message: ruleData["message"] ?? null,
3532
+ tags: ruleData["tags"] ?? [],
3533
+ createdAt: ruleData["createdAt"] ? new Date(ruleData["createdAt"]).toISOString() : "",
3534
+ updatedAt: ruleData["updatedAt"] ? new Date(ruleData["updatedAt"]).toISOString() : "",
3535
+ creationAuthorId: ruleData["creationAuthorId"] ?? null,
3536
+ isDefault: ruleData["isDefault"] ?? false,
3537
+ isDeleted: ruleData["isDeleted"] ?? false,
3538
+ filters: (ruleData["filters"] ?? []).map((f) => ({
3539
+ action: f.action ?? "",
3540
+ query: f.query ?? ""
3541
+ }))
3542
+ };
3543
+ }
3544
+ function formatSignal(signal) {
3545
+ const attrs = signal.attributes ?? {};
3546
+ const customAttrs = attrs;
3547
+ return {
3548
+ id: signal.id ?? "",
3549
+ type: String(signal.type ?? ""),
3550
+ timestamp: attrs.timestamp?.toISOString() ?? "",
3551
+ attributes: {
3552
+ message: attrs.message ?? null,
3553
+ status: customAttrs["status"] ?? null,
3554
+ severity: customAttrs["severity"] ?? null,
3555
+ tags: attrs.tags ?? [],
3556
+ custom: attrs.custom ?? {}
3557
+ }
3558
+ };
3559
+ }
3560
+ async function listRules(api, params, limits) {
3561
+ const response = await api.listSecurityMonitoringRules({
3562
+ pageSize: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
3563
+ pageNumber: 0
3564
+ });
3565
+ const rules = (response.data ?? []).map(formatRule);
3566
+ return {
3567
+ rules,
3568
+ meta: {
3569
+ totalCount: rules.length
3570
+ }
3571
+ };
3572
+ }
3573
+ async function getRule(api, ruleId) {
3574
+ const response = await api.getSecurityMonitoringRule({ ruleId });
3575
+ return {
3576
+ rule: formatRule(response)
3577
+ };
3578
+ }
3579
+ async function searchSignals(api, params, limits) {
3580
+ const nowMs = Date.now();
3581
+ const defaultFromMs = nowMs - 24 * 60 * 60 * 1e3;
3582
+ const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
3583
+ const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
3584
+ let queryString = params.query ?? "*";
3585
+ if (params.severity) {
3586
+ queryString = `severity:${params.severity} ${queryString}`.trim();
3587
+ }
3588
+ if (params.status) {
3589
+ queryString = `status:${params.status} ${queryString}`.trim();
3590
+ }
3591
+ const response = await api.searchSecurityMonitoringSignals({
3592
+ body: {
3593
+ filter: {
3594
+ query: queryString,
3595
+ from: new Date(fromTime * 1e3),
3596
+ to: new Date(toTime * 1e3)
3597
+ },
3598
+ page: {
3599
+ limit: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
3600
+ cursor: params.pageCursor
3601
+ },
3602
+ sort: "timestamp"
3603
+ }
3604
+ });
3605
+ const signals = (response.data ?? []).map(formatSignal);
3606
+ return {
3607
+ signals,
3608
+ meta: {
3609
+ nextCursor: response.meta?.page?.after ?? null,
3610
+ totalCount: signals.length,
3611
+ timeRange: {
3612
+ from: new Date(fromTime * 1e3).toISOString(),
3613
+ to: new Date(toTime * 1e3).toISOString()
3614
+ }
3615
+ }
3616
+ };
3617
+ }
3618
+ async function listFindings(api, params, limits) {
3619
+ const response = await api.searchSecurityMonitoringSignals({
3620
+ body: {
3621
+ filter: {
3622
+ query: params.query ?? "@workflow.rule.type:workload_security OR @workflow.rule.type:cloud_configuration",
3623
+ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3),
3624
+ // Last 7 days
3625
+ to: /* @__PURE__ */ new Date()
3626
+ },
3627
+ page: {
3628
+ limit: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
3629
+ cursor: params.pageCursor
3630
+ }
3631
+ }
3632
+ });
3633
+ const findings = (response.data ?? []).map(formatSignal);
3634
+ return {
3635
+ findings,
3636
+ meta: {
3637
+ nextCursor: response.meta?.page?.after ?? null,
3638
+ totalCount: findings.length
3639
+ }
3640
+ };
3641
+ }
3642
+ function registerSecurityTool(server, api, limits) {
3643
+ server.tool(
3644
+ "security",
3645
+ "Query Datadog Security Monitoring. Actions: rules (list detection rules), signals (search security signals), findings (list security findings). Use for: threat detection, compliance, security posture, incident investigation.",
3646
+ InputSchema13,
3647
+ async ({ action, id, query, from, to, severity, status, pageSize, pageCursor }) => {
3648
+ try {
3649
+ switch (action) {
3650
+ case "rules":
3651
+ if (id) {
3652
+ return toolResult(await getRule(api, id));
3653
+ }
3654
+ return toolResult(await listRules(api, { pageSize, pageCursor }, limits));
3655
+ case "signals":
3656
+ return toolResult(await searchSignals(api, { query, from, to, severity, status, pageSize, pageCursor }, limits));
3657
+ case "findings":
3658
+ return toolResult(await listFindings(api, { query, pageSize, pageCursor }, limits));
3659
+ default:
3660
+ throw new Error(`Unknown action: ${action}`);
3661
+ }
3662
+ } catch (error) {
3663
+ handleDatadogError(error);
3664
+ }
3665
+ }
3666
+ );
3667
+ }
3668
+
3669
+ // src/tools/notebooks.ts
3670
+ import { z as z15 } from "zod";
3671
+ var ActionSchema14 = z15.enum(["list", "get", "create", "update", "delete"]);
3672
+ var InputSchema14 = {
3673
+ action: ActionSchema14.describe("Action to perform"),
3674
+ id: z15.number().optional().describe("Notebook ID (required for get/update/delete actions)"),
3675
+ query: z15.string().optional().describe("Search query for notebooks"),
3676
+ authorHandle: z15.string().optional().describe("Filter by author handle (email)"),
3677
+ excludeAuthorHandle: z15.string().optional().describe("Exclude notebooks by author handle"),
3678
+ includeCells: z15.boolean().optional().describe("Include cell content in response (default: true for get)"),
3679
+ name: z15.string().optional().describe("Notebook name (for create/update)"),
3680
+ cells: z15.array(z15.object({
3681
+ type: z15.enum(["markdown", "timeseries", "toplist", "heatmap", "distribution", "log_stream"]),
3682
+ content: z15.unknown()
3683
+ })).optional().describe("Notebook cells (for create/update)"),
3684
+ time: z15.object({
3685
+ liveSpan: z15.string().optional(),
3686
+ start: z15.number().optional(),
3687
+ end: z15.number().optional()
3688
+ }).optional().describe("Time configuration for notebook"),
3689
+ status: z15.enum(["published"]).optional().describe("Notebook status"),
3690
+ pageSize: z15.number().optional().describe("Number of notebooks to return"),
3691
+ pageNumber: z15.number().optional().describe("Page number for pagination")
3692
+ };
3693
+ function formatNotebookSummary(nb) {
3694
+ const attrs = nb.attributes ?? {};
3695
+ return {
3696
+ id: nb.id ?? 0,
3697
+ name: attrs.name ?? "",
3698
+ author: {
3699
+ handle: attrs.author?.handle ?? null,
3700
+ name: attrs.author?.name ?? null
3701
+ },
3702
+ status: String(attrs.status ?? ""),
3703
+ cellCount: attrs.cells?.length ?? 0,
3704
+ created: attrs.created?.toISOString() ?? "",
3705
+ modified: attrs.modified?.toISOString() ?? "",
3706
+ metadata: {
3707
+ isTemplate: attrs.metadata?.isTemplate ?? null,
3708
+ takeSnapshots: attrs.metadata?.takeSnapshots ?? null
3709
+ }
3710
+ };
3711
+ }
3712
+ function formatNotebookDetail(nb) {
3713
+ const attrs = nb.attributes ?? {};
3714
+ return {
3715
+ id: nb.id ?? 0,
3716
+ name: attrs.name ?? "",
3717
+ author: {
3718
+ handle: attrs.author?.handle ?? null,
3719
+ name: attrs.author?.name ?? null
3720
+ },
3721
+ status: String(attrs.status ?? ""),
3722
+ cellCount: attrs.cells?.length ?? 0,
3723
+ created: attrs.created?.toISOString() ?? "",
3724
+ modified: attrs.modified?.toISOString() ?? "",
3725
+ metadata: {
3726
+ isTemplate: attrs.metadata?.isTemplate ?? null,
3727
+ takeSnapshots: attrs.metadata?.takeSnapshots ?? null
3728
+ },
3729
+ cells: (attrs.cells ?? []).map((cell) => ({
3730
+ id: String(cell.id ?? ""),
3731
+ type: String(cell.type ?? ""),
3732
+ attributes: cell.attributes ?? {}
3733
+ })),
3734
+ time: {
3735
+ liveSpan: attrs.time ? String(attrs.time["liveSpan"] ?? "") : null
3736
+ }
3737
+ };
3738
+ }
3739
+ async function listNotebooks(api, params, limits) {
3740
+ const response = await api.listNotebooks({
3741
+ query: params.query,
3742
+ authorHandle: params.authorHandle,
3743
+ excludeAuthorHandle: params.excludeAuthorHandle,
3744
+ includeCells: params.includeCells ?? false,
3745
+ count: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
3746
+ start: (params.pageNumber ?? 0) * (params.pageSize ?? limits.maxResults)
3747
+ });
3748
+ const notebooks = (response.data ?? []).map(formatNotebookSummary);
3749
+ return {
3750
+ notebooks,
3751
+ meta: {
3752
+ totalCount: response.meta?.page?.totalCount ?? notebooks.length,
3753
+ totalFilteredCount: response.meta?.page?.totalFilteredCount ?? notebooks.length
3754
+ }
3755
+ };
3756
+ }
3757
+ async function getNotebook(api, notebookId) {
3758
+ const response = await api.getNotebook({ notebookId });
3759
+ if (!response.data) {
3760
+ throw new Error(`Notebook ${notebookId} not found`);
3761
+ }
3762
+ return {
3763
+ notebook: formatNotebookDetail(response.data)
3764
+ };
3765
+ }
3766
+ async function createNotebook(api, params) {
3767
+ const cells = (params.cells ?? []).map((cell) => {
3768
+ if (cell.type === "markdown") {
3769
+ return {
3770
+ type: "notebook_cells",
3771
+ attributes: {
3772
+ definition: {
3773
+ type: "markdown",
3774
+ text: String(cell.content ?? "")
3775
+ }
3776
+ }
3777
+ };
3778
+ }
3779
+ return {
3780
+ type: "notebook_cells",
3781
+ attributes: {
3782
+ definition: cell.content
3783
+ }
3784
+ };
3785
+ });
3786
+ if (cells.length === 0) {
3787
+ cells.push({
3788
+ type: "notebook_cells",
3789
+ attributes: {
3790
+ definition: {
3791
+ type: "markdown",
3792
+ text: "# New Notebook\n\nStart adding content here."
3793
+ }
3794
+ }
3795
+ });
3796
+ }
3797
+ const timeConfig = params.time?.liveSpan ? { liveSpan: params.time.liveSpan } : { liveSpan: "1h" };
3798
+ const response = await api.createNotebook({
3799
+ body: {
3800
+ data: {
3801
+ type: "notebooks",
3802
+ attributes: {
3803
+ name: params.name,
3804
+ cells,
3805
+ time: timeConfig,
3806
+ status: params.status ?? "published"
3807
+ }
3808
+ }
3809
+ }
3810
+ });
3811
+ if (!response.data) {
3812
+ throw new Error("Failed to create notebook");
3813
+ }
3814
+ return {
3815
+ success: true,
3816
+ notebook: formatNotebookDetail(response.data),
3817
+ message: `Notebook "${params.name}" created successfully`
3818
+ };
3819
+ }
3820
+ async function updateNotebook(api, notebookId, params) {
3821
+ const existing = await api.getNotebook({ notebookId });
3822
+ if (!existing.data) {
3823
+ throw new Error(`Notebook ${notebookId} not found`);
3824
+ }
3825
+ const existingAttrs = existing.data.attributes ?? {};
3826
+ let cells;
3827
+ if (params.cells) {
3828
+ cells = params.cells.map((cell) => {
3829
+ if (cell.type === "markdown") {
3830
+ return {
3831
+ type: "notebook_cells",
3832
+ attributes: {
3833
+ definition: {
3834
+ type: "markdown",
3835
+ text: String(cell.content ?? "")
3836
+ }
3837
+ }
3838
+ };
3839
+ }
3840
+ return {
3841
+ type: "notebook_cells",
3842
+ attributes: {
3843
+ definition: cell.content
3844
+ }
3845
+ };
3846
+ });
3847
+ }
3848
+ const timeConfig = params.time?.liveSpan ? { liveSpan: params.time.liveSpan } : void 0;
3849
+ const response = await api.updateNotebook({
3850
+ notebookId,
3851
+ body: {
3852
+ data: {
3853
+ type: "notebooks",
3854
+ attributes: {
3855
+ name: params.name ?? existingAttrs.name ?? "",
3856
+ cells: cells ?? existingAttrs.cells?.map((c) => ({
3857
+ id: c.id,
3858
+ type: "notebook_cells",
3859
+ attributes: c.attributes
3860
+ })) ?? [],
3861
+ time: timeConfig ?? existingAttrs.time ?? { liveSpan: "1h" },
3862
+ status: params.status ?? existingAttrs.status
3863
+ }
3864
+ }
3865
+ }
3866
+ });
3867
+ if (!response.data) {
3868
+ throw new Error("Failed to update notebook");
3869
+ }
3870
+ return {
3871
+ success: true,
3872
+ notebook: formatNotebookDetail(response.data),
3873
+ message: `Notebook ${notebookId} updated successfully`
3874
+ };
3875
+ }
3876
+ async function deleteNotebook(api, notebookId) {
3877
+ await api.deleteNotebook({ notebookId });
3878
+ return {
3879
+ success: true,
3880
+ message: `Notebook ${notebookId} deleted successfully`
3881
+ };
3882
+ }
3883
+ function registerNotebooksTool(server, api, limits, readOnly = false) {
3884
+ server.tool(
3885
+ "notebooks",
3886
+ "Manage Datadog Notebooks. Actions: list (search notebooks), get (by ID with cells), create (new notebook), update (modify notebook), delete (remove notebook). Use for: runbooks, incident documentation, investigation notes, dashboards as code.",
3887
+ InputSchema14,
3888
+ async ({ action, id, query, authorHandle, excludeAuthorHandle, includeCells, name, cells, time, status, pageSize, pageNumber }) => {
3889
+ try {
3890
+ checkReadOnly(action, readOnly);
3891
+ switch (action) {
3892
+ case "list":
3893
+ return toolResult(await listNotebooks(api, { query, authorHandle, excludeAuthorHandle, includeCells, pageSize, pageNumber }, limits));
3894
+ case "get": {
3895
+ const notebookId = requireParam(id, "id", "get");
3896
+ return toolResult(await getNotebook(api, notebookId));
3897
+ }
3898
+ case "create": {
3899
+ const notebookName = requireParam(name, "name", "create");
3900
+ return toolResult(await createNotebook(api, {
3901
+ name: notebookName,
3902
+ cells,
3903
+ time,
3904
+ status
3905
+ }));
3906
+ }
3907
+ case "update": {
3908
+ const notebookId = requireParam(id, "id", "update");
3909
+ return toolResult(await updateNotebook(api, notebookId, {
3910
+ name,
3911
+ cells,
3912
+ time,
3913
+ status
3914
+ }));
3915
+ }
3916
+ case "delete": {
3917
+ const notebookId = requireParam(id, "id", "delete");
3918
+ return toolResult(await deleteNotebook(api, notebookId));
3919
+ }
3920
+ default:
3921
+ throw new Error(`Unknown action: ${action}`);
3922
+ }
3923
+ } catch (error) {
3924
+ handleDatadogError(error);
3925
+ }
3926
+ }
3927
+ );
3928
+ }
3929
+
3930
+ // src/tools/users.ts
3931
+ import { z as z16 } from "zod";
3932
+ var ActionSchema15 = z16.enum(["list", "get"]);
3933
+ var InputSchema15 = {
3934
+ action: ActionSchema15.describe("Action to perform"),
3935
+ id: z16.string().optional().describe("User ID (required for get action)"),
3936
+ filter: z16.string().optional().describe("Filter users by name or email"),
3937
+ status: z16.enum(["Active", "Pending", "Disabled"]).optional().describe("Filter by user status"),
3938
+ pageSize: z16.number().optional().describe("Number of users to return per page"),
3939
+ pageNumber: z16.number().optional().describe("Page number for pagination")
3940
+ };
3941
+ function formatUser(user) {
3942
+ const attrs = user.attributes ?? {};
3943
+ const relationships = user.relationships ?? {};
3944
+ const roles = (relationships.roles?.data ?? []).map((r) => r.id ?? "");
3945
+ const orgId = relationships.org?.data?.id ?? null;
3946
+ return {
3947
+ id: user.id ?? "",
3948
+ email: attrs.email ?? "",
3949
+ name: attrs.name ?? "",
3950
+ status: attrs.status ?? "",
3951
+ title: attrs.title ?? null,
3952
+ verified: attrs.verified ?? false,
3953
+ disabled: attrs.disabled ?? false,
3954
+ createdAt: attrs.createdAt?.toISOString() ?? "",
3955
+ modifiedAt: attrs.modifiedAt?.toISOString() ?? "",
3956
+ relationships: {
3957
+ roles,
3958
+ org: orgId
3959
+ }
3960
+ };
3961
+ }
3962
+ async function listUsers(api, params, limits) {
3963
+ const response = await api.listUsers({
3964
+ filter: params.filter,
3965
+ filterStatus: params.status,
3966
+ pageSize: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
3967
+ pageNumber: params.pageNumber ?? 0
3968
+ });
3969
+ const users = (response.data ?? []).map(formatUser);
3970
+ return {
3971
+ users,
3972
+ meta: {
3973
+ page: response.meta?.page ?? {},
3974
+ totalCount: users.length
3975
+ }
3976
+ };
3977
+ }
3978
+ async function getUser(api, userId) {
3979
+ const response = await api.getUser({ userId });
3980
+ if (!response.data) {
3981
+ throw new Error(`User ${userId} not found`);
3982
+ }
3983
+ return {
3984
+ user: formatUser(response.data)
3985
+ };
3986
+ }
3987
+ function registerUsersTool(server, api, limits) {
3988
+ server.tool(
3989
+ "users",
3990
+ "Manage Datadog users. Actions: list (with filters), get (by ID). Use for: access management, user auditing, team organization.",
3991
+ InputSchema15,
3992
+ async ({ action, id, filter, status, pageSize, pageNumber }) => {
3993
+ try {
3994
+ switch (action) {
3995
+ case "list":
3996
+ return toolResult(await listUsers(api, { filter, status, pageSize, pageNumber }, limits));
3997
+ case "get": {
3998
+ const userId = requireParam(id, "id", "get");
3999
+ return toolResult(await getUser(api, userId));
4000
+ }
4001
+ default:
4002
+ throw new Error(`Unknown action: ${action}`);
4003
+ }
4004
+ } catch (error) {
4005
+ handleDatadogError(error);
4006
+ }
4007
+ }
4008
+ );
4009
+ }
4010
+
4011
+ // src/tools/teams.ts
4012
+ import { z as z17 } from "zod";
4013
+ var ActionSchema16 = z17.enum(["list", "get", "members"]);
4014
+ var InputSchema16 = {
4015
+ action: ActionSchema16.describe("Action to perform"),
4016
+ id: z17.string().optional().describe("Team ID (required for get/members actions)"),
4017
+ filter: z17.string().optional().describe("Filter teams by name"),
4018
+ pageSize: z17.number().optional().describe("Number of teams to return per page"),
4019
+ pageNumber: z17.number().optional().describe("Page number for pagination")
4020
+ };
4021
+ function formatTeam(team) {
4022
+ const attrs = team.attributes ?? {};
4023
+ return {
4024
+ id: team.id ?? "",
4025
+ name: attrs.name ?? "",
4026
+ handle: attrs.handle ?? "",
4027
+ description: attrs.description ?? null,
4028
+ summary: attrs.summary ?? null,
4029
+ linkCount: attrs.linkCount ?? 0,
4030
+ userCount: attrs.userCount ?? 0,
4031
+ createdAt: attrs.createdAt?.toISOString() ?? "",
4032
+ modifiedAt: attrs.modifiedAt?.toISOString() ?? ""
4033
+ };
4034
+ }
4035
+ function formatTeamMember(member) {
4036
+ const attrs = member.attributes ?? {};
4037
+ const relationships = member.relationships ?? {};
4038
+ return {
4039
+ id: member.id ?? "",
4040
+ type: String(member.type ?? ""),
4041
+ attributes: {
4042
+ role: String(attrs.role ?? "")
4043
+ },
4044
+ relationships: {
4045
+ userId: relationships.user?.data?.id ?? null
4046
+ }
4047
+ };
4048
+ }
4049
+ async function listTeams(api, params, limits) {
4050
+ const response = await api.listTeams({
4051
+ filterKeyword: params.filter,
4052
+ pageSize: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
4053
+ pageNumber: params.pageNumber ?? 0
4054
+ });
4055
+ const teams = (response.data ?? []).map(formatTeam);
4056
+ return {
4057
+ teams,
4058
+ meta: {
4059
+ totalCount: teams.length
4060
+ }
4061
+ };
4062
+ }
4063
+ async function getTeam(api, teamId) {
4064
+ const response = await api.getTeam({ teamId });
4065
+ if (!response.data) {
4066
+ throw new Error(`Team ${teamId} not found`);
4067
+ }
4068
+ return {
4069
+ team: formatTeam(response.data)
4070
+ };
4071
+ }
4072
+ async function getTeamMembers(api, teamId, limits) {
4073
+ const response = await api.getTeamMemberships({
4074
+ teamId,
4075
+ pageSize: limits.maxResults
4076
+ });
4077
+ const members = (response.data ?? []).map(formatTeamMember);
4078
+ return {
4079
+ members,
4080
+ meta: {
4081
+ totalCount: members.length
4082
+ }
4083
+ };
4084
+ }
4085
+ function registerTeamsTool(server, api, limits) {
4086
+ server.tool(
4087
+ "teams",
4088
+ "Manage Datadog teams. Actions: list (with filters), get (by ID), members (list team members). Use for: team organization, access management, collaboration.",
4089
+ InputSchema16,
4090
+ async ({ action, id, filter, pageSize, pageNumber }) => {
4091
+ try {
4092
+ switch (action) {
4093
+ case "list":
4094
+ return toolResult(await listTeams(api, { filter, pageSize, pageNumber }, limits));
4095
+ case "get": {
4096
+ const teamId = requireParam(id, "id", "get");
4097
+ return toolResult(await getTeam(api, teamId));
4098
+ }
4099
+ case "members": {
4100
+ const teamId = requireParam(id, "id", "members");
4101
+ return toolResult(await getTeamMembers(api, teamId, limits));
4102
+ }
4103
+ default:
4104
+ throw new Error(`Unknown action: ${action}`);
4105
+ }
4106
+ } catch (error) {
4107
+ handleDatadogError(error);
4108
+ }
4109
+ }
4110
+ );
4111
+ }
4112
+
4113
+ // src/tools/tags.ts
4114
+ import { z as z18 } from "zod";
4115
+ var ActionSchema17 = z18.enum(["list", "get", "add", "update", "delete"]);
4116
+ var InputSchema17 = {
4117
+ action: ActionSchema17.describe("Action to perform"),
4118
+ hostName: z18.string().optional().describe("Host name (required for get/add/update/delete actions)"),
4119
+ tags: z18.array(z18.string()).optional().describe('Tags to add or set (for add/update actions). Format: "key:value"'),
4120
+ source: z18.string().optional().describe('Source of the tags (e.g., "users", "datadog"). Defaults to "users"')
4121
+ };
4122
+ async function listAllTags(api, source) {
4123
+ const response = await api.listHostTags({
4124
+ source
4125
+ });
4126
+ const tags = response.tags ?? {};
4127
+ return {
4128
+ hosts: tags,
4129
+ totalHosts: Object.keys(tags).length
4130
+ };
4131
+ }
4132
+ async function getHostTags(api, hostName, source) {
4133
+ const response = await api.getHostTags({
4134
+ hostName,
4135
+ source
4136
+ });
4137
+ return {
4138
+ hostName,
4139
+ tags: response.tags ?? [],
4140
+ source: source ?? null
4141
+ };
4142
+ }
4143
+ async function addHostTags(api, hostName, tags, source) {
4144
+ const response = await api.createHostTags({
4145
+ hostName,
4146
+ body: {
4147
+ host: hostName,
4148
+ tags
4149
+ },
4150
+ source
4151
+ });
4152
+ return {
4153
+ success: true,
4154
+ hostName,
4155
+ tags: response.tags ?? tags,
4156
+ message: `Tags added to host ${hostName}`
4157
+ };
4158
+ }
4159
+ async function updateHostTags(api, hostName, tags, source) {
4160
+ const response = await api.updateHostTags({
4161
+ hostName,
4162
+ body: {
4163
+ host: hostName,
4164
+ tags
4165
+ },
4166
+ source
4167
+ });
4168
+ return {
4169
+ success: true,
4170
+ hostName,
4171
+ tags: response.tags ?? tags,
4172
+ message: `Tags updated for host ${hostName}`
4173
+ };
4174
+ }
4175
+ async function deleteHostTags(api, hostName, source) {
4176
+ await api.deleteHostTags({
4177
+ hostName,
4178
+ source
4179
+ });
4180
+ return {
4181
+ success: true,
4182
+ hostName,
4183
+ message: `Tags deleted from host ${hostName}`
4184
+ };
4185
+ }
4186
+ function registerTagsTool(server, api, _limits, readOnly = false) {
4187
+ server.tool(
4188
+ "tags",
4189
+ "Manage Datadog host tags. Actions: list (all host tags), get (tags for specific host), add (create tags), update (replace tags), delete (remove all tags). Use for: infrastructure organization, filtering, grouping.",
4190
+ InputSchema17,
4191
+ async ({ action, hostName, tags, source }) => {
4192
+ try {
4193
+ checkReadOnly(action, readOnly);
4194
+ switch (action) {
4195
+ case "list":
4196
+ return toolResult(await listAllTags(api, source));
4197
+ case "get": {
4198
+ const host = requireParam(hostName, "hostName", "get");
4199
+ return toolResult(await getHostTags(api, host, source));
4200
+ }
4201
+ case "add": {
4202
+ const host = requireParam(hostName, "hostName", "add");
4203
+ const tagList = requireParam(tags, "tags", "add");
4204
+ return toolResult(await addHostTags(api, host, tagList, source));
4205
+ }
4206
+ case "update": {
4207
+ const host = requireParam(hostName, "hostName", "update");
4208
+ const tagList = requireParam(tags, "tags", "update");
4209
+ return toolResult(await updateHostTags(api, host, tagList, source));
4210
+ }
4211
+ case "delete": {
4212
+ const host = requireParam(hostName, "hostName", "delete");
4213
+ return toolResult(await deleteHostTags(api, host, source));
4214
+ }
4215
+ default:
4216
+ throw new Error(`Unknown action: ${action}`);
4217
+ }
4218
+ } catch (error) {
4219
+ handleDatadogError(error);
4220
+ }
4221
+ }
4222
+ );
4223
+ }
4224
+
4225
+ // src/tools/usage.ts
4226
+ import { z as z19 } from "zod";
4227
+ var ActionSchema18 = z19.enum(["summary", "hosts", "logs", "custom_metrics", "indexed_spans", "ingested_spans"]);
4228
+ var InputSchema18 = {
4229
+ action: ActionSchema18.describe("Action to perform: summary (overall usage), hosts, logs, custom_metrics, indexed_spans, ingested_spans"),
4230
+ from: z19.string().optional().describe('Start time (ISO 8601 date like "2024-01-01", or relative like "30d")'),
4231
+ to: z19.string().optional().describe('End time (ISO 8601 date like "2024-01-31", or relative like "now")'),
4232
+ includeOrgDetails: z19.boolean().optional().describe("Include usage breakdown by organization (for multi-org accounts)")
4233
+ };
4234
+ function parseDate(dateStr, defaultDate) {
4235
+ if (!dateStr) return defaultDate;
4236
+ if (dateStr.match(/^\d+[hdwmy]$/)) {
4237
+ const seconds = parseTime(dateStr, Math.floor(Date.now() / 1e3));
4238
+ return new Date(seconds * 1e3);
4239
+ }
4240
+ const parsed = new Date(dateStr);
4241
+ if (!isNaN(parsed.getTime())) {
4242
+ return parsed;
4243
+ }
4244
+ return defaultDate;
4245
+ }
4246
+ async function getUsageSummary(api, params) {
4247
+ const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
4248
+ const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
4249
+ const response = await api.getUsageSummary({
4250
+ startMonth: startDate,
4251
+ endMonth: endDate,
4252
+ includeOrgDetails: params.includeOrgDetails
4253
+ });
4254
+ return {
4255
+ startDate: response.startDate?.toISOString() ?? startDate.toISOString(),
4256
+ endDate: response.endDate?.toISOString() ?? endDate.toISOString(),
4257
+ aggsTotal: {
4258
+ apmHostTop99p: response.apmHostTop99PSum ?? null,
4259
+ infraHostTop99p: response.infraHostTop99PSum ?? null
4260
+ },
4261
+ usage: (response.usage ?? []).map((u) => ({
4262
+ date: u.date?.toISOString() ?? "",
4263
+ orgName: u["orgName"] ?? null,
4264
+ apmHostTop99pSum: u.apmHostTop99P ?? null,
4265
+ infraHostTop99pSum: u.infraHostTop99P ?? null,
4266
+ logsIndexedLogsUsageSum: u.indexedEventsCountSum ?? null,
4267
+ ingestedEventsBytesSum: u.ingestedEventsBytesSum ?? null,
4268
+ customMetricsAvgPerHour: null
4269
+ // Not in summary
4270
+ }))
4271
+ };
4272
+ }
4273
+ async function getHostsUsage(api, params) {
4274
+ const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
4275
+ const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
4276
+ const response = await api.getUsageHosts({
4277
+ startHr: startDate,
4278
+ endHr: endDate
4279
+ });
4280
+ return {
4281
+ startDate: startDate.toISOString(),
4282
+ endDate: endDate.toISOString(),
4283
+ usage: (response.usage ?? []).map((u) => ({
4284
+ date: u.hour?.toISOString() ?? "",
4285
+ agentHostTop99p: u.agentHostCount ?? null,
4286
+ awsHostTop99p: u.awsHostCount ?? null,
4287
+ azureHostTop99p: u.azureHostCount ?? null,
4288
+ gcpHostTop99p: u.gcpHostCount ?? null,
4289
+ infraHostTop99p: u.hostCount ?? null,
4290
+ containerTop99p: u.containerCount ?? null
4291
+ }))
4292
+ };
4293
+ }
4294
+ async function getLogsUsage(api, params) {
4295
+ const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
4296
+ const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
4297
+ const response = await api.getUsageLogs({
4298
+ startHr: startDate,
4299
+ endHr: endDate
4300
+ });
4301
+ return {
4302
+ startDate: startDate.toISOString(),
4303
+ endDate: endDate.toISOString(),
4304
+ usage: (response.usage ?? []).map((u) => ({
4305
+ date: u.hour?.toISOString() ?? "",
4306
+ logsIndexedLogsUsageSum: u.indexedEventsCount ?? null,
4307
+ logsLiveIndexedLogsUsageSum: u.indexedEventsCount ?? null,
4308
+ logsRehydratedIndexedLogsUsageSum: u["logsRehydratedIndexedCount"] ?? null
4309
+ }))
4310
+ };
4311
+ }
4312
+ async function getCustomMetricsUsage(api, params) {
4313
+ const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
4314
+ const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
4315
+ const response = await api.getUsageTimeseries({
4316
+ startHr: startDate,
4317
+ endHr: endDate
4318
+ });
4319
+ return {
4320
+ startDate: startDate.toISOString(),
4321
+ endDate: endDate.toISOString(),
4322
+ usage: (response.usage ?? []).map((u) => ({
4323
+ date: u.hour?.toISOString() ?? "",
4324
+ avgMetricsCount: u.numCustomTimeseries ?? null,
4325
+ maxMetricsCount: null
4326
+ // Not directly available
4327
+ }))
4328
+ };
4329
+ }
4330
+ async function getIndexedSpansUsage(api, params) {
4331
+ const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
4332
+ const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
4333
+ const response = await api.getUsageIndexedSpans({
4334
+ startHr: startDate,
4335
+ endHr: endDate
4336
+ });
4337
+ return {
4338
+ startDate: startDate.toISOString(),
4339
+ endDate: endDate.toISOString(),
4340
+ usage: (response.usage ?? []).map((u) => ({
4341
+ date: u.hour?.toISOString() ?? "",
4342
+ indexedSpansCount: u.indexedEventsCount ?? null,
4343
+ ingestedSpansBytes: null
4344
+ }))
4345
+ };
4346
+ }
4347
+ async function getIngestedSpansUsage(api, params) {
4348
+ const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
4349
+ const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
4350
+ const response = await api.getIngestedSpans({
4351
+ startHr: startDate,
4352
+ endHr: endDate
4353
+ });
4354
+ return {
4355
+ startDate: startDate.toISOString(),
4356
+ endDate: endDate.toISOString(),
4357
+ usage: (response.usage ?? []).map((u) => ({
4358
+ date: u.hour?.toISOString() ?? "",
4359
+ indexedSpansCount: null,
4360
+ ingestedSpansBytes: u["ingestedTracesBytes"] ?? null
4361
+ }))
4362
+ };
4363
+ }
4364
+ function registerUsageTool(server, api, _limits) {
4365
+ server.tool(
4366
+ "usage",
4367
+ "Query Datadog usage metering data. Actions: summary (overall usage), hosts (infrastructure), logs, custom_metrics, indexed_spans, ingested_spans. Use for: cost management, capacity planning, usage tracking, billing analysis.",
4368
+ InputSchema18,
4369
+ async ({ action, from, to, includeOrgDetails }) => {
4370
+ try {
4371
+ switch (action) {
4372
+ case "summary":
4373
+ return toolResult(await getUsageSummary(api, { from, to, includeOrgDetails }));
4374
+ case "hosts":
4375
+ return toolResult(await getHostsUsage(api, { from, to }));
4376
+ case "logs":
4377
+ return toolResult(await getLogsUsage(api, { from, to }));
4378
+ case "custom_metrics":
4379
+ return toolResult(await getCustomMetricsUsage(api, { from, to }));
4380
+ case "indexed_spans":
4381
+ return toolResult(await getIndexedSpansUsage(api, { from, to }));
4382
+ case "ingested_spans":
4383
+ return toolResult(await getIngestedSpansUsage(api, { from, to }));
4384
+ default:
4385
+ throw new Error(`Unknown action: ${action}`);
4386
+ }
4387
+ } catch (error) {
4388
+ handleDatadogError(error);
4389
+ }
4390
+ }
4391
+ );
4392
+ }
4393
+
4394
+ // src/tools/auth.ts
4395
+ import { z as z20 } from "zod";
4396
+ var ActionSchema19 = z20.enum(["validate"]);
4397
+ var InputSchema19 = {
4398
+ action: ActionSchema19.describe("Action to perform: validate - test if API key and App key are valid")
4399
+ };
4400
+ function registerAuthTool(server, clients) {
4401
+ server.tool(
4402
+ "auth",
4403
+ "Validate Datadog API credentials. Use this to verify that the API key and App key are correctly configured before performing other operations.",
4404
+ InputSchema19,
4405
+ async ({ action }) => {
4406
+ try {
4407
+ switch (action) {
4408
+ case "validate":
4409
+ return toolResult(await validateCredentials(clients));
4410
+ default:
4411
+ throw new Error(`Unknown action: ${action}`);
4412
+ }
4413
+ } catch (error) {
4414
+ handleDatadogError(error);
4415
+ }
4416
+ }
4417
+ );
4418
+ }
4419
+ async function validateCredentials(clients) {
4420
+ const apiKeyResult = await clients.auth.validate();
4421
+ if (!apiKeyResult.valid) {
4422
+ return {
4423
+ valid: false,
4424
+ apiKeyValid: false,
4425
+ appKeyValid: false,
4426
+ error: "API key is invalid",
4427
+ suggestion: "Check that your DD_API_KEY environment variable is correct"
4428
+ };
4429
+ }
4430
+ try {
4431
+ await clients.users.listUsers({ pageSize: 1 });
4432
+ return {
4433
+ valid: true,
4434
+ apiKeyValid: true,
4435
+ appKeyValid: true,
4436
+ message: "Both API key and App key are valid and working",
4437
+ permissions: "Credentials have sufficient permissions to access the Datadog API"
4438
+ };
4439
+ } catch (appKeyError) {
4440
+ const errorMessage = appKeyError instanceof Error ? appKeyError.message : String(appKeyError);
4441
+ const isAuthError = errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("Forbidden");
4442
+ return {
4443
+ valid: !isAuthError,
4444
+ apiKeyValid: true,
4445
+ appKeyValid: !isAuthError,
4446
+ warning: isAuthError ? "App key may be invalid or have insufficient permissions" : "API key is valid. App key validation inconclusive.",
4447
+ error: errorMessage,
4448
+ suggestion: isAuthError ? "Check that your DD_APP_KEY environment variable is correct and has appropriate scopes" : "Credentials appear valid but encountered an issue during validation"
4449
+ };
4450
+ }
4451
+ }
4452
+
4453
+ // src/tools/index.ts
4454
+ function registerAllTools(server, clients, limits, features, site = "datadoghq.com") {
4455
+ const { readOnly, disabledTools } = features;
4456
+ const enabled = (tool) => !disabledTools.includes(tool);
4457
+ if (enabled("monitors")) registerMonitorsTool(server, clients.monitors, limits, readOnly, site);
4458
+ if (enabled("dashboards")) registerDashboardsTool(server, clients.dashboards, limits, readOnly, site);
4459
+ if (enabled("logs")) registerLogsTool(server, clients.logs, limits, site);
4460
+ if (enabled("metrics")) registerMetricsTool(server, clients.metricsV1, clients.metricsV2, limits, site);
4461
+ if (enabled("traces")) registerTracesTool(server, clients.spans, clients.services, limits, site);
4462
+ if (enabled("events")) registerEventsTool(server, clients.eventsV1, clients.eventsV2, clients.monitors, limits, readOnly, site);
4463
+ if (enabled("incidents")) registerIncidentsTool(server, clients.incidents, limits, readOnly, site);
4464
+ if (enabled("slos")) registerSlosTool(server, clients.slo, limits, readOnly, site);
4465
+ if (enabled("synthetics")) registerSyntheticsTool(server, clients.synthetics, limits, readOnly, site);
4466
+ if (enabled("hosts")) registerHostsTool(server, clients.hosts, limits, readOnly);
4467
+ if (enabled("downtimes")) registerDowntimesTool(server, clients.downtimes, limits, readOnly);
4468
+ if (enabled("rum")) registerRumTool(server, clients.rum, limits, site);
4469
+ if (enabled("security")) registerSecurityTool(server, clients.security, limits);
4470
+ if (enabled("notebooks")) registerNotebooksTool(server, clients.notebooks, limits, readOnly, site);
4471
+ if (enabled("users")) registerUsersTool(server, clients.users, limits);
4472
+ if (enabled("teams")) registerTeamsTool(server, clients.teams, limits);
4473
+ if (enabled("tags")) registerTagsTool(server, clients.tags, limits, readOnly);
4474
+ if (enabled("usage")) registerUsageTool(server, clients.usage, limits);
4475
+ if (enabled("auth")) registerAuthTool(server, clients);
4476
+ }
4477
+
4478
+ // src/server.ts
4479
+ function createServer(config) {
4480
+ const server = new McpServer({
4481
+ name: config.server.name,
4482
+ version: config.server.version
4483
+ });
4484
+ const clients = createDatadogClients(config.datadog);
4485
+ registerAllTools(server, clients, config.limits, config.features, config.datadog.site);
4486
+ return server;
4487
+ }
4488
+
4489
+ // src/transport/stdio.ts
4490
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4491
+ async function connectStdio(server) {
4492
+ const transport = new StdioServerTransport();
4493
+ await server.connect(transport);
4494
+ console.error("[MCP] Datadog MCP server running on stdio");
4495
+ }
4496
+
4497
+ // src/transport/http.ts
4498
+ import express from "express";
4499
+ import { randomUUID } from "crypto";
4500
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4501
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4502
+ var transports = {};
4503
+ async function connectHttp(server, config) {
4504
+ const app = express();
4505
+ app.use(express.json());
4506
+ app.get("/health", (_req, res) => {
4507
+ res.json({ status: "ok", name: config.name, version: config.version });
4508
+ });
4509
+ app.post("/mcp", async (req, res) => {
4510
+ const sessionId = req.headers["mcp-session-id"];
4511
+ let transport;
4512
+ if (sessionId && transports[sessionId]) {
4513
+ transport = transports[sessionId];
4514
+ } else if (!sessionId && isInitializeRequest(req.body)) {
4515
+ transport = new StreamableHTTPServerTransport({
4516
+ sessionIdGenerator: () => randomUUID(),
4517
+ onsessioninitialized: (id) => {
4518
+ transports[id] = transport;
4519
+ console.error(`[MCP] Session initialized: ${id}`);
4520
+ }
4521
+ });
4522
+ transport.onclose = () => {
4523
+ if (transport.sessionId) {
4524
+ delete transports[transport.sessionId];
4525
+ console.error(`[MCP] Session closed: ${transport.sessionId}`);
4526
+ }
4527
+ };
4528
+ await server.connect(transport);
4529
+ } else {
4530
+ res.status(400).json({
4531
+ jsonrpc: "2.0",
4532
+ error: { code: -32e3, message: "Invalid session" },
4533
+ id: null
4534
+ });
4535
+ return;
4536
+ }
4537
+ await transport.handleRequest(req, res, req.body);
4538
+ });
4539
+ app.get("/mcp", async (req, res) => {
4540
+ const sessionId = req.headers["mcp-session-id"];
4541
+ const transport = transports[sessionId];
4542
+ if (transport) {
4543
+ await transport.handleRequest(req, res);
4544
+ } else {
4545
+ res.status(400).json({ error: "Invalid session" });
4546
+ }
4547
+ });
4548
+ app.delete("/mcp", async (req, res) => {
4549
+ const sessionId = req.headers["mcp-session-id"];
4550
+ const transport = transports[sessionId];
4551
+ if (transport) {
4552
+ await transport.handleRequest(req, res);
4553
+ } else {
4554
+ res.status(400).json({ error: "Invalid session" });
4555
+ }
4556
+ });
4557
+ app.listen(config.port, config.host, () => {
4558
+ console.error(`[MCP] Datadog MCP server running on http://${config.host}:${config.port}/mcp`);
4559
+ console.error(`[MCP] Health check available at http://${config.host}:${config.port}/health`);
4560
+ });
4561
+ }
4562
+
4563
+ // src/index.ts
4564
+ async function main() {
4565
+ try {
4566
+ const config = loadConfig();
4567
+ const server = createServer(config);
4568
+ if (config.server.transport === "http") {
4569
+ await connectHttp(server, config.server);
4570
+ } else {
4571
+ await connectStdio(server);
4572
+ }
4573
+ } catch (error) {
4574
+ console.error("[MCP] Failed to start server:", error);
4575
+ process.exit(1);
4576
+ }
4577
+ }
4578
+ main();
4579
+ //# sourceMappingURL=index.js.map