datadog-mcp 5.5.0 → 5.7.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 CHANGED
@@ -292550,7 +292550,12 @@ var configSchema = external_exports.object({
292550
292550
  // Fallback when AI doesn't specify log limit
292551
292551
  defaultMetricDataPoints: external_exports.number().default(1e3),
292552
292552
  // Fallback for timeseries data points
292553
- defaultTimeRangeHours: external_exports.number().default(24)
292553
+ defaultTimeRangeHours: external_exports.number().default(24),
292554
+ // Maximum events scanned by the `events.histogram` action before returning a
292555
+ // partial result with `bucketCountIncomplete: true` and `nextCursor`.
292556
+ // Bounds latency on bucketed queries while still giving callers a path to
292557
+ // continue scanning if they want a fully complete histogram.
292558
+ maxEventsForHistogram: external_exports.number().default(5e3)
292554
292559
  }).default({}),
292555
292560
  features: external_exports.object({
292556
292561
  readOnly: external_exports.boolean().default(false),
@@ -292625,7 +292630,8 @@ function loadConfig() {
292625
292630
  defaultLimit: Number.parseInt(process.env.MCP_DEFAULT_LIMIT ?? "50", 10),
292626
292631
  defaultLogLines: Number.parseInt(process.env.MCP_DEFAULT_LOG_LINES ?? "200", 10),
292627
292632
  defaultMetricDataPoints: Number.parseInt(process.env.MCP_DEFAULT_METRIC_POINTS ?? "1000", 10),
292628
- defaultTimeRangeHours: Number.parseInt(process.env.MCP_DEFAULT_TIME_RANGE ?? "24", 10)
292633
+ defaultTimeRangeHours: Number.parseInt(process.env.MCP_DEFAULT_TIME_RANGE ?? "24", 10),
292634
+ maxEventsForHistogram: Number.parseInt(process.env.MCP_MAX_EVENTS_HISTOGRAM ?? "5000", 10)
292629
292635
  },
292630
292636
  features: {
292631
292637
  readOnly: args.booleans.has("read-only") || process.env.MCP_READ_ONLY === "true",
@@ -302904,6 +302910,182 @@ function formatDurationNs(ns) {
302904
302910
  return `${(ns / 6e10).toFixed(2)}m`;
302905
302911
  }
302906
302912
 
302913
+ // src/utils/timezone.ts
302914
+ var ERROR_PREFIX = "EINVALID_TIMEZONE";
302915
+ var WEEKDAY_TO_INDEX = {
302916
+ Sunday: 0,
302917
+ Monday: 1,
302918
+ Tuesday: 2,
302919
+ Wednesday: 3,
302920
+ Thursday: 4,
302921
+ Friday: 5,
302922
+ Saturday: 6
302923
+ };
302924
+ function validateIanaZone(tz) {
302925
+ if (typeof tz !== "string" || tz.length === 0) {
302926
+ throw new Error(
302927
+ `${ERROR_PREFIX}: timezone must be a non-empty IANA identifier (e.g. "UTC", "Europe/Paris", "America/New_York")`
302928
+ );
302929
+ }
302930
+ try {
302931
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
302932
+ return;
302933
+ } catch {
302934
+ const suggestions = suggestZones(tz);
302935
+ const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
302936
+ throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.${suggestionText}`);
302937
+ }
302938
+ }
302939
+ function suggestZones(input) {
302940
+ let zones;
302941
+ try {
302942
+ zones = Intl.supportedValuesOf("timeZone");
302943
+ } catch {
302944
+ return [];
302945
+ }
302946
+ const lower = input.toLowerCase();
302947
+ const scored = zones.map((zone) => ({
302948
+ zone,
302949
+ score: scoreZone(zone, lower)
302950
+ }));
302951
+ scored.sort((a, b) => a.score - b.score);
302952
+ return scored.slice(0, 3).map((s) => s.zone);
302953
+ }
302954
+ function scoreZone(zone, lowerInput) {
302955
+ const lowerZone = zone.toLowerCase();
302956
+ const baseDistance = levenshtein(lowerZone, lowerInput);
302957
+ if (lowerZone.includes(lowerInput) || lowerInput.includes(lowerZone)) {
302958
+ return baseDistance - 100;
302959
+ }
302960
+ return baseDistance;
302961
+ }
302962
+ function levenshtein(a, b) {
302963
+ if (a.length === 0) return b.length;
302964
+ if (b.length === 0) return a.length;
302965
+ if (a.length > b.length) {
302966
+ const tmp = a;
302967
+ a = b;
302968
+ b = tmp;
302969
+ }
302970
+ let previous = new Array(a.length + 1);
302971
+ let current = new Array(a.length + 1);
302972
+ for (let i = 0; i <= a.length; i++) previous[i] = i;
302973
+ for (let j = 1; j <= b.length; j++) {
302974
+ current[0] = j;
302975
+ const bChar = b.charCodeAt(j - 1);
302976
+ for (let i = 1; i <= a.length; i++) {
302977
+ const cost = a.charCodeAt(i - 1) === bChar ? 0 : 1;
302978
+ const prevI = previous[i] ?? 0;
302979
+ const currIMinus1 = current[i - 1] ?? 0;
302980
+ const prevIMinus1 = previous[i - 1] ?? 0;
302981
+ current[i] = Math.min(prevI + 1, currIMinus1 + 1, prevIMinus1 + cost);
302982
+ }
302983
+ const tmp = previous;
302984
+ previous = current;
302985
+ current = tmp;
302986
+ }
302987
+ return previous[a.length] ?? 0;
302988
+ }
302989
+ var partsFormatterCache = /* @__PURE__ */ new Map();
302990
+ var offsetFormatterCache = /* @__PURE__ */ new Map();
302991
+ function getPartsFormatter(tz) {
302992
+ const cached2 = partsFormatterCache.get(tz);
302993
+ if (cached2) return cached2;
302994
+ const fmt = new Intl.DateTimeFormat("en-US", {
302995
+ timeZone: tz,
302996
+ hourCycle: "h23",
302997
+ // 0-23, avoids "24" edge case at midnight
302998
+ year: "numeric",
302999
+ month: "2-digit",
303000
+ day: "2-digit",
303001
+ hour: "2-digit",
303002
+ minute: "2-digit",
303003
+ second: "2-digit",
303004
+ weekday: "long"
303005
+ });
303006
+ partsFormatterCache.set(tz, fmt);
303007
+ return fmt;
303008
+ }
303009
+ function getOffsetFormatter(tz) {
303010
+ const cached2 = offsetFormatterCache.get(tz);
303011
+ if (cached2) return cached2;
303012
+ const fmt = new Intl.DateTimeFormat("en-US", {
303013
+ timeZone: tz,
303014
+ timeZoneName: "longOffset"
303015
+ });
303016
+ offsetFormatterCache.set(tz, fmt);
303017
+ return fmt;
303018
+ }
303019
+ function getDateParts(epochMs, tz) {
303020
+ let parts;
303021
+ try {
303022
+ parts = getPartsFormatter(tz).formatToParts(epochMs);
303023
+ } catch {
303024
+ throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.`);
303025
+ }
303026
+ const out = {
303027
+ year: "",
303028
+ month: "",
303029
+ day: "",
303030
+ hour: "",
303031
+ minute: "",
303032
+ second: "",
303033
+ weekday: ""
303034
+ };
303035
+ for (const part of parts) {
303036
+ if (part.type === "year") out.year = part.value;
303037
+ else if (part.type === "month") out.month = part.value;
303038
+ else if (part.type === "day") out.day = part.value;
303039
+ else if (part.type === "hour") out.hour = part.value;
303040
+ else if (part.type === "minute") out.minute = part.value;
303041
+ else if (part.type === "second") out.second = part.value;
303042
+ else if (part.type === "weekday") out.weekday = part.value;
303043
+ }
303044
+ return out;
303045
+ }
303046
+ function getOffsetIso(epochMs, tz) {
303047
+ let parts;
303048
+ try {
303049
+ parts = getOffsetFormatter(tz).formatToParts(epochMs);
303050
+ } catch {
303051
+ throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.`);
303052
+ }
303053
+ const offsetPart = parts.find((p) => p.type === "timeZoneName");
303054
+ const raw = offsetPart?.value ?? "";
303055
+ if (raw === "GMT" || raw === "GMT+00:00" || raw === "GMT-00:00") {
303056
+ return "Z";
303057
+ }
303058
+ if (raw.startsWith("GMT")) {
303059
+ return raw.slice(3);
303060
+ }
303061
+ return raw;
303062
+ }
303063
+ function formatLocal(epochMs, tz) {
303064
+ validateIanaZone(tz);
303065
+ const parts = getDateParts(epochMs, tz);
303066
+ const offset = getOffsetIso(epochMs, tz);
303067
+ return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;
303068
+ }
303069
+ function bucketHourOfDay(epochMs, tz) {
303070
+ validateIanaZone(tz);
303071
+ const parts = getDateParts(epochMs, tz);
303072
+ return Number(parts.hour);
303073
+ }
303074
+ function bucketDayOfWeek(epochMs, tz) {
303075
+ validateIanaZone(tz);
303076
+ const parts = getDateParts(epochMs, tz);
303077
+ const index = WEEKDAY_TO_INDEX[parts.weekday];
303078
+ if (index === void 0) {
303079
+ throw new Error(`${ERROR_PREFIX}: could not resolve weekday for "${parts.weekday}" in "${tz}"`);
303080
+ }
303081
+ return index;
303082
+ }
303083
+ function bucketDayOfMonth(epochMs, tz) {
303084
+ validateIanaZone(tz);
303085
+ const parts = getDateParts(epochMs, tz);
303086
+ return Number(parts.day);
303087
+ }
303088
+
302907
303089
  // src/tools/events.ts
302908
303090
  var ActionSchema = external_exports.enum([
302909
303091
  "list",
@@ -302914,8 +303096,10 @@ var ActionSchema = external_exports.enum([
302914
303096
  "top",
302915
303097
  "timeseries",
302916
303098
  "incidents",
302917
- "discover"
303099
+ "discover",
303100
+ "histogram"
302918
303101
  ]);
303102
+ var HistogramBucketBySchema = external_exports.enum(["hour_of_day", "day_of_week", "day_of_month"]);
302919
303103
  var InputSchema = {
302920
303104
  action: ActionSchema.describe("Action to perform"),
302921
303105
  id: external_exports.string().optional().describe("Event ID (for get action)"),
@@ -302945,8 +303129,35 @@ var InputSchema = {
302945
303129
  ),
302946
303130
  maxEvents: external_exports.number().min(1).max(5e3).optional().describe(
302947
303131
  "Maximum events to fetch for grouping in top action (default: 5000, max: 5000). Higher = more accurate but slower"
303132
+ ),
303133
+ // Monitor transition filter (additive — see requirement 5.2 / monitors action=history)
303134
+ transitionType: external_exports.array(
303135
+ external_exports.enum([
303136
+ "alert",
303137
+ "alert recovery",
303138
+ "warning",
303139
+ "warning recovery",
303140
+ "no data",
303141
+ "no data recovery",
303142
+ "renotify"
303143
+ ])
303144
+ ).optional().describe(
303145
+ 'Filter events by monitor state transition type. When set, restricts results to events with @monitor.transition.transition_type matching any value. Use ["alert","alert recovery"] to count real fires/recoveries and skip renotifies. Empty array is treated as undefined (no filter). For a fires-only count by monitor ID, prefer monitors action=history.'
303146
+ ),
303147
+ // Histogram action (Requirement 3): bucket events by local hour/day-of-week/day-of-month.
303148
+ bucket_by: HistogramBucketBySchema.optional().describe(
303149
+ "Bucket dimension for histogram action: hour_of_day (0-23), day_of_week (0=Sun..6=Sat), day_of_month (1-31)."
303150
+ ),
303151
+ timezone: external_exports.string().optional().describe(
303152
+ 'Optional IANA timezone (e.g. "UTC", "Europe/Paris"). DST-safe. For histogram: controls hour/day bucketing (default: UTC). For search/aggregate/top/incidents read actions: adds sibling *Local ISO 8601 strings (e.g. timestampLocal) next to existing timestamps. Omit for byte-identical legacy shape.'
302948
303153
  )
302949
303154
  };
303155
+ function annotateEventTimezone(event, tz) {
303156
+ if (!event.timestamp) return event;
303157
+ const ms = new Date(event.timestamp).getTime();
303158
+ if (!Number.isFinite(ms)) return event;
303159
+ return { ...event, timestampLocal: formatLocal(ms, tz) };
303160
+ }
302950
303161
  function extractMonitorInfo(title) {
302951
303162
  const priorityMatch = title.match(/^\[P(\d+)\]\s*/);
302952
303163
  const priority = priorityMatch ? `P${priorityMatch[1]}` : void 0;
@@ -303133,17 +303344,17 @@ async function listEventsV1(api, params, limits) {
303133
303344
  tags: params.tags?.join(","),
303134
303345
  unaggregated: true
303135
303346
  });
303136
- let events = response.events ?? [];
303347
+ let events2 = response.events ?? [];
303137
303348
  if (params.query) {
303138
303349
  const lowerQuery = params.query.toLowerCase();
303139
- events = events.filter(
303350
+ events2 = events2.filter(
303140
303351
  (e) => e.title?.toLowerCase().includes(lowerQuery) || e.text?.toLowerCase().includes(lowerQuery)
303141
303352
  );
303142
303353
  }
303143
- const result = events.slice(0, effectiveLimit).map(formatEventV1);
303354
+ const result = events2.slice(0, effectiveLimit).map(formatEventV1);
303144
303355
  return {
303145
303356
  events: result,
303146
- total: events.length
303357
+ total: events2.length
303147
303358
  };
303148
303359
  }
303149
303360
  async function getEventV1(api, id) {
@@ -303172,6 +303383,88 @@ async function createEventV1(api, params) {
303172
303383
  }
303173
303384
  };
303174
303385
  }
303386
+ var UNINDEXED_ALERT_TAG_PREFIXES = [
303387
+ "monitor_priority",
303388
+ "notification_preset",
303389
+ "monitor_tags",
303390
+ "alert_cycle_key",
303391
+ "monitor_group_key",
303392
+ "notification_method"
303393
+ ];
303394
+ var NARROW_TIME_RANGE_THRESHOLD_MS = 5 * 60 * 1e3;
303395
+ function extractTagPrefixes(query, tags) {
303396
+ const prefixes = [];
303397
+ if (query) {
303398
+ const re = /(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*):[^\s)]+/g;
303399
+ let match;
303400
+ while ((match = re.exec(query)) !== null) {
303401
+ if (match[1]) {
303402
+ prefixes.push(match[1]);
303403
+ }
303404
+ }
303405
+ }
303406
+ if (tags) {
303407
+ for (const tag of tags) {
303408
+ const colonIdx = tag.indexOf(":");
303409
+ if (colonIdx > 0) {
303410
+ prefixes.push(tag.slice(0, colonIdx));
303411
+ }
303412
+ }
303413
+ }
303414
+ return prefixes;
303415
+ }
303416
+ function countNonSourceTerms(query) {
303417
+ if (!query) return 0;
303418
+ const tokens = query.split(/\s+/).filter((t) => t.length > 0 && t !== "OR" && t !== "AND");
303419
+ let nonSource = 0;
303420
+ for (const token of tokens) {
303421
+ const stripped = token.replace(/[()]/g, "");
303422
+ if (stripped.length === 0) continue;
303423
+ if (stripped.startsWith("source:")) continue;
303424
+ nonSource++;
303425
+ }
303426
+ return nonSource;
303427
+ }
303428
+ function computeDiagnostics(input) {
303429
+ const diagnostics = [];
303430
+ const query = input.query ?? "";
303431
+ const queryHasSourceAlert = /(^|\s|\()source:alert(\s|\)|$)/.test(query) || // NOSONAR S5852: anchored alternation, bounded input, no nested quantifiers
303432
+ (input.sources?.includes("alert") ?? false) || (input.tags?.includes("source:alert") ?? false);
303433
+ if (queryHasSourceAlert) {
303434
+ const prefixes = extractTagPrefixes(input.query, input.tags);
303435
+ const unindexedHits = prefixes.filter((p) => UNINDEXED_ALERT_TAG_PREFIXES.includes(p));
303436
+ const uniqueHits = Array.from(new Set(unindexedHits));
303437
+ if (uniqueHits.length > 0) {
303438
+ diagnostics.push({
303439
+ code: "UNINDEXED_TAG_PREFIX",
303440
+ message: `Query filters on tag prefix(es) that Datadog does not index for source:alert events: ${uniqueHits.join(", ")}.`,
303441
+ hint: "Drop these filters and post-filter the results client-side, or aggregate via monitors/get + monitors.list with matching options."
303442
+ });
303443
+ }
303444
+ }
303445
+ if (typeof input.fromMs === "number" && typeof input.toMs === "number" && input.toMs > input.fromMs && input.toMs - input.fromMs < NARROW_TIME_RANGE_THRESHOLD_MS) {
303446
+ diagnostics.push({
303447
+ code: "NARROW_TIME_RANGE",
303448
+ message: "Time range is shorter than 5 minutes; alert events may not have been indexed yet.",
303449
+ hint: "Widen the range (e.g. last 1h) or retry after the indexing delay (~30s) has elapsed."
303450
+ });
303451
+ }
303452
+ if (queryHasSourceAlert) {
303453
+ const otherTerms = countNonSourceTerms(input.query);
303454
+ const otherTags = (input.tags ?? []).filter((t) => !t.startsWith("source:")).length;
303455
+ if (otherTerms === 0 && otherTags === 0) {
303456
+ diagnostics.push({
303457
+ code: "RESTRICTIVE_SOURCE_FILTER",
303458
+ message: "Only source:alert filter was applied; the matching event set may genuinely be empty.",
303459
+ hint: "Use events.aggregate or monitors.list to confirm no alerts fired in the window, or broaden sources (e.g. source:monitor, source:audit)."
303460
+ });
303461
+ }
303462
+ }
303463
+ return diagnostics;
303464
+ }
303465
+ function quoteIfNeeded(value) {
303466
+ return /^[A-Za-z0-9_.-]+$/.test(value) ? value : `"${value}"`;
303467
+ }
303175
303468
  function buildEventQuery(params) {
303176
303469
  const parts = [];
303177
303470
  if (params.query) {
@@ -303189,9 +303482,16 @@ function buildEventQuery(params) {
303189
303482
  if (params.priority) {
303190
303483
  parts.push(`priority:${params.priority}`);
303191
303484
  }
303485
+ if (params.transitionType && params.transitionType.length > 0) {
303486
+ const inner = params.transitionType.map(quoteIfNeeded).join(" OR ");
303487
+ parts.push(`@monitor.transition.transition_type:(${inner})`);
303488
+ }
303192
303489
  return parts.length > 0 ? parts.join(" ") : "*";
303193
303490
  }
303194
303491
  async function searchEventsV2(api, params, limits, site) {
303492
+ if (params.timezone !== void 0) {
303493
+ validateIanaZone(params.timezone);
303494
+ }
303195
303495
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303196
303496
  const defaultTo = now();
303197
303497
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303204,7 +303504,8 @@ async function searchEventsV2(api, params, limits, site) {
303204
303504
  query: params.query,
303205
303505
  sources: params.sources,
303206
303506
  tags: params.tags,
303207
- priority: params.priority
303507
+ priority: params.priority,
303508
+ transitionType: params.transitionType
303208
303509
  });
303209
303510
  const effectiveLimit = params.limit ?? limits.defaultLimit;
303210
303511
  const body = {
@@ -303220,12 +303521,13 @@ async function searchEventsV2(api, params, limits, site) {
303220
303521
  }
303221
303522
  };
303222
303523
  const response = await api.searchEvents({ body });
303223
- const events = (response.data ?? []).map(formatEventV2);
303524
+ const rawEvents = (response.data ?? []).map(formatEventV2);
303525
+ const events2 = params.timezone !== void 0 ? rawEvents.map((e) => annotateEventTimezone(e, params.timezone)) : rawEvents;
303224
303526
  const nextCursor = response.meta?.page?.after;
303225
- return {
303226
- events,
303527
+ const baseResult = {
303528
+ events: events2,
303227
303529
  meta: {
303228
- count: events.length,
303530
+ count: events2.length,
303229
303531
  query: fullQuery,
303230
303532
  from: fromTime,
303231
303533
  to: toTime,
@@ -303233,8 +303535,131 @@ async function searchEventsV2(api, params, limits, site) {
303233
303535
  datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
303234
303536
  }
303235
303537
  };
303538
+ if (events2.length === 0) {
303539
+ const diagnostics = computeDiagnostics({
303540
+ query: params.query,
303541
+ tags: params.tags,
303542
+ sources: params.sources,
303543
+ fromMs: validFrom * 1e3,
303544
+ toMs: validTo * 1e3
303545
+ });
303546
+ return { ...baseResult, diagnostics };
303547
+ }
303548
+ return baseResult;
303549
+ }
303550
+ function bucketEvent(epochMs, bucketBy, tz) {
303551
+ switch (bucketBy) {
303552
+ case "hour_of_day":
303553
+ return bucketHourOfDay(epochMs, tz);
303554
+ case "day_of_week":
303555
+ return bucketDayOfWeek(epochMs, tz);
303556
+ case "day_of_month":
303557
+ return bucketDayOfMonth(epochMs, tz);
303558
+ default: {
303559
+ const exhaustive = bucketBy;
303560
+ throw new Error(`Unhandled bucket_by: ${String(exhaustive)}`);
303561
+ }
303562
+ }
303563
+ }
303564
+ function eventEpochMs(event) {
303565
+ const ts = event.attributes?.timestamp;
303566
+ if (ts === void 0 || ts === null) return null;
303567
+ if (ts instanceof Date) {
303568
+ const ms2 = ts.getTime();
303569
+ return Number.isFinite(ms2) ? ms2 : null;
303570
+ }
303571
+ const ms = new Date(String(ts)).getTime();
303572
+ return Number.isFinite(ms) ? ms : null;
303573
+ }
303574
+ async function histogramEventsV2(api, params, limits, site) {
303575
+ const timezone = params.timezone ?? "UTC";
303576
+ validateIanaZone(timezone);
303577
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303578
+ const defaultTo = now();
303579
+ const [validFrom, validTo] = ensureValidTimeRange(
303580
+ parseTime(params.from, defaultFrom),
303581
+ parseTime(params.to, defaultTo)
303582
+ );
303583
+ const fromTime = new Date(validFrom * 1e3).toISOString();
303584
+ const toTime = new Date(validTo * 1e3).toISOString();
303585
+ const fullQuery = buildEventQuery({
303586
+ query: params.query,
303587
+ sources: params.sources,
303588
+ tags: params.tags
303589
+ });
303590
+ const cap = limits.maxEventsForHistogram;
303591
+ const perPage = Math.max(1, Math.min(1e3, cap));
303592
+ const buckets = {};
303593
+ let totalEvents = 0;
303594
+ let cursor = params.cursor;
303595
+ let bucketCountIncomplete = false;
303596
+ let exhaustedPages = false;
303597
+ const maxPages = 100;
303598
+ let pageCount = 0;
303599
+ while (pageCount < maxPages) {
303600
+ const body = {
303601
+ filter: {
303602
+ query: fullQuery,
303603
+ from: fromTime,
303604
+ to: toTime
303605
+ },
303606
+ sort: "timestamp",
303607
+ page: {
303608
+ limit: perPage,
303609
+ cursor
303610
+ }
303611
+ };
303612
+ const response = await api.searchEvents({ body });
303613
+ const data = response.data ?? [];
303614
+ const responseCursor = response.meta?.page?.after ?? void 0;
303615
+ for (const event of data) {
303616
+ const epochMs = eventEpochMs(event);
303617
+ if (epochMs === null) continue;
303618
+ const bucket = bucketEvent(epochMs, params.bucket_by, timezone);
303619
+ const key = String(bucket);
303620
+ buckets[key] = (buckets[key] ?? 0) + 1;
303621
+ totalEvents++;
303622
+ if (totalEvents >= cap) {
303623
+ bucketCountIncomplete = true;
303624
+ cursor = responseCursor;
303625
+ break;
303626
+ }
303627
+ }
303628
+ if (bucketCountIncomplete) break;
303629
+ if (data.length === 0 || !responseCursor) {
303630
+ exhaustedPages = true;
303631
+ break;
303632
+ }
303633
+ cursor = responseCursor;
303634
+ pageCount++;
303635
+ }
303636
+ const result = {
303637
+ buckets,
303638
+ bucketBy: params.bucket_by,
303639
+ timezone,
303640
+ totalEvents,
303641
+ meta: {
303642
+ query: fullQuery,
303643
+ from: fromTime,
303644
+ to: toTime,
303645
+ datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
303646
+ }
303647
+ };
303648
+ if (bucketCountIncomplete) {
303649
+ result.bucketCountIncomplete = true;
303650
+ if (cursor) {
303651
+ result.nextCursor = cursor;
303652
+ }
303653
+ } else if (!exhaustedPages && pageCount >= maxPages && cursor) {
303654
+ result.bucketCountIncomplete = true;
303655
+ result.nextCursor = cursor;
303656
+ }
303657
+ return result;
303236
303658
  }
303237
303659
  async function aggregateEventsV2(api, params, limits, site) {
303660
+ if (params.timezone !== void 0) {
303661
+ validateIanaZone(params.timezone);
303662
+ }
303238
303663
  const counts = /* @__PURE__ */ new Map();
303239
303664
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303240
303665
  const defaultTo = now();
@@ -303247,7 +303672,8 @@ async function aggregateEventsV2(api, params, limits, site) {
303247
303672
  const fullQuery = buildEventQuery({
303248
303673
  query: params.query,
303249
303674
  sources: params.sources,
303250
- tags: params.tags
303675
+ tags: params.tags,
303676
+ transitionType: params.transitionType
303251
303677
  });
303252
303678
  const groupByFields = params.groupBy ?? ["monitor_name"];
303253
303679
  const maxEventsToAggregate = 1e4;
@@ -303270,9 +303696,9 @@ async function aggregateEventsV2(api, params, limits, site) {
303270
303696
  while (pageCount < maxPages && eventCount < maxEventsToAggregate) {
303271
303697
  const pageBody = { ...body, page: { ...body.page, cursor } };
303272
303698
  const response = await api.searchEvents({ body: pageBody });
303273
- const events = response.data ?? [];
303274
- if (events.length === 0) break;
303275
- for (const event of events) {
303699
+ const events2 = response.data ?? [];
303700
+ if (events2.length === 0) break;
303701
+ for (const event of events2) {
303276
303702
  const formatted = formatEventV2(event);
303277
303703
  const groupKey = buildGroupKey(formatted, groupByFields);
303278
303704
  const existing = counts.get(groupKey);
@@ -303293,7 +303719,8 @@ async function aggregateEventsV2(api, params, limits, site) {
303293
303719
  const buckets = sorted.map(([key, data]) => ({
303294
303720
  key,
303295
303721
  count: data.count,
303296
- sample: data.sample
303722
+ // Requirement 4: annotate sample timestamps only when timezone is supplied.
303723
+ sample: params.timezone !== void 0 ? annotateEventTimezone(data.sample, params.timezone) : data.sample
303297
303724
  }));
303298
303725
  return {
303299
303726
  buckets,
@@ -303310,6 +303737,9 @@ async function aggregateEventsV2(api, params, limits, site) {
303310
303737
  };
303311
303738
  }
303312
303739
  async function topEventsV2(api, params, limits, site) {
303740
+ if (params.timezone !== void 0) {
303741
+ validateIanaZone(params.timezone);
303742
+ }
303313
303743
  if (params.contextTags !== void 0) {
303314
303744
  if (!Array.isArray(params.contextTags)) {
303315
303745
  throw new Error("contextTags must be an array");
@@ -303329,7 +303759,8 @@ async function topEventsV2(api, params, limits, site) {
303329
303759
  to: params.to,
303330
303760
  sources: params.sources,
303331
303761
  tags: effectiveTags,
303332
- limit: params.maxEvents ?? 5e3
303762
+ limit: params.maxEvents ?? 5e3,
303763
+ transitionType: params.transitionType
303333
303764
  },
303334
303765
  limits,
303335
303766
  site
@@ -303346,7 +303777,7 @@ async function topEventsV2(api, params, limits, site) {
303346
303777
  value = event.monitorInfo?.name ?? event.title;
303347
303778
  } else {
303348
303779
  const tag = event.tags.find((t) => t.startsWith(`${field}:`));
303349
- value = tag ? tag.split(":", 2)[1] : "unknown";
303780
+ value = tag ? tag.split(":", 2)[1] ?? "unknown" : "unknown";
303350
303781
  }
303351
303782
  groupValues[field] = value;
303352
303783
  keyParts.push(`${field}:${value}`);
@@ -303407,6 +303838,9 @@ function parseIntervalToMs(interval) {
303407
303838
  return ns ? Math.floor(ns / 1e6) : 36e5;
303408
303839
  }
303409
303840
  async function timeseriesEventsV2(api, params, limits, site) {
303841
+ if (params.timezone !== void 0) {
303842
+ validateIanaZone(params.timezone);
303843
+ }
303410
303844
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303411
303845
  const defaultTo = now();
303412
303846
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303418,7 +303852,8 @@ async function timeseriesEventsV2(api, params, limits, site) {
303418
303852
  const fullQuery = buildEventQuery({
303419
303853
  query: params.query ?? "source:alert",
303420
303854
  sources: params.sources,
303421
- tags: params.tags
303855
+ tags: params.tags,
303856
+ transitionType: params.transitionType
303422
303857
  });
303423
303858
  const intervalMs = parseIntervalToMs(params.interval);
303424
303859
  const groupByFields = params.groupBy ?? ["monitor_name"];
@@ -303440,9 +303875,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
303440
303875
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
303441
303876
  const pageBody = { ...body, page: { ...body.page, cursor } };
303442
303877
  const response = await api.searchEvents({ body: pageBody });
303443
- const events = response.data ?? [];
303444
- if (events.length === 0) break;
303445
- for (const event of events) {
303878
+ const events2 = response.data ?? [];
303879
+ if (events2.length === 0) break;
303880
+ for (const event of events2) {
303446
303881
  const formatted = formatEventV2(event);
303447
303882
  const groupKey = buildGroupKey(formatted, groupByFields);
303448
303883
  const eventTs = new Date(formatted.timestamp).getTime();
@@ -303459,6 +303894,7 @@ async function timeseriesEventsV2(api, params, limits, site) {
303459
303894
  if (!cursor) break;
303460
303895
  pageCount++;
303461
303896
  }
303897
+ const tz = params.timezone;
303462
303898
  const sortedBuckets = [...timeBuckets.entries()].sort((a, b) => a[0] - b[0]).map(([bucketTs, groupCounts]) => {
303463
303899
  const counts = {};
303464
303900
  let total = 0;
@@ -303466,12 +303902,16 @@ async function timeseriesEventsV2(api, params, limits, site) {
303466
303902
  counts[key] = count;
303467
303903
  total += count;
303468
303904
  }
303469
- return {
303905
+ const bucket = {
303470
303906
  timestamp: new Date(bucketTs).toISOString(),
303471
303907
  timestampMs: bucketTs,
303472
303908
  counts,
303473
303909
  total
303474
303910
  };
303911
+ if (tz !== void 0) {
303912
+ bucket.timestampLocal = formatLocal(bucketTs, tz);
303913
+ }
303914
+ return bucket;
303475
303915
  });
303476
303916
  const effectiveLimit = params.limit ?? 100;
303477
303917
  const limitedBuckets = sortedBuckets.slice(0, effectiveLimit);
@@ -303492,6 +303932,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
303492
303932
  };
303493
303933
  }
303494
303934
  async function incidentsEventsV2(api, params, limits, site) {
303935
+ if (params.timezone !== void 0) {
303936
+ validateIanaZone(params.timezone);
303937
+ }
303495
303938
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303496
303939
  const defaultTo = now();
303497
303940
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303503,7 +303946,8 @@ async function incidentsEventsV2(api, params, limits, site) {
303503
303946
  const fullQuery = buildEventQuery({
303504
303947
  query: params.query ?? "source:alert",
303505
303948
  sources: params.sources,
303506
- tags: params.tags
303949
+ tags: params.tags,
303950
+ transitionType: params.transitionType
303507
303951
  });
303508
303952
  const dedupeWindowNs = parseDurationToNs(params.dedupeWindow ?? "5m");
303509
303953
  const dedupeWindowMs = dedupeWindowNs ? Math.floor(dedupeWindowNs / 1e6) : 3e5;
@@ -303525,9 +303969,9 @@ async function incidentsEventsV2(api, params, limits, site) {
303525
303969
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
303526
303970
  const pageBody = { ...body, page: { ...body.page, cursor } };
303527
303971
  const response = await api.searchEvents({ body: pageBody });
303528
- const events = response.data ?? [];
303529
- if (events.length === 0) break;
303530
- for (const event of events) {
303972
+ const events2 = response.data ?? [];
303973
+ if (events2.length === 0) break;
303974
+ for (const event of events2) {
303531
303975
  const formatted = formatEventV2(event);
303532
303976
  const monitorName = formatted.monitorInfo?.name ?? formatted.title;
303533
303977
  if (!monitorName) {
@@ -303595,6 +304039,7 @@ async function incidentsEventsV2(api, params, limits, site) {
303595
304039
  if (!cursor) break;
303596
304040
  pageCount++;
303597
304041
  }
304042
+ const tz = params.timezone;
303598
304043
  const incidentList = [...incidents.values()].map((inc) => {
303599
304044
  let duration3;
303600
304045
  if (inc.recoveredAt) {
@@ -303607,7 +304052,7 @@ async function incidentsEventsV2(api, params, limits, site) {
303607
304052
  duration3 = `${(durationMs / 36e5).toFixed(1)}h`;
303608
304053
  }
303609
304054
  }
303610
- return {
304055
+ const base = {
303611
304056
  monitorName: inc.monitorName,
303612
304057
  firstTrigger: inc.firstTrigger.toISOString(),
303613
304058
  lastTrigger: inc.lastTrigger.toISOString(),
@@ -303615,8 +304060,17 @@ async function incidentsEventsV2(api, params, limits, site) {
303615
304060
  recovered: inc.recovered,
303616
304061
  recoveredAt: inc.recoveredAt?.toISOString(),
303617
304062
  duration: duration3,
303618
- sample: inc.sample
304063
+ // Requirement 4: annotate the nested sample event timestamp when tz is supplied.
304064
+ sample: tz !== void 0 ? annotateEventTimezone(inc.sample, tz) : inc.sample
303619
304065
  };
304066
+ if (tz !== void 0) {
304067
+ base.firstTriggerLocal = formatLocal(inc.firstTrigger.getTime(), tz);
304068
+ base.lastTriggerLocal = formatLocal(inc.lastTrigger.getTime(), tz);
304069
+ if (inc.recoveredAt) {
304070
+ base.recoveredAtLocal = formatLocal(inc.recoveredAt.getTime(), tz);
304071
+ }
304072
+ }
304073
+ return base;
303620
304074
  });
303621
304075
  incidentList.sort(
303622
304076
  (a, b) => new Date(b.firstTrigger).getTime() - new Date(a.firstTrigger).getTime()
@@ -303639,15 +304093,15 @@ async function incidentsEventsV2(api, params, limits, site) {
303639
304093
  }
303640
304094
  };
303641
304095
  }
303642
- async function enrichWithMonitorMetadata(events, monitorsApi) {
304096
+ async function enrichWithMonitorMetadata(events2, monitorsApi) {
303643
304097
  const monitorIds = /* @__PURE__ */ new Set();
303644
- for (const event of events) {
304098
+ for (const event of events2) {
303645
304099
  if (event.monitorId) {
303646
304100
  monitorIds.add(event.monitorId);
303647
304101
  }
303648
304102
  }
303649
304103
  if (monitorIds.size === 0) {
303650
- return events;
304104
+ return events2;
303651
304105
  }
303652
304106
  const monitorCache = /* @__PURE__ */ new Map();
303653
304107
  try {
@@ -303661,9 +304115,9 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
303661
304115
  }
303662
304116
  }
303663
304117
  } catch {
303664
- return events;
304118
+ return events2;
303665
304119
  }
303666
- return events.map((event) => {
304120
+ return events2.map((event) => {
303667
304121
  const enriched = { ...event };
303668
304122
  if (event.monitorId) {
303669
304123
  const monitor = monitorCache.get(event.monitorId);
@@ -303688,18 +304142,26 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
303688
304142
  function registerEventsTool(server, apiV1, apiV2, monitorsApi, limits, readOnly = false, site = "datadoghq.com") {
303689
304143
  server.tool(
303690
304144
  "events",
303691
- `Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents, discover.
304145
+ `Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents, discover, histogram.
303692
304146
  For monitor alerts, use tags: ["source:alert"].
303693
304147
 
304148
+ IMPORTANT \u2014 re-evaluation vs transition:
304149
+ - source:alert events INCLUDE renotifies and re-evaluations (every Datadog re-evaluation of an alerting monitor emits an event). A "how many times did monitor X fire" question answered with source:alert alone over-counts.
304150
+ - To restrict to real state transitions, pass transitionType (e.g. ["alert","alert recovery"]). This appends @monitor.transition.transition_type:(...) to the query and matches the design's live investigation.
304151
+ - For a fires-only numeric count rooted in a single monitor ID, prefer the higher-level primitive monitors action=history \u2014 it returns {transitions, count, meta} with the same filter applied for you.
304152
+
304153
+ transitionType: Optional array of monitor transition types (alert, alert recovery, warning, warning recovery, no data, no data recovery, renotify). Empty array is treated as undefined.
303694
304154
  top: Generic event grouping by any fields (groupBy parameter). Returns groups ranked by count with optional context breakdown.
303695
304155
  - Example: {groupBy: ["service"], message: "...", service: "api", total_count: 50, by_context: [{context: "queue:X", count: 30}]}
303696
304156
  - Use for deployments, configs, custom events, or monitor alerts
303697
304157
  - Returns "message" field (event title), NOT monitor name (use monitors tool for real names)
304158
+ - total_count includes renotifies when source:alert is used without transitionType \u2014 see monitors action=history for fires-only counts
303698
304159
  discover: Returns available tag prefixes from events.
303699
304160
  aggregate: Custom groupBy, returns pipe-delimited keys.
303700
304161
  search: Full event details.
303701
304162
  timeseries: Time-bucketed trends with interval.
303702
- incidents: Deduplicate alerts with dedupeWindow.`,
304163
+ incidents: Deduplicate alerts with dedupeWindow.
304164
+ histogram: Bucket events by local hour_of_day / day_of_week / day_of_month in the requested IANA timezone (DST-safe). Pass bucket_by (required) and optional timezone (default UTC) and cursor (for continuation). Caps at limits.maxEventsForHistogram (default 5000); when reached returns bucketCountIncomplete:true + nextCursor.`,
303703
304165
  InputSchema,
303704
304166
  async ({
303705
304167
  action,
@@ -303720,7 +304182,10 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303720
304182
  dedupeWindow,
303721
304183
  enrich,
303722
304184
  contextTags,
303723
- maxEvents
304185
+ maxEvents,
304186
+ transitionType,
304187
+ bucket_by,
304188
+ timezone
303724
304189
  }) => {
303725
304190
  try {
303726
304191
  checkReadOnly(action, readOnly);
@@ -303769,7 +304234,9 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303769
304234
  tags,
303770
304235
  priority,
303771
304236
  limit,
303772
- cursor
304237
+ cursor,
304238
+ transitionType,
304239
+ timezone
303773
304240
  },
303774
304241
  limits,
303775
304242
  site
@@ -303791,7 +304258,9 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303791
304258
  sources,
303792
304259
  tags,
303793
304260
  groupBy,
303794
- limit
304261
+ limit,
304262
+ transitionType,
304263
+ timezone
303795
304264
  },
303796
304265
  limits,
303797
304266
  site
@@ -303810,7 +304279,9 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303810
304279
  limit,
303811
304280
  groupBy,
303812
304281
  contextTags,
303813
- maxEvents
304282
+ maxEvents,
304283
+ transitionType,
304284
+ timezone
303814
304285
  },
303815
304286
  limits,
303816
304287
  site
@@ -303843,7 +304314,9 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303843
304314
  tags,
303844
304315
  groupBy,
303845
304316
  interval,
303846
- limit
304317
+ limit,
304318
+ transitionType,
304319
+ timezone
303847
304320
  },
303848
304321
  limits,
303849
304322
  site
@@ -303860,12 +304333,34 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303860
304333
  sources,
303861
304334
  tags,
303862
304335
  dedupeWindow,
303863
- limit
304336
+ limit,
304337
+ transitionType,
304338
+ timezone
303864
304339
  },
303865
304340
  limits,
303866
304341
  site
303867
304342
  )
303868
304343
  );
304344
+ case "histogram": {
304345
+ const histogramBucketBy = requireParam(bucket_by, "bucket_by", "histogram");
304346
+ return toolResult(
304347
+ await histogramEventsV2(
304348
+ apiV2,
304349
+ {
304350
+ query,
304351
+ from,
304352
+ to,
304353
+ sources,
304354
+ tags,
304355
+ bucket_by: histogramBucketBy,
304356
+ timezone,
304357
+ cursor
304358
+ },
304359
+ limits,
304360
+ site
304361
+ )
304362
+ );
304363
+ }
303869
304364
  default:
303870
304365
  throw new Error(`Unknown action: ${action}`);
303871
304366
  }
@@ -303876,6 +304371,177 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303876
304371
  );
303877
304372
  }
303878
304373
 
304374
+ // src/utils/templatePreview.ts
304375
+ var SUPPORTED_CONDITIONALS = [
304376
+ "is_alert",
304377
+ "is_warning",
304378
+ "is_no_data",
304379
+ "is_recovery",
304380
+ "is_alert_to_warning",
304381
+ "is_warning_to_alert"
304382
+ ];
304383
+ var SUPPORTED_SET = new Set(SUPPORTED_CONDITIONALS);
304384
+ function unsupportedSyntaxError(detail) {
304385
+ const supportedList = SUPPORTED_CONDITIONALS.join(", ");
304386
+ return new Error(
304387
+ `EUNSUPPORTED_TEMPLATE_SYNTAX: ${detail}. Supported syntax is {{variable.name}} and conditionals {{#name}}...{{/name}} / {{^name}}...{{/name}} where name is one of: ${supportedList}. Loops ({{#each ...}}) and partials ({{> ...}}) are not supported.`
304388
+ );
304389
+ }
304390
+ function lookupVariable(path, variables) {
304391
+ if (Object.prototype.hasOwnProperty.call(variables, path)) {
304392
+ const value = variables[path];
304393
+ return value === void 0 || value === null ? void 0 : String(value);
304394
+ }
304395
+ const segments = path.split(".");
304396
+ let cursor = variables;
304397
+ for (const segment of segments) {
304398
+ if (cursor === null || cursor === void 0) {
304399
+ return void 0;
304400
+ }
304401
+ if (typeof cursor !== "object") {
304402
+ return void 0;
304403
+ }
304404
+ const record2 = cursor;
304405
+ if (!Object.prototype.hasOwnProperty.call(record2, segment)) {
304406
+ return void 0;
304407
+ }
304408
+ cursor = record2[segment];
304409
+ }
304410
+ if (cursor === void 0 || cursor === null) {
304411
+ return void 0;
304412
+ }
304413
+ if (typeof cursor === "object") {
304414
+ return void 0;
304415
+ }
304416
+ return String(cursor);
304417
+ }
304418
+ var TAG_REGEX = /\{\{\s*([#^/>])?\s*([^}]*?)\s*\}\}/g;
304419
+ function parseBlocks(template) {
304420
+ const stack = [{ children: [] }];
304421
+ let cursor = 0;
304422
+ TAG_REGEX.lastIndex = 0;
304423
+ let match;
304424
+ while ((match = TAG_REGEX.exec(template)) !== null) {
304425
+ const [fullTag, prefix, rawName] = match;
304426
+ const tagStart = match.index;
304427
+ const name = rawName?.trim() ?? "";
304428
+ if (tagStart > cursor) {
304429
+ const top = stack[stack.length - 1];
304430
+ if (top) {
304431
+ top.children.push({ kind: "literal", text: template.slice(cursor, tagStart) });
304432
+ }
304433
+ }
304434
+ if (prefix === ">") {
304435
+ throw unsupportedSyntaxError(`partials are not supported (found {{> ${name}}})`);
304436
+ }
304437
+ if (prefix === "#" || prefix === "^") {
304438
+ if (name.startsWith("each") || /\s/.test(name)) {
304439
+ throw unsupportedSyntaxError(`loops are not supported (found {{${prefix}${name}}})`);
304440
+ }
304441
+ if (!SUPPORTED_SET.has(name)) {
304442
+ throw unsupportedSyntaxError(`unknown conditional '${name}' in {{${prefix}${name}}}`);
304443
+ }
304444
+ stack.push({
304445
+ children: [],
304446
+ closer: { conditional: name, negated: prefix === "^" }
304447
+ });
304448
+ } else if (prefix === "/") {
304449
+ const frame = stack.pop();
304450
+ if (!frame || !frame.closer) {
304451
+ throw unsupportedSyntaxError(`unmatched closing tag {{/${name}}}`);
304452
+ }
304453
+ if (frame.closer.conditional !== name) {
304454
+ throw unsupportedSyntaxError(
304455
+ `mismatched closing tag {{/${name}}} (expected {{/${frame.closer.conditional}}})`
304456
+ );
304457
+ }
304458
+ const parent = stack[stack.length - 1];
304459
+ if (!parent) {
304460
+ throw unsupportedSyntaxError("block stack underflow while closing tag");
304461
+ }
304462
+ parent.children.push({
304463
+ kind: "block",
304464
+ conditional: frame.closer.conditional,
304465
+ negated: frame.closer.negated,
304466
+ children: frame.children
304467
+ });
304468
+ } else {
304469
+ const top = stack[stack.length - 1];
304470
+ if (top) {
304471
+ top.children.push({ kind: "literal", text: fullTag });
304472
+ }
304473
+ }
304474
+ cursor = tagStart + fullTag.length;
304475
+ }
304476
+ if (cursor < template.length) {
304477
+ const top = stack[stack.length - 1];
304478
+ if (top) {
304479
+ top.children.push({ kind: "literal", text: template.slice(cursor) });
304480
+ }
304481
+ }
304482
+ if (stack.length !== 1) {
304483
+ const open = stack[stack.length - 1]?.closer?.conditional;
304484
+ throw unsupportedSyntaxError(`unclosed conditional block ${open ? `{{#${open}}}` : ""}`);
304485
+ }
304486
+ const root = stack[0];
304487
+ if (!root) {
304488
+ throw unsupportedSyntaxError("parser produced no root frame");
304489
+ }
304490
+ return root.children;
304491
+ }
304492
+ function renderBlocks(tokens, conditionals, resolved) {
304493
+ let out = "";
304494
+ for (const token of tokens) {
304495
+ if (token.kind === "literal") {
304496
+ out += token.text;
304497
+ continue;
304498
+ }
304499
+ const flag = conditionals[token.conditional] ?? false;
304500
+ resolved[token.conditional] = flag;
304501
+ const include = token.negated ? !flag : flag;
304502
+ if (include) {
304503
+ out += renderBlocks(token.children, conditionals, resolved);
304504
+ } else {
304505
+ renderBlocks(token.children, conditionals, resolved);
304506
+ }
304507
+ }
304508
+ return out;
304509
+ }
304510
+ var VARIABLE_TAG_REGEX = /\{\{\s*([^#^/>\s][^}]*?)\s*\}\}/g;
304511
+ function substituteVariables(text, variables) {
304512
+ const usedSet = /* @__PURE__ */ new Set();
304513
+ const missingSet = /* @__PURE__ */ new Set();
304514
+ const rendered = text.replace(VARIABLE_TAG_REGEX, (_match, captured) => {
304515
+ const name = captured.trim();
304516
+ const value = lookupVariable(name, variables);
304517
+ if (value === void 0) {
304518
+ missingSet.add(name);
304519
+ return `{{undefined:${name}}}`;
304520
+ }
304521
+ usedSet.add(name);
304522
+ return value;
304523
+ });
304524
+ return {
304525
+ rendered,
304526
+ used: [...usedSet],
304527
+ missing: [...missingSet]
304528
+ };
304529
+ }
304530
+ function renderMonitorTemplate(template, context) {
304531
+ const variables = context.variables ?? {};
304532
+ const conditionals = context.conditionals ?? {};
304533
+ const tree = parseBlocks(template);
304534
+ const conditionalsResolved = {};
304535
+ const afterConditionals = renderBlocks(tree, conditionals, conditionalsResolved);
304536
+ const { rendered, used, missing } = substituteVariables(afterConditionals, variables);
304537
+ return {
304538
+ rendered,
304539
+ variablesUsed: used,
304540
+ variablesMissing: missing,
304541
+ conditionalsResolved
304542
+ };
304543
+ }
304544
+
303879
304545
  // src/tools/monitors.ts
303880
304546
  var ActionSchema2 = external_exports.enum([
303881
304547
  "list",
@@ -303886,8 +304552,15 @@ var ActionSchema2 = external_exports.enum([
303886
304552
  "delete",
303887
304553
  "mute",
303888
304554
  "unmute",
303889
- "top"
304555
+ "top",
304556
+ "history",
304557
+ "preview",
304558
+ "test_notification"
303890
304559
  ]);
304560
+ var PreviewContextSchema = external_exports.object({
304561
+ variables: external_exports.record(external_exports.unknown()).optional(),
304562
+ conditionals: external_exports.record(external_exports.enum(SUPPORTED_CONDITIONALS), external_exports.boolean()).optional()
304563
+ }).optional().describe("Substitution context for monitors.preview (variables + conditionals).");
303891
304564
  var InputSchema2 = {
303892
304565
  action: ActionSchema2.describe("Action to perform"),
303893
304566
  id: external_exports.string().optional().describe("Monitor ID (required for get/update/delete/mute/unmute)"),
@@ -303899,15 +304572,44 @@ var InputSchema2 = {
303899
304572
  ),
303900
304573
  limit: external_exports.number().min(1).optional().describe("Maximum number of monitors to return (default: 50)"),
303901
304574
  config: external_exports.record(external_exports.unknown()).optional().describe("Monitor configuration (for create/update)"),
303902
- message: external_exports.string().optional().describe("Mute message (for mute action)"),
304575
+ message: external_exports.string().optional().describe(
304576
+ "Mute message (for mute action) OR inline template source for the preview action. For preview, supply either this inline string or `monitor_id` (or the existing `id` field) so the action can load the monitor message via getMonitor."
304577
+ ),
303903
304578
  end: external_exports.number().optional().describe("Mute end timestamp (for mute action)"),
304579
+ monitor_id: external_exports.number().optional().describe(
304580
+ "Numeric monitor ID used by the preview action when no inline `message` is supplied. Equivalent to passing the existing `id` field as a numeric string."
304581
+ ),
304582
+ context: PreviewContextSchema,
303904
304583
  // Top action parameters
303905
304584
  from: external_exports.string().optional().describe('Start time (ISO 8601, relative like "1h", or Unix timestamp)'),
303906
304585
  to: external_exports.string().optional().describe('End time (ISO 8601, relative like "1h", or Unix timestamp)'),
303907
304586
  contextTags: external_exports.array(external_exports.string()).optional().describe(
303908
304587
  "Tag prefixes for context breakdown in top action (default: queue, service, ingress, pod_name, kube_namespace, kube_container_name)"
303909
304588
  ),
303910
- maxEvents: external_exports.number().min(1).max(5e3).optional().describe("Maximum events to fetch for top action (default: 5000, max: 5000)")
304589
+ maxEvents: external_exports.number().min(1).max(5e3).optional().describe("Maximum events to fetch for top action (default: 5000, max: 5000)"),
304590
+ // History action parameters
304591
+ transitionType: external_exports.array(
304592
+ external_exports.enum([
304593
+ "alert",
304594
+ "alert recovery",
304595
+ "warning",
304596
+ "warning recovery",
304597
+ "no data",
304598
+ "no data recovery",
304599
+ "renotify"
304600
+ ])
304601
+ ).optional().describe(
304602
+ 'For history action: filter by monitor state transition types. Default: ["alert","alert recovery"] (real fires + recoveries, excludes renotifies). Pass ["alert"] for fires only, or include "renotify" for full chronological audit.'
304603
+ ),
304604
+ group: external_exports.string().optional().describe(
304605
+ 'For history action: filter transitions to a specific multi-alert monitor group (e.g., "pod_name:foo,kube_namespace:bar"). Optional; omit for all groups.'
304606
+ ),
304607
+ dry_run: external_exports.boolean().optional().describe(
304608
+ "When create + dry_run=true, validate the monitor body via POST /api/v1/monitor/validate without creating it. Allowed under --read-only because no monitor is created. Returns { valid, dryRun, monitor }. 400 responses surface verbatim like a failed create."
304609
+ ),
304610
+ timezone: external_exports.string().optional().describe(
304611
+ 'Optional IANA timezone (e.g. "UTC", "Europe/Paris"). When supplied on get/list, the response adds sibling createdLocal/modifiedLocal ISO 8601 strings next to created/modified. Omit for byte-identical legacy shape. Invalid zones return EINVALID_TIMEZONE.'
304612
+ )
303911
304613
  };
303912
304614
  var MonitorThresholdsSchema = external_exports.object({
303913
304615
  critical: external_exports.number().optional(),
@@ -304001,6 +304703,18 @@ function summarizeZodIssue(error2) {
304001
304703
  const expected = issue2.code === "invalid_type" && "expected" in issue2 ? `expected ${String(issue2.expected)}` : issue2.message;
304002
304704
  return `${path}: ${expected}`;
304003
304705
  }
304706
+ function annotateMonitorTimezone(monitor, tz) {
304707
+ const annotated = { ...monitor };
304708
+ if (monitor.created) {
304709
+ const ms = new Date(monitor.created).getTime();
304710
+ if (Number.isFinite(ms)) annotated.createdLocal = formatLocal(ms, tz);
304711
+ }
304712
+ if (monitor.modified) {
304713
+ const ms = new Date(monitor.modified).getTime();
304714
+ if (Number.isFinite(ms)) annotated.modifiedLocal = formatLocal(ms, tz);
304715
+ }
304716
+ return annotated;
304717
+ }
304004
304718
  function formatMonitor(m, site = "datadoghq.com") {
304005
304719
  const monitorId = m.id ?? 0;
304006
304720
  return {
@@ -304032,14 +304746,165 @@ function formatMonitorDetail(m, site = "datadoghq.com") {
304032
304746
  }
304033
304747
  return detail;
304034
304748
  }
304035
- async function listMonitors(api, params, limits, site) {
304749
+ var DEFAULT_HISTORY_TRANSITION_TYPES = [
304750
+ "alert",
304751
+ "alert recovery"
304752
+ ];
304753
+ function quoteIfNeeded2(value) {
304754
+ return /^[A-Za-z0-9_.-]+$/.test(value) ? value : `"${value}"`;
304755
+ }
304756
+ function buildMonitorHistoryQuery(params) {
304757
+ const parts = ["source:alert", `@monitor.id:${params.monitorId}`];
304758
+ const transitionTypes = params.transitionType && params.transitionType.length > 0 ? params.transitionType : void 0;
304759
+ if (transitionTypes) {
304760
+ const inner = transitionTypes.map(quoteIfNeeded2).join(" OR ");
304761
+ parts.push(`@monitor.transition.transition_type:(${inner})`);
304762
+ }
304763
+ if (params.group && params.group.length > 0) {
304764
+ const escaped = params.group.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
304765
+ parts.push(`@monitor.groups:"${escaped}"`);
304766
+ }
304767
+ return parts.join(" ");
304768
+ }
304769
+ function isMonitorState(value) {
304770
+ return value === "Alert" || value === "Warn" || value === "OK" || value === "No Data";
304771
+ }
304772
+ function isTransitionType(value) {
304773
+ return value === "alert" || value === "alert recovery" || value === "warning" || value === "warning recovery" || value === "no data" || value === "no data recovery" || value === "renotify";
304774
+ }
304775
+ function extractTimestamp(outer, inner) {
304776
+ const outerTs = outer.timestamp;
304777
+ if (outerTs instanceof Date) {
304778
+ return outerTs.toISOString();
304779
+ }
304780
+ if (typeof outerTs === "string" && outerTs.length > 0) {
304781
+ const d = new Date(outerTs);
304782
+ if (!Number.isNaN(d.getTime())) return d.toISOString();
304783
+ }
304784
+ const innerTs = inner.timestamp;
304785
+ if (typeof innerTs === "number" && Number.isFinite(innerTs)) {
304786
+ return new Date(innerTs).toISOString();
304787
+ }
304788
+ if (typeof innerTs === "string" && innerTs.length > 0) {
304789
+ const parsed = Number.parseInt(innerTs, 10);
304790
+ if (!Number.isNaN(parsed)) {
304791
+ return new Date(parsed).toISOString();
304792
+ }
304793
+ }
304794
+ return "";
304795
+ }
304796
+ function formatMonitorTransition(event) {
304797
+ const outer = event.attributes ?? {};
304798
+ const inner = outer.attributes ?? {};
304799
+ const monitor = inner.monitor;
304800
+ if (!monitor) {
304801
+ return null;
304802
+ }
304803
+ const transition = monitor.transition ?? monitor.additionalProperties?.transition;
304804
+ if (!transition) {
304805
+ return null;
304806
+ }
304807
+ const fromState = isMonitorState(transition.source_state) ? transition.source_state : null;
304808
+ const toState = isMonitorState(transition.destination_state) ? transition.destination_state : null;
304809
+ const transitionType = isTransitionType(transition.transition_type) ? transition.transition_type : null;
304810
+ if (!fromState || !toState || !transitionType) {
304811
+ return null;
304812
+ }
304813
+ const groupsRaw = monitor.groups;
304814
+ const group = Array.isArray(groupsRaw) && groupsRaw.length > 0 ? groupsRaw.map((g) => String(g)).join(",") : null;
304815
+ const monitorId = typeof monitor.id === "number" ? monitor.id : 0;
304816
+ const monitorName = typeof monitor.name === "string" && monitor.name.length > 0 ? monitor.name : `Monitor ${monitorId}`;
304817
+ return {
304818
+ timestamp: extractTimestamp(outer, inner),
304819
+ monitorId,
304820
+ monitorName,
304821
+ group,
304822
+ fromState,
304823
+ toState,
304824
+ transitionType,
304825
+ eventId: String(event.id ?? "")
304826
+ };
304827
+ }
304828
+ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
304829
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
304830
+ const defaultTo = now();
304831
+ const [validFrom, validTo] = ensureValidTimeRange(
304832
+ parseTime(params.from, defaultFrom),
304833
+ parseTime(params.to, defaultTo)
304834
+ );
304835
+ const fromTime = new Date(validFrom * 1e3).toISOString();
304836
+ const toTime = new Date(validTo * 1e3).toISOString();
304837
+ const effectiveTransitionTypes = params.transitionType && params.transitionType.length > 0 ? params.transitionType : [...DEFAULT_HISTORY_TRANSITION_TYPES];
304838
+ const query = buildMonitorHistoryQuery({
304839
+ monitorId,
304840
+ transitionType: effectiveTransitionTypes,
304841
+ group: params.group
304842
+ });
304843
+ const transitions = [];
304844
+ const maxEventsToProcess = 1e4;
304845
+ const maxPages = 100;
304846
+ let eventCount = 0;
304847
+ let pageCount = 0;
304848
+ const body = {
304849
+ filter: {
304850
+ query,
304851
+ from: fromTime,
304852
+ to: toTime
304853
+ },
304854
+ sort: "timestamp",
304855
+ page: { limit: 1e3 }
304856
+ };
304857
+ let cursor;
304858
+ while (pageCount < maxPages && eventCount < maxEventsToProcess) {
304859
+ const pageBody = { ...body, page: { ...body.page, cursor } };
304860
+ const response = await eventsApi.searchEvents({ body: pageBody });
304861
+ const events2 = response.data ?? [];
304862
+ if (events2.length === 0) break;
304863
+ for (const event of events2) {
304864
+ const transition = formatMonitorTransition(event);
304865
+ if (transition !== null) {
304866
+ transitions.push(transition);
304867
+ }
304868
+ eventCount++;
304869
+ if (eventCount >= maxEventsToProcess) break;
304870
+ }
304871
+ cursor = response.meta?.page?.after;
304872
+ if (!cursor) break;
304873
+ pageCount++;
304874
+ }
304875
+ const truncated = eventCount >= maxEventsToProcess;
304876
+ const resolvedGroup = params.group && params.group.length > 0 ? params.group : null;
304877
+ const count = transitions.length;
304878
+ const meta = {
304879
+ monitorId,
304880
+ query,
304881
+ from: fromTime,
304882
+ to: toTime,
304883
+ transitionTypes: effectiveTransitionTypes,
304884
+ group: resolvedGroup,
304885
+ count,
304886
+ totalFetched: eventCount,
304887
+ truncated,
304888
+ datadog_url: buildEventsUrl(query, validFrom, validTo, site)
304889
+ };
304890
+ return {
304891
+ transitions,
304892
+ count,
304893
+ meta
304894
+ };
304895
+ }
304896
+ async function listMonitors(api, params, limits, site, timezone) {
304897
+ if (timezone !== void 0) {
304898
+ validateIanaZone(timezone);
304899
+ }
304036
304900
  const effectiveLimit = params.limit ?? limits.defaultLimit;
304037
304901
  const response = await api.listMonitors({
304038
304902
  name: params.name,
304039
304903
  tags: params.tags?.join(","),
304040
304904
  groupStates: params.groupStates?.join(",")
304041
304905
  });
304042
- const monitors2 = response.slice(0, effectiveLimit).map((m) => formatMonitor(m, site));
304906
+ const baseMonitors = response.slice(0, effectiveLimit).map((m) => formatMonitor(m, site));
304907
+ const monitors2 = timezone !== void 0 ? baseMonitors.map((m) => annotateMonitorTimezone(m, timezone)) : baseMonitors;
304043
304908
  const statusCounts = {
304044
304909
  total: response.length,
304045
304910
  alert: response.filter((m) => m.overallState === "Alert").length,
@@ -304056,14 +304921,19 @@ async function listMonitors(api, params, limits, site) {
304056
304921
  )
304057
304922
  };
304058
304923
  }
304059
- async function getMonitor(api, id, site) {
304924
+ async function getMonitor(api, id, site, timezone) {
304925
+ if (timezone !== void 0) {
304926
+ validateIanaZone(timezone);
304927
+ }
304060
304928
  const monitorId = Number.parseInt(id, 10);
304061
304929
  if (Number.isNaN(monitorId)) {
304062
304930
  throw new Error(`Invalid monitor ID: ${id}`);
304063
304931
  }
304064
304932
  const monitor = await api.getMonitor({ monitorId });
304933
+ const baseDetail = formatMonitorDetail(monitor, site);
304934
+ const detail = timezone !== void 0 ? annotateMonitorTimezone(baseDetail, timezone) : baseDetail;
304065
304935
  return {
304066
- monitor: formatMonitorDetail(monitor, site),
304936
+ monitor: detail,
304067
304937
  datadog_url: buildMonitorUrl(monitorId, site)
304068
304938
  };
304069
304939
  }
@@ -304165,6 +305035,37 @@ async function createMonitor(api, config2, site = "datadoghq.com") {
304165
305035
  }
304166
305036
  return result;
304167
305037
  }
305038
+ async function dryRunMonitor(api, config2) {
305039
+ const normalized = normalizeMonitorConfig(config2);
305040
+ const body = normalized;
305041
+ await api.validateMonitor({ body });
305042
+ return {
305043
+ valid: true,
305044
+ dryRun: true,
305045
+ monitor: normalized
305046
+ };
305047
+ }
305048
+ async function previewMonitor(api, args, site = "datadoghq.com") {
305049
+ let template;
305050
+ let monitorId;
305051
+ if (args.inlineMessage !== void 0 && args.inlineMessage !== "") {
305052
+ template = args.inlineMessage;
305053
+ } else if (args.monitorIdSource !== void 0 && args.monitorIdSource !== "") {
305054
+ const loaded = await getMonitor(api, args.monitorIdSource, site);
305055
+ template = loaded.monitor.message ?? "";
305056
+ monitorId = loaded.monitor.id;
305057
+ } else {
305058
+ throw new Error(
305059
+ "Action 'preview' requires either an inline 'message' or a 'monitor_id' (or 'id') to load the template from."
305060
+ );
305061
+ }
305062
+ const result = renderMonitorTemplate(template, args.context ?? {});
305063
+ return monitorId !== void 0 ? { ...result, monitorId } : result;
305064
+ }
305065
+ var TEST_NOTIFICATION_NOT_SUPPORTED_MESSAGE = "ENOT_SUPPORTED: action 'test_notification' is not implemented \u2014 the Datadog public REST API exposes no monitor test-notification endpoint at v1 or v2 as of the events-dx-improvements spec. Use the Datadog UI's 'Test Notifications' button on the monitor page, or open an issue if Datadog publishes such an endpoint. Reference: https://docs.datadoghq.com/api/latest/monitors/";
305066
+ function testNotificationMonitor(_args) {
305067
+ throw new McpError(ErrorCode.InvalidRequest, TEST_NOTIFICATION_NOT_SUPPORTED_MESSAGE);
305068
+ }
304168
305069
  async function updateMonitor(api, id, config2, site = "datadoghq.com") {
304169
305070
  const monitorId = Number.parseInt(id, 10);
304170
305071
  const normalized = normalizeMonitorConfig(config2, true);
@@ -304249,7 +305150,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304249
305150
  }
304250
305151
  });
304251
305152
  const rawEvents = searchResponse.data ?? [];
304252
- const events = rawEvents.map(formatEventV2);
305153
+ const events2 = rawEvents.map(formatEventV2);
304253
305154
  const contextPrefixes = new Set(
304254
305155
  params.contextTags ?? [
304255
305156
  "queue",
@@ -304261,7 +305162,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304261
305162
  ]
304262
305163
  );
304263
305164
  const monitorGroups = /* @__PURE__ */ new Map();
304264
- for (const event of events) {
305165
+ for (const event of events2) {
304265
305166
  const monitorId = event.monitorId;
304266
305167
  if (typeof monitorId !== "number") continue;
304267
305168
  let group = monitorGroups.get(monitorId);
@@ -304330,7 +305231,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304330
305231
  from: fromTime,
304331
305232
  to: toTime,
304332
305233
  totalMonitors: monitorGroups.size,
304333
- totalEvents: events.length,
305234
+ totalEvents: events2.length,
304334
305235
  contextPrefixes: Array.from(contextPrefixes),
304335
305236
  datadog_url: buildEventsUrl(query, validFrom, validTo, site)
304336
305237
  }
@@ -304339,7 +305240,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304339
305240
  function registerMonitorsTool(server, api, eventsApi, limits, readOnly = false, site = "datadoghq.com") {
304340
305241
  server.tool(
304341
305242
  "monitors",
304342
- `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top.
305243
+ `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history, preview, test_notification.
304343
305244
  Filters: name, tags, groupStates (alert/warn/ok/no data).
304344
305245
  get/create/update return the full options object so callers can safely read-then-patch.
304345
305246
 
@@ -304361,8 +305262,40 @@ top: Ranked monitors by alert frequency with real monitor names and context brea
304361
305262
  - Returns: {rank, monitor_id, name (with {{template.vars}}), message (template), total_count, by_context}
304362
305263
  - Perfect for weekly/daily alert reports
304363
305264
  - Gets real monitor names from monitors API (not event titles)
304364
-
304365
- For generic event grouping (deployments, configs), use events tool instead.`,
305265
+ - WARNING: total_count is the raw alert-event count and INCLUDES renotifies/re-evaluations.
305266
+ For monitors stuck in Alert state, Datadog emits a renotify event every renotify_interval
305267
+ minutes, which inflates this count well beyond the number of real fires. When the question
305268
+ is "how many times did this monitor actually fire", use action=history instead.
305269
+
305270
+ history: Count and list real state transitions for one monitor over a time window.
305271
+ - Inputs: id (required, monitor ID), from/to (optional time range), transitionType (optional
305272
+ filter, defaults to ["alert","alert recovery"]), group (optional multi-alert group filter).
305273
+ - Returns: {transitions: [{timestamp, monitorId, monitorName, group, fromState, toState,
305274
+ transitionType, eventId}], count, meta}
305275
+ - count = transitions.length \u2014 the number of REAL state changes (fires + recoveries by
305276
+ default), NOT the renotify-inflated count returned by action=top or events action=search.
305277
+ - Backed by Datadog v2 events search with a hardcoded source:alert + @monitor.transition.
305278
+ transition_type filter that excludes renotifies by default. To include renotifies, pass
305279
+ transitionType including "renotify".
305280
+
305281
+ preview: Render a Datadog monitor message template against a context (read-only safe).
305282
+ - Inputs: either inline 'message' OR 'monitor_id' (or existing 'id'); plus optional 'context' { variables, conditionals }.
305283
+ - Supported syntax: {{variable.name}} substitution and conditional blocks {{#name}}...{{/name}} / {{^name}}...{{/name}}
305284
+ where name is one of: ${SUPPORTED_CONDITIONALS.join(", ")}.
305285
+ - Missing variables render as {{undefined:name}} markers and are reported in 'variablesMissing'.
305286
+ - Loops ({{#each ...}}) and partials ({{> ...}}) return EUNSUPPORTED_TEMPLATE_SYNTAX.
305287
+ - Allowed under --read-only (no mutation; at most a getMonitor load).
305288
+
305289
+ test_notification: KNOWN LIMITATION \u2014 always returns ENOT_SUPPORTED.
305290
+ - Datadog's public REST API exposes no monitor test-notification endpoint at v1 or v2
305291
+ (audited against the official OpenAPI specs). The v1 SDK has no notifyMonitor / testMonitor method.
305292
+ - Allowed under --read-only because no Datadog HTTP call is attempted.
305293
+ - If Datadog publishes such an endpoint in future, this action will be reimplemented to invoke it.
305294
+ - Workaround: use the 'Test Notifications' button in the Datadog monitor UI.
305295
+
305296
+ For generic event grouping (deployments, configs), use events tool instead. Note that the
305297
+ events tool's action=search with source:alert ALSO includes renotifies; use its
305298
+ transitionType filter (or this action=history) for fires-only counts.`,
304366
305299
  InputSchema2,
304367
305300
  async ({
304368
305301
  action,
@@ -304373,22 +305306,32 @@ For generic event grouping (deployments, configs), use events tool instead.`,
304373
305306
  groupStates,
304374
305307
  limit,
304375
305308
  config: config2,
305309
+ message,
304376
305310
  end,
304377
305311
  from,
304378
305312
  to,
304379
305313
  contextTags,
304380
- maxEvents
305314
+ maxEvents,
305315
+ transitionType,
305316
+ group,
305317
+ dry_run: dryRun,
305318
+ monitor_id: monitorIdNum,
305319
+ context,
305320
+ timezone
304381
305321
  }) => {
304382
305322
  try {
304383
- checkReadOnly(action, readOnly);
305323
+ const isDryRunCreate = action === "create" && dryRun === true;
305324
+ if (!isDryRunCreate) {
305325
+ checkReadOnly(action, readOnly);
305326
+ }
304384
305327
  switch (action) {
304385
305328
  case "list":
304386
305329
  return toolResult(
304387
- await listMonitors(api, { name, tags, groupStates, limit }, limits, site)
305330
+ await listMonitors(api, { name, tags, groupStates, limit }, limits, site, timezone)
304388
305331
  );
304389
305332
  case "get": {
304390
305333
  const monitorId = requireParam(id, "id", "get");
304391
- return toolResult(await getMonitor(api, monitorId, site));
305334
+ return toolResult(await getMonitor(api, monitorId, site, timezone));
304392
305335
  }
304393
305336
  case "search": {
304394
305337
  const searchQuery = requireParam(query, "query", "search");
@@ -304396,6 +305339,9 @@ For generic event grouping (deployments, configs), use events tool instead.`,
304396
305339
  }
304397
305340
  case "create": {
304398
305341
  const monitorConfig = requireParam(config2, "config", "create");
305342
+ if (dryRun) {
305343
+ return toolResult(await dryRunMonitor(api, monitorConfig));
305344
+ }
304399
305345
  return toolResult(await createMonitor(api, monitorConfig, site));
304400
305346
  }
304401
305347
  case "update": {
@@ -304432,6 +305378,40 @@ For generic event grouping (deployments, configs), use events tool instead.`,
304432
305378
  site
304433
305379
  )
304434
305380
  );
305381
+ case "history": {
305382
+ const monitorIdString = requireParam(id, "id", "history");
305383
+ const monitorId = Number.parseInt(monitorIdString, 10);
305384
+ if (Number.isNaN(monitorId)) {
305385
+ throw new Error(`Invalid monitor ID: ${monitorIdString}`);
305386
+ }
305387
+ return toolResult(
305388
+ await historyMonitor(
305389
+ eventsApi,
305390
+ monitorId,
305391
+ { from, to, transitionType, group },
305392
+ limits,
305393
+ site
305394
+ )
305395
+ );
305396
+ }
305397
+ case "preview": {
305398
+ const monitorIdSource = monitorIdNum !== void 0 ? String(monitorIdNum) : id ?? void 0;
305399
+ return toolResult(
305400
+ await previewMonitor(
305401
+ api,
305402
+ {
305403
+ inlineMessage: message,
305404
+ monitorIdSource,
305405
+ context
305406
+ },
305407
+ site
305408
+ )
305409
+ );
305410
+ }
305411
+ case "test_notification": {
305412
+ const monitorIdSource = monitorIdNum !== void 0 ? String(monitorIdNum) : id ?? void 0;
305413
+ return testNotificationMonitor({ monitorId: monitorIdSource });
305414
+ }
304435
305415
  default:
304436
305416
  throw new Error(`Unknown action: ${action}`);
304437
305417
  }
@@ -305048,6 +306028,56 @@ var InputSchema5 = {
305048
306028
  "Maximum data points per timeseries (for query action). AI controls resolution vs token usage (default: 1000)."
305049
306029
  )
305050
306030
  };
306031
+ var ROLLUP_METHODS = /* @__PURE__ */ new Set(["avg", "max", "min", "sum", "count"]);
306032
+ var DEFAULT_ROLLUP_METHOD = "avg";
306033
+ function parseRollupFromQuery(query) {
306034
+ if (typeof query !== "string") return null;
306035
+ const matches = [...query.matchAll(/\.rollup\(\s*([^)]*?)\s*\)/g)];
306036
+ const lastMatch = matches[matches.length - 1];
306037
+ if (lastMatch === void 0) return null;
306038
+ const inner = lastMatch[1];
306039
+ if (inner === void 0 || inner.length === 0) return null;
306040
+ const parts = inner.split(",").map((p) => p.trim());
306041
+ if (parts.length === 1) {
306042
+ const raw = parts[0] ?? "";
306043
+ const interval = Number.parseInt(raw, 10);
306044
+ if (!Number.isFinite(interval) || interval <= 0 || String(interval) !== raw) {
306045
+ return null;
306046
+ }
306047
+ return { interval, method: DEFAULT_ROLLUP_METHOD, methodInferred: true };
306048
+ }
306049
+ if (parts.length === 2) {
306050
+ const method = parts[0] ?? "";
306051
+ const secondsRaw = parts[1] ?? "";
306052
+ if (!ROLLUP_METHODS.has(method)) return null;
306053
+ const interval = Number.parseInt(secondsRaw, 10);
306054
+ if (!Number.isFinite(interval) || interval <= 0 || String(interval) !== secondsRaw) {
306055
+ return null;
306056
+ }
306057
+ return { interval, method, methodInferred: false };
306058
+ }
306059
+ return null;
306060
+ }
306061
+ function computeEffectiveRollup(series) {
306062
+ const intervals = [];
306063
+ for (const s of series) {
306064
+ const pts = s.pointlist ?? [];
306065
+ if (pts.length < 2) continue;
306066
+ const first = pts[0];
306067
+ const second = pts[1];
306068
+ if (first === void 0 || second === void 0) continue;
306069
+ const deltaMs = (second[0] ?? 0) - (first[0] ?? 0);
306070
+ if (deltaMs <= 0) continue;
306071
+ intervals.push(Math.round(deltaMs / 1e3));
306072
+ }
306073
+ const primary = intervals[0];
306074
+ if (primary === void 0) return null;
306075
+ const unique = Array.from(new Set(intervals)).sort((a, b) => a - b);
306076
+ if (unique.length === 1) {
306077
+ return { interval: primary };
306078
+ }
306079
+ return { interval: primary, intervalsObserved: unique };
306080
+ }
305051
306081
  async function queryMetrics(api, params, limits, site) {
305052
306082
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
305053
306083
  const defaultTo = now();
@@ -305060,7 +306090,8 @@ async function queryMetrics(api, params, limits, site) {
305060
306090
  to: toTs,
305061
306091
  query: params.query
305062
306092
  });
305063
- const series = (response.series ?? []).map((s) => ({
306093
+ const rawSeries = response.series ?? [];
306094
+ const series = rawSeries.map((s) => ({
305064
306095
  metric: s.metric ?? "",
305065
306096
  points: (s.pointlist ?? []).slice(0, params.pointLimit ?? limits.defaultMetricDataPoints).map((p) => ({
305066
306097
  timestamp: p[0] ?? 0,
@@ -305069,6 +306100,13 @@ async function queryMetrics(api, params, limits, site) {
305069
306100
  scope: s.scope ?? "",
305070
306101
  tags: s.tagSet ?? []
305071
306102
  }));
306103
+ const rollupRequested = parseRollupFromQuery(params.query);
306104
+ const rollupEffective = computeEffectiveRollup(
306105
+ rawSeries.map((s) => ({
306106
+ pointlist: s.pointlist ?? []
306107
+ }))
306108
+ );
306109
+ const rollupOverridden = rollupRequested !== null && rollupEffective !== null && (rollupEffective.interval !== rollupRequested.interval || (rollupEffective.intervalsObserved?.some((i) => i !== rollupRequested.interval) ?? false));
305072
306110
  return {
305073
306111
  series,
305074
306112
  meta: {
@@ -305076,7 +306114,10 @@ async function queryMetrics(api, params, limits, site) {
305076
306114
  from: new Date(fromTs * 1e3).toISOString(),
305077
306115
  to: new Date(toTs * 1e3).toISOString(),
305078
306116
  seriesCount: series.length,
305079
- datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site)
306117
+ datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site),
306118
+ rollupRequested,
306119
+ rollupEffective,
306120
+ rollupOverridden
305080
306121
  }
305081
306122
  };
305082
306123
  }
@@ -306617,11 +307658,11 @@ async function searchEvents(api, params, limits, site) {
306617
307658
  sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
306618
307659
  pageLimit: params.limit ?? limits.defaultLimit
306619
307660
  });
306620
- const events = (response.data ?? []).map(formatEvent);
307661
+ const events2 = (response.data ?? []).map(formatEvent);
306621
307662
  return {
306622
- events,
307663
+ events: events2,
306623
307664
  meta: {
306624
- totalCount: events.length,
307665
+ totalCount: events2.length,
306625
307666
  timeRange: {
306626
307667
  from: new Date(fromTime * 1e3).toISOString(),
306627
307668
  to: new Date(toTime * 1e3).toISOString()
@@ -306834,19 +307875,19 @@ async function getSessionWaterfall(api, params, limits, site) {
306834
307875
  sort: "timestamp",
306835
307876
  pageLimit: limits.defaultLimit
306836
307877
  });
306837
- const events = (response.data ?? []).map(formatWaterfallEvent);
307878
+ const events2 = (response.data ?? []).map(formatWaterfallEvent);
306838
307879
  const summary = {
306839
- views: events.filter((e) => e.type === "view").length,
306840
- resources: events.filter((e) => e.type === "resource").length,
306841
- actions: events.filter((e) => e.type === "action").length,
306842
- errors: events.filter((e) => e.type === "error").length,
306843
- longTasks: events.filter((e) => e.type === "long_task").length
307880
+ views: events2.filter((e) => e.type === "view").length,
307881
+ resources: events2.filter((e) => e.type === "resource").length,
307882
+ actions: events2.filter((e) => e.type === "action").length,
307883
+ errors: events2.filter((e) => e.type === "error").length,
307884
+ longTasks: events2.filter((e) => e.type === "long_task").length
306844
307885
  };
306845
307886
  return {
306846
- events,
307887
+ events: events2,
306847
307888
  summary,
306848
307889
  meta: {
306849
- totalCount: events.length,
307890
+ totalCount: events2.length,
306850
307891
  applicationId: params.applicationId,
306851
307892
  sessionId: params.sessionId,
306852
307893
  viewId: params.viewId ?? null,
@@ -308026,6 +309067,17 @@ var dashboards = {
308026
309067
  docsUrl: "https://docs.datadoghq.com/api/latest/dashboards/"
308027
309068
  };
308028
309069
 
309070
+ // src/schema/events.ts
309071
+ var events = {
309072
+ /**
309073
+ * Diagnostic codes emitted on zero-result `events.search` responses.
309074
+ * Each code is also documented inline in `src/tools/events.ts` next to the
309075
+ * heuristic that produces it.
309076
+ */
309077
+ diagnosticCodes: ["UNINDEXED_TAG_PREFIX", "NARROW_TIME_RANGE", "RESTRICTIVE_SOURCE_FILTER"],
309078
+ docsUrl: "https://docs.datadoghq.com/api/latest/events/"
309079
+ };
309080
+
308029
309081
  // src/schema/metrics.ts
308030
309082
  var metrics = {
308031
309083
  aggregators: ["avg", "max", "min", "sum", "count"],
@@ -308086,14 +309138,14 @@ var slos = {
308086
309138
  };
308087
309139
 
308088
309140
  // src/schema/index.ts
308089
- var schemas = { dashboards, metrics, monitors, slos };
309141
+ var schemas = { dashboards, events, metrics, monitors, slos };
308090
309142
  var schemaResources = Object.keys(schemas);
308091
309143
 
308092
309144
  // src/tools/schema.ts
308093
309145
  var ResourceSchema2 = external_exports.enum(schemaResources);
308094
309146
  var InputSchema20 = {
308095
309147
  resource: ResourceSchema2.describe(
308096
- "Datadog resource type to get schema for: dashboards, metrics, monitors, slos"
309148
+ "Datadog resource type to get schema for: dashboards, events, metrics, monitors, slos"
308097
309149
  )
308098
309150
  };
308099
309151
  function getSchema(resource) {