datadog-mcp 5.6.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)"),
@@ -302959,8 +303143,21 @@ var InputSchema = {
302959
303143
  ])
302960
303144
  ).optional().describe(
302961
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.'
302962
303153
  )
302963
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
+ }
302964
303161
  function extractMonitorInfo(title) {
302965
303162
  const priorityMatch = title.match(/^\[P(\d+)\]\s*/);
302966
303163
  const priority = priorityMatch ? `P${priorityMatch[1]}` : void 0;
@@ -303147,17 +303344,17 @@ async function listEventsV1(api, params, limits) {
303147
303344
  tags: params.tags?.join(","),
303148
303345
  unaggregated: true
303149
303346
  });
303150
- let events = response.events ?? [];
303347
+ let events2 = response.events ?? [];
303151
303348
  if (params.query) {
303152
303349
  const lowerQuery = params.query.toLowerCase();
303153
- events = events.filter(
303350
+ events2 = events2.filter(
303154
303351
  (e) => e.title?.toLowerCase().includes(lowerQuery) || e.text?.toLowerCase().includes(lowerQuery)
303155
303352
  );
303156
303353
  }
303157
- const result = events.slice(0, effectiveLimit).map(formatEventV1);
303354
+ const result = events2.slice(0, effectiveLimit).map(formatEventV1);
303158
303355
  return {
303159
303356
  events: result,
303160
- total: events.length
303357
+ total: events2.length
303161
303358
  };
303162
303359
  }
303163
303360
  async function getEventV1(api, id) {
@@ -303186,6 +303383,85 @@ async function createEventV1(api, params) {
303186
303383
  }
303187
303384
  };
303188
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
+ }
303189
303465
  function quoteIfNeeded(value) {
303190
303466
  return /^[A-Za-z0-9_.-]+$/.test(value) ? value : `"${value}"`;
303191
303467
  }
@@ -303213,6 +303489,9 @@ function buildEventQuery(params) {
303213
303489
  return parts.length > 0 ? parts.join(" ") : "*";
303214
303490
  }
303215
303491
  async function searchEventsV2(api, params, limits, site) {
303492
+ if (params.timezone !== void 0) {
303493
+ validateIanaZone(params.timezone);
303494
+ }
303216
303495
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303217
303496
  const defaultTo = now();
303218
303497
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303242,12 +303521,13 @@ async function searchEventsV2(api, params, limits, site) {
303242
303521
  }
303243
303522
  };
303244
303523
  const response = await api.searchEvents({ body });
303245
- 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;
303246
303526
  const nextCursor = response.meta?.page?.after;
303247
- return {
303248
- events,
303527
+ const baseResult = {
303528
+ events: events2,
303249
303529
  meta: {
303250
- count: events.length,
303530
+ count: events2.length,
303251
303531
  query: fullQuery,
303252
303532
  from: fromTime,
303253
303533
  to: toTime,
@@ -303255,8 +303535,131 @@ async function searchEventsV2(api, params, limits, site) {
303255
303535
  datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
303256
303536
  }
303257
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;
303258
303658
  }
303259
303659
  async function aggregateEventsV2(api, params, limits, site) {
303660
+ if (params.timezone !== void 0) {
303661
+ validateIanaZone(params.timezone);
303662
+ }
303260
303663
  const counts = /* @__PURE__ */ new Map();
303261
303664
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303262
303665
  const defaultTo = now();
@@ -303293,9 +303696,9 @@ async function aggregateEventsV2(api, params, limits, site) {
303293
303696
  while (pageCount < maxPages && eventCount < maxEventsToAggregate) {
303294
303697
  const pageBody = { ...body, page: { ...body.page, cursor } };
303295
303698
  const response = await api.searchEvents({ body: pageBody });
303296
- const events = response.data ?? [];
303297
- if (events.length === 0) break;
303298
- for (const event of events) {
303699
+ const events2 = response.data ?? [];
303700
+ if (events2.length === 0) break;
303701
+ for (const event of events2) {
303299
303702
  const formatted = formatEventV2(event);
303300
303703
  const groupKey = buildGroupKey(formatted, groupByFields);
303301
303704
  const existing = counts.get(groupKey);
@@ -303316,7 +303719,8 @@ async function aggregateEventsV2(api, params, limits, site) {
303316
303719
  const buckets = sorted.map(([key, data]) => ({
303317
303720
  key,
303318
303721
  count: data.count,
303319
- 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
303320
303724
  }));
303321
303725
  return {
303322
303726
  buckets,
@@ -303333,6 +303737,9 @@ async function aggregateEventsV2(api, params, limits, site) {
303333
303737
  };
303334
303738
  }
303335
303739
  async function topEventsV2(api, params, limits, site) {
303740
+ if (params.timezone !== void 0) {
303741
+ validateIanaZone(params.timezone);
303742
+ }
303336
303743
  if (params.contextTags !== void 0) {
303337
303744
  if (!Array.isArray(params.contextTags)) {
303338
303745
  throw new Error("contextTags must be an array");
@@ -303370,7 +303777,7 @@ async function topEventsV2(api, params, limits, site) {
303370
303777
  value = event.monitorInfo?.name ?? event.title;
303371
303778
  } else {
303372
303779
  const tag = event.tags.find((t) => t.startsWith(`${field}:`));
303373
- value = tag ? tag.split(":", 2)[1] : "unknown";
303780
+ value = tag ? tag.split(":", 2)[1] ?? "unknown" : "unknown";
303374
303781
  }
303375
303782
  groupValues[field] = value;
303376
303783
  keyParts.push(`${field}:${value}`);
@@ -303431,6 +303838,9 @@ function parseIntervalToMs(interval) {
303431
303838
  return ns ? Math.floor(ns / 1e6) : 36e5;
303432
303839
  }
303433
303840
  async function timeseriesEventsV2(api, params, limits, site) {
303841
+ if (params.timezone !== void 0) {
303842
+ validateIanaZone(params.timezone);
303843
+ }
303434
303844
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303435
303845
  const defaultTo = now();
303436
303846
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303465,9 +303875,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
303465
303875
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
303466
303876
  const pageBody = { ...body, page: { ...body.page, cursor } };
303467
303877
  const response = await api.searchEvents({ body: pageBody });
303468
- const events = response.data ?? [];
303469
- if (events.length === 0) break;
303470
- for (const event of events) {
303878
+ const events2 = response.data ?? [];
303879
+ if (events2.length === 0) break;
303880
+ for (const event of events2) {
303471
303881
  const formatted = formatEventV2(event);
303472
303882
  const groupKey = buildGroupKey(formatted, groupByFields);
303473
303883
  const eventTs = new Date(formatted.timestamp).getTime();
@@ -303484,6 +303894,7 @@ async function timeseriesEventsV2(api, params, limits, site) {
303484
303894
  if (!cursor) break;
303485
303895
  pageCount++;
303486
303896
  }
303897
+ const tz = params.timezone;
303487
303898
  const sortedBuckets = [...timeBuckets.entries()].sort((a, b) => a[0] - b[0]).map(([bucketTs, groupCounts]) => {
303488
303899
  const counts = {};
303489
303900
  let total = 0;
@@ -303491,12 +303902,16 @@ async function timeseriesEventsV2(api, params, limits, site) {
303491
303902
  counts[key] = count;
303492
303903
  total += count;
303493
303904
  }
303494
- return {
303905
+ const bucket = {
303495
303906
  timestamp: new Date(bucketTs).toISOString(),
303496
303907
  timestampMs: bucketTs,
303497
303908
  counts,
303498
303909
  total
303499
303910
  };
303911
+ if (tz !== void 0) {
303912
+ bucket.timestampLocal = formatLocal(bucketTs, tz);
303913
+ }
303914
+ return bucket;
303500
303915
  });
303501
303916
  const effectiveLimit = params.limit ?? 100;
303502
303917
  const limitedBuckets = sortedBuckets.slice(0, effectiveLimit);
@@ -303517,6 +303932,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
303517
303932
  };
303518
303933
  }
303519
303934
  async function incidentsEventsV2(api, params, limits, site) {
303935
+ if (params.timezone !== void 0) {
303936
+ validateIanaZone(params.timezone);
303937
+ }
303520
303938
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303521
303939
  const defaultTo = now();
303522
303940
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303551,9 +303969,9 @@ async function incidentsEventsV2(api, params, limits, site) {
303551
303969
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
303552
303970
  const pageBody = { ...body, page: { ...body.page, cursor } };
303553
303971
  const response = await api.searchEvents({ body: pageBody });
303554
- const events = response.data ?? [];
303555
- if (events.length === 0) break;
303556
- for (const event of events) {
303972
+ const events2 = response.data ?? [];
303973
+ if (events2.length === 0) break;
303974
+ for (const event of events2) {
303557
303975
  const formatted = formatEventV2(event);
303558
303976
  const monitorName = formatted.monitorInfo?.name ?? formatted.title;
303559
303977
  if (!monitorName) {
@@ -303621,6 +304039,7 @@ async function incidentsEventsV2(api, params, limits, site) {
303621
304039
  if (!cursor) break;
303622
304040
  pageCount++;
303623
304041
  }
304042
+ const tz = params.timezone;
303624
304043
  const incidentList = [...incidents.values()].map((inc) => {
303625
304044
  let duration3;
303626
304045
  if (inc.recoveredAt) {
@@ -303633,7 +304052,7 @@ async function incidentsEventsV2(api, params, limits, site) {
303633
304052
  duration3 = `${(durationMs / 36e5).toFixed(1)}h`;
303634
304053
  }
303635
304054
  }
303636
- return {
304055
+ const base = {
303637
304056
  monitorName: inc.monitorName,
303638
304057
  firstTrigger: inc.firstTrigger.toISOString(),
303639
304058
  lastTrigger: inc.lastTrigger.toISOString(),
@@ -303641,8 +304060,17 @@ async function incidentsEventsV2(api, params, limits, site) {
303641
304060
  recovered: inc.recovered,
303642
304061
  recoveredAt: inc.recoveredAt?.toISOString(),
303643
304062
  duration: duration3,
303644
- 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
303645
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;
303646
304074
  });
303647
304075
  incidentList.sort(
303648
304076
  (a, b) => new Date(b.firstTrigger).getTime() - new Date(a.firstTrigger).getTime()
@@ -303665,15 +304093,15 @@ async function incidentsEventsV2(api, params, limits, site) {
303665
304093
  }
303666
304094
  };
303667
304095
  }
303668
- async function enrichWithMonitorMetadata(events, monitorsApi) {
304096
+ async function enrichWithMonitorMetadata(events2, monitorsApi) {
303669
304097
  const monitorIds = /* @__PURE__ */ new Set();
303670
- for (const event of events) {
304098
+ for (const event of events2) {
303671
304099
  if (event.monitorId) {
303672
304100
  monitorIds.add(event.monitorId);
303673
304101
  }
303674
304102
  }
303675
304103
  if (monitorIds.size === 0) {
303676
- return events;
304104
+ return events2;
303677
304105
  }
303678
304106
  const monitorCache = /* @__PURE__ */ new Map();
303679
304107
  try {
@@ -303687,9 +304115,9 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
303687
304115
  }
303688
304116
  }
303689
304117
  } catch {
303690
- return events;
304118
+ return events2;
303691
304119
  }
303692
- return events.map((event) => {
304120
+ return events2.map((event) => {
303693
304121
  const enriched = { ...event };
303694
304122
  if (event.monitorId) {
303695
304123
  const monitor = monitorCache.get(event.monitorId);
@@ -303714,7 +304142,7 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
303714
304142
  function registerEventsTool(server, apiV1, apiV2, monitorsApi, limits, readOnly = false, site = "datadoghq.com") {
303715
304143
  server.tool(
303716
304144
  "events",
303717
- `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.
303718
304146
  For monitor alerts, use tags: ["source:alert"].
303719
304147
 
303720
304148
  IMPORTANT \u2014 re-evaluation vs transition:
@@ -303732,7 +304160,8 @@ discover: Returns available tag prefixes from events.
303732
304160
  aggregate: Custom groupBy, returns pipe-delimited keys.
303733
304161
  search: Full event details.
303734
304162
  timeseries: Time-bucketed trends with interval.
303735
- 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.`,
303736
304165
  InputSchema,
303737
304166
  async ({
303738
304167
  action,
@@ -303754,7 +304183,9 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303754
304183
  enrich,
303755
304184
  contextTags,
303756
304185
  maxEvents,
303757
- transitionType
304186
+ transitionType,
304187
+ bucket_by,
304188
+ timezone
303758
304189
  }) => {
303759
304190
  try {
303760
304191
  checkReadOnly(action, readOnly);
@@ -303804,7 +304235,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303804
304235
  priority,
303805
304236
  limit,
303806
304237
  cursor,
303807
- transitionType
304238
+ transitionType,
304239
+ timezone
303808
304240
  },
303809
304241
  limits,
303810
304242
  site
@@ -303827,7 +304259,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303827
304259
  tags,
303828
304260
  groupBy,
303829
304261
  limit,
303830
- transitionType
304262
+ transitionType,
304263
+ timezone
303831
304264
  },
303832
304265
  limits,
303833
304266
  site
@@ -303847,7 +304280,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303847
304280
  groupBy,
303848
304281
  contextTags,
303849
304282
  maxEvents,
303850
- transitionType
304283
+ transitionType,
304284
+ timezone
303851
304285
  },
303852
304286
  limits,
303853
304287
  site
@@ -303881,7 +304315,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303881
304315
  groupBy,
303882
304316
  interval,
303883
304317
  limit,
303884
- transitionType
304318
+ transitionType,
304319
+ timezone
303885
304320
  },
303886
304321
  limits,
303887
304322
  site
@@ -303899,12 +304334,33 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303899
304334
  tags,
303900
304335
  dedupeWindow,
303901
304336
  limit,
303902
- transitionType
304337
+ transitionType,
304338
+ timezone
304339
+ },
304340
+ limits,
304341
+ site
304342
+ )
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
303903
304358
  },
303904
304359
  limits,
303905
304360
  site
303906
304361
  )
303907
304362
  );
304363
+ }
303908
304364
  default:
303909
304365
  throw new Error(`Unknown action: ${action}`);
303910
304366
  }
@@ -303915,6 +304371,177 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303915
304371
  );
303916
304372
  }
303917
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
+
303918
304545
  // src/tools/monitors.ts
303919
304546
  var ActionSchema2 = external_exports.enum([
303920
304547
  "list",
@@ -303926,8 +304553,14 @@ var ActionSchema2 = external_exports.enum([
303926
304553
  "mute",
303927
304554
  "unmute",
303928
304555
  "top",
303929
- "history"
304556
+ "history",
304557
+ "preview",
304558
+ "test_notification"
303930
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).");
303931
304564
  var InputSchema2 = {
303932
304565
  action: ActionSchema2.describe("Action to perform"),
303933
304566
  id: external_exports.string().optional().describe("Monitor ID (required for get/update/delete/mute/unmute)"),
@@ -303939,8 +304572,14 @@ var InputSchema2 = {
303939
304572
  ),
303940
304573
  limit: external_exports.number().min(1).optional().describe("Maximum number of monitors to return (default: 50)"),
303941
304574
  config: external_exports.record(external_exports.unknown()).optional().describe("Monitor configuration (for create/update)"),
303942
- 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
+ ),
303943
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,
303944
304583
  // Top action parameters
303945
304584
  from: external_exports.string().optional().describe('Start time (ISO 8601, relative like "1h", or Unix timestamp)'),
303946
304585
  to: external_exports.string().optional().describe('End time (ISO 8601, relative like "1h", or Unix timestamp)'),
@@ -303964,6 +304603,12 @@ var InputSchema2 = {
303964
304603
  ),
303965
304604
  group: external_exports.string().optional().describe(
303966
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.'
303967
304612
  )
303968
304613
  };
303969
304614
  var MonitorThresholdsSchema = external_exports.object({
@@ -304058,6 +304703,18 @@ function summarizeZodIssue(error2) {
304058
304703
  const expected = issue2.code === "invalid_type" && "expected" in issue2 ? `expected ${String(issue2.expected)}` : issue2.message;
304059
304704
  return `${path}: ${expected}`;
304060
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
+ }
304061
304718
  function formatMonitor(m, site = "datadoghq.com") {
304062
304719
  const monitorId = m.id ?? 0;
304063
304720
  return {
@@ -304201,9 +304858,9 @@ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
304201
304858
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
304202
304859
  const pageBody = { ...body, page: { ...body.page, cursor } };
304203
304860
  const response = await eventsApi.searchEvents({ body: pageBody });
304204
- const events = response.data ?? [];
304205
- if (events.length === 0) break;
304206
- for (const event of events) {
304861
+ const events2 = response.data ?? [];
304862
+ if (events2.length === 0) break;
304863
+ for (const event of events2) {
304207
304864
  const transition = formatMonitorTransition(event);
304208
304865
  if (transition !== null) {
304209
304866
  transitions.push(transition);
@@ -304236,14 +304893,18 @@ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
304236
304893
  meta
304237
304894
  };
304238
304895
  }
304239
- async function listMonitors(api, params, limits, site) {
304896
+ async function listMonitors(api, params, limits, site, timezone) {
304897
+ if (timezone !== void 0) {
304898
+ validateIanaZone(timezone);
304899
+ }
304240
304900
  const effectiveLimit = params.limit ?? limits.defaultLimit;
304241
304901
  const response = await api.listMonitors({
304242
304902
  name: params.name,
304243
304903
  tags: params.tags?.join(","),
304244
304904
  groupStates: params.groupStates?.join(",")
304245
304905
  });
304246
- 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;
304247
304908
  const statusCounts = {
304248
304909
  total: response.length,
304249
304910
  alert: response.filter((m) => m.overallState === "Alert").length,
@@ -304260,14 +304921,19 @@ async function listMonitors(api, params, limits, site) {
304260
304921
  )
304261
304922
  };
304262
304923
  }
304263
- async function getMonitor(api, id, site) {
304924
+ async function getMonitor(api, id, site, timezone) {
304925
+ if (timezone !== void 0) {
304926
+ validateIanaZone(timezone);
304927
+ }
304264
304928
  const monitorId = Number.parseInt(id, 10);
304265
304929
  if (Number.isNaN(monitorId)) {
304266
304930
  throw new Error(`Invalid monitor ID: ${id}`);
304267
304931
  }
304268
304932
  const monitor = await api.getMonitor({ monitorId });
304933
+ const baseDetail = formatMonitorDetail(monitor, site);
304934
+ const detail = timezone !== void 0 ? annotateMonitorTimezone(baseDetail, timezone) : baseDetail;
304269
304935
  return {
304270
- monitor: formatMonitorDetail(monitor, site),
304936
+ monitor: detail,
304271
304937
  datadog_url: buildMonitorUrl(monitorId, site)
304272
304938
  };
304273
304939
  }
@@ -304369,6 +305035,37 @@ async function createMonitor(api, config2, site = "datadoghq.com") {
304369
305035
  }
304370
305036
  return result;
304371
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
+ }
304372
305069
  async function updateMonitor(api, id, config2, site = "datadoghq.com") {
304373
305070
  const monitorId = Number.parseInt(id, 10);
304374
305071
  const normalized = normalizeMonitorConfig(config2, true);
@@ -304453,7 +305150,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304453
305150
  }
304454
305151
  });
304455
305152
  const rawEvents = searchResponse.data ?? [];
304456
- const events = rawEvents.map(formatEventV2);
305153
+ const events2 = rawEvents.map(formatEventV2);
304457
305154
  const contextPrefixes = new Set(
304458
305155
  params.contextTags ?? [
304459
305156
  "queue",
@@ -304465,7 +305162,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304465
305162
  ]
304466
305163
  );
304467
305164
  const monitorGroups = /* @__PURE__ */ new Map();
304468
- for (const event of events) {
305165
+ for (const event of events2) {
304469
305166
  const monitorId = event.monitorId;
304470
305167
  if (typeof monitorId !== "number") continue;
304471
305168
  let group = monitorGroups.get(monitorId);
@@ -304534,7 +305231,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304534
305231
  from: fromTime,
304535
305232
  to: toTime,
304536
305233
  totalMonitors: monitorGroups.size,
304537
- totalEvents: events.length,
305234
+ totalEvents: events2.length,
304538
305235
  contextPrefixes: Array.from(contextPrefixes),
304539
305236
  datadog_url: buildEventsUrl(query, validFrom, validTo, site)
304540
305237
  }
@@ -304543,7 +305240,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304543
305240
  function registerMonitorsTool(server, api, eventsApi, limits, readOnly = false, site = "datadoghq.com") {
304544
305241
  server.tool(
304545
305242
  "monitors",
304546
- `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history.
305243
+ `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history, preview, test_notification.
304547
305244
  Filters: name, tags, groupStates (alert/warn/ok/no data).
304548
305245
  get/create/update return the full options object so callers can safely read-then-patch.
304549
305246
 
@@ -304581,6 +305278,21 @@ history: Count and list real state transitions for one monitor over a time windo
304581
305278
  transition_type filter that excludes renotifies by default. To include renotifies, pass
304582
305279
  transitionType including "renotify".
304583
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
+
304584
305296
  For generic event grouping (deployments, configs), use events tool instead. Note that the
304585
305297
  events tool's action=search with source:alert ALSO includes renotifies; use its
304586
305298
  transitionType filter (or this action=history) for fires-only counts.`,
@@ -304594,24 +305306,32 @@ transitionType filter (or this action=history) for fires-only counts.`,
304594
305306
  groupStates,
304595
305307
  limit,
304596
305308
  config: config2,
305309
+ message,
304597
305310
  end,
304598
305311
  from,
304599
305312
  to,
304600
305313
  contextTags,
304601
305314
  maxEvents,
304602
305315
  transitionType,
304603
- group
305316
+ group,
305317
+ dry_run: dryRun,
305318
+ monitor_id: monitorIdNum,
305319
+ context,
305320
+ timezone
304604
305321
  }) => {
304605
305322
  try {
304606
- checkReadOnly(action, readOnly);
305323
+ const isDryRunCreate = action === "create" && dryRun === true;
305324
+ if (!isDryRunCreate) {
305325
+ checkReadOnly(action, readOnly);
305326
+ }
304607
305327
  switch (action) {
304608
305328
  case "list":
304609
305329
  return toolResult(
304610
- await listMonitors(api, { name, tags, groupStates, limit }, limits, site)
305330
+ await listMonitors(api, { name, tags, groupStates, limit }, limits, site, timezone)
304611
305331
  );
304612
305332
  case "get": {
304613
305333
  const monitorId = requireParam(id, "id", "get");
304614
- return toolResult(await getMonitor(api, monitorId, site));
305334
+ return toolResult(await getMonitor(api, monitorId, site, timezone));
304615
305335
  }
304616
305336
  case "search": {
304617
305337
  const searchQuery = requireParam(query, "query", "search");
@@ -304619,6 +305339,9 @@ transitionType filter (or this action=history) for fires-only counts.`,
304619
305339
  }
304620
305340
  case "create": {
304621
305341
  const monitorConfig = requireParam(config2, "config", "create");
305342
+ if (dryRun) {
305343
+ return toolResult(await dryRunMonitor(api, monitorConfig));
305344
+ }
304622
305345
  return toolResult(await createMonitor(api, monitorConfig, site));
304623
305346
  }
304624
305347
  case "update": {
@@ -304671,6 +305394,24 @@ transitionType filter (or this action=history) for fires-only counts.`,
304671
305394
  )
304672
305395
  );
304673
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
+ }
304674
305415
  default:
304675
305416
  throw new Error(`Unknown action: ${action}`);
304676
305417
  }
@@ -305287,6 +306028,56 @@ var InputSchema5 = {
305287
306028
  "Maximum data points per timeseries (for query action). AI controls resolution vs token usage (default: 1000)."
305288
306029
  )
305289
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
+ }
305290
306081
  async function queryMetrics(api, params, limits, site) {
305291
306082
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
305292
306083
  const defaultTo = now();
@@ -305299,7 +306090,8 @@ async function queryMetrics(api, params, limits, site) {
305299
306090
  to: toTs,
305300
306091
  query: params.query
305301
306092
  });
305302
- const series = (response.series ?? []).map((s) => ({
306093
+ const rawSeries = response.series ?? [];
306094
+ const series = rawSeries.map((s) => ({
305303
306095
  metric: s.metric ?? "",
305304
306096
  points: (s.pointlist ?? []).slice(0, params.pointLimit ?? limits.defaultMetricDataPoints).map((p) => ({
305305
306097
  timestamp: p[0] ?? 0,
@@ -305308,6 +306100,13 @@ async function queryMetrics(api, params, limits, site) {
305308
306100
  scope: s.scope ?? "",
305309
306101
  tags: s.tagSet ?? []
305310
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));
305311
306110
  return {
305312
306111
  series,
305313
306112
  meta: {
@@ -305315,7 +306114,10 @@ async function queryMetrics(api, params, limits, site) {
305315
306114
  from: new Date(fromTs * 1e3).toISOString(),
305316
306115
  to: new Date(toTs * 1e3).toISOString(),
305317
306116
  seriesCount: series.length,
305318
- datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site)
306117
+ datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site),
306118
+ rollupRequested,
306119
+ rollupEffective,
306120
+ rollupOverridden
305319
306121
  }
305320
306122
  };
305321
306123
  }
@@ -306856,11 +307658,11 @@ async function searchEvents(api, params, limits, site) {
306856
307658
  sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
306857
307659
  pageLimit: params.limit ?? limits.defaultLimit
306858
307660
  });
306859
- const events = (response.data ?? []).map(formatEvent);
307661
+ const events2 = (response.data ?? []).map(formatEvent);
306860
307662
  return {
306861
- events,
307663
+ events: events2,
306862
307664
  meta: {
306863
- totalCount: events.length,
307665
+ totalCount: events2.length,
306864
307666
  timeRange: {
306865
307667
  from: new Date(fromTime * 1e3).toISOString(),
306866
307668
  to: new Date(toTime * 1e3).toISOString()
@@ -307073,19 +307875,19 @@ async function getSessionWaterfall(api, params, limits, site) {
307073
307875
  sort: "timestamp",
307074
307876
  pageLimit: limits.defaultLimit
307075
307877
  });
307076
- const events = (response.data ?? []).map(formatWaterfallEvent);
307878
+ const events2 = (response.data ?? []).map(formatWaterfallEvent);
307077
307879
  const summary = {
307078
- views: events.filter((e) => e.type === "view").length,
307079
- resources: events.filter((e) => e.type === "resource").length,
307080
- actions: events.filter((e) => e.type === "action").length,
307081
- errors: events.filter((e) => e.type === "error").length,
307082
- 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
307083
307885
  };
307084
307886
  return {
307085
- events,
307887
+ events: events2,
307086
307888
  summary,
307087
307889
  meta: {
307088
- totalCount: events.length,
307890
+ totalCount: events2.length,
307089
307891
  applicationId: params.applicationId,
307090
307892
  sessionId: params.sessionId,
307091
307893
  viewId: params.viewId ?? null,
@@ -308265,6 +309067,17 @@ var dashboards = {
308265
309067
  docsUrl: "https://docs.datadoghq.com/api/latest/dashboards/"
308266
309068
  };
308267
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
+
308268
309081
  // src/schema/metrics.ts
308269
309082
  var metrics = {
308270
309083
  aggregators: ["avg", "max", "min", "sum", "count"],
@@ -308325,14 +309138,14 @@ var slos = {
308325
309138
  };
308326
309139
 
308327
309140
  // src/schema/index.ts
308328
- var schemas = { dashboards, metrics, monitors, slos };
309141
+ var schemas = { dashboards, events, metrics, monitors, slos };
308329
309142
  var schemaResources = Object.keys(schemas);
308330
309143
 
308331
309144
  // src/tools/schema.ts
308332
309145
  var ResourceSchema2 = external_exports.enum(schemaResources);
308333
309146
  var InputSchema20 = {
308334
309147
  resource: ResourceSchema2.describe(
308335
- "Datadog resource type to get schema for: dashboards, metrics, monitors, slos"
309148
+ "Datadog resource type to get schema for: dashboards, events, metrics, monitors, slos"
308336
309149
  )
308337
309150
  };
308338
309151
  function getSchema(resource) {