datadog-mcp 5.6.0 → 5.8.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",
@@ -302780,6 +302786,10 @@ function buildRumSessionUrl(applicationId, sessionId, site = "datadoghq.com") {
302780
302786
  const base = getAppBaseUrl(site);
302781
302787
  return `${base}/rum/replay/sessions/${sessionId}?applicationId=${encodeURIComponent(applicationId)}`;
302782
302788
  }
302789
+ function buildSloUrl(sloId, site = "datadoghq.com") {
302790
+ const base = getAppBaseUrl(site);
302791
+ return `${base}/slo/${sloId}`;
302792
+ }
302783
302793
 
302784
302794
  // src/utils/time.ts
302785
302795
  function hoursAgo(hours) {
@@ -302904,6 +302914,182 @@ function formatDurationNs(ns) {
302904
302914
  return `${(ns / 6e10).toFixed(2)}m`;
302905
302915
  }
302906
302916
 
302917
+ // src/utils/timezone.ts
302918
+ var ERROR_PREFIX = "EINVALID_TIMEZONE";
302919
+ var WEEKDAY_TO_INDEX = {
302920
+ Sunday: 0,
302921
+ Monday: 1,
302922
+ Tuesday: 2,
302923
+ Wednesday: 3,
302924
+ Thursday: 4,
302925
+ Friday: 5,
302926
+ Saturday: 6
302927
+ };
302928
+ function validateIanaZone(tz) {
302929
+ if (typeof tz !== "string" || tz.length === 0) {
302930
+ throw new Error(
302931
+ `${ERROR_PREFIX}: timezone must be a non-empty IANA identifier (e.g. "UTC", "Europe/Paris", "America/New_York")`
302932
+ );
302933
+ }
302934
+ try {
302935
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
302936
+ return;
302937
+ } catch {
302938
+ const suggestions = suggestZones(tz);
302939
+ const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
302940
+ throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.${suggestionText}`);
302941
+ }
302942
+ }
302943
+ function suggestZones(input) {
302944
+ let zones;
302945
+ try {
302946
+ zones = Intl.supportedValuesOf("timeZone");
302947
+ } catch {
302948
+ return [];
302949
+ }
302950
+ const lower = input.toLowerCase();
302951
+ const scored = zones.map((zone) => ({
302952
+ zone,
302953
+ score: scoreZone(zone, lower)
302954
+ }));
302955
+ scored.sort((a, b) => a.score - b.score);
302956
+ return scored.slice(0, 3).map((s) => s.zone);
302957
+ }
302958
+ function scoreZone(zone, lowerInput) {
302959
+ const lowerZone = zone.toLowerCase();
302960
+ const baseDistance = levenshtein(lowerZone, lowerInput);
302961
+ if (lowerZone.includes(lowerInput) || lowerInput.includes(lowerZone)) {
302962
+ return baseDistance - 100;
302963
+ }
302964
+ return baseDistance;
302965
+ }
302966
+ function levenshtein(a, b) {
302967
+ if (a.length === 0) return b.length;
302968
+ if (b.length === 0) return a.length;
302969
+ if (a.length > b.length) {
302970
+ const tmp = a;
302971
+ a = b;
302972
+ b = tmp;
302973
+ }
302974
+ let previous = new Array(a.length + 1);
302975
+ let current = new Array(a.length + 1);
302976
+ for (let i = 0; i <= a.length; i++) previous[i] = i;
302977
+ for (let j = 1; j <= b.length; j++) {
302978
+ current[0] = j;
302979
+ const bChar = b.charCodeAt(j - 1);
302980
+ for (let i = 1; i <= a.length; i++) {
302981
+ const cost = a.charCodeAt(i - 1) === bChar ? 0 : 1;
302982
+ const prevI = previous[i] ?? 0;
302983
+ const currIMinus1 = current[i - 1] ?? 0;
302984
+ const prevIMinus1 = previous[i - 1] ?? 0;
302985
+ current[i] = Math.min(prevI + 1, currIMinus1 + 1, prevIMinus1 + cost);
302986
+ }
302987
+ const tmp = previous;
302988
+ previous = current;
302989
+ current = tmp;
302990
+ }
302991
+ return previous[a.length] ?? 0;
302992
+ }
302993
+ var partsFormatterCache = /* @__PURE__ */ new Map();
302994
+ var offsetFormatterCache = /* @__PURE__ */ new Map();
302995
+ function getPartsFormatter(tz) {
302996
+ const cached2 = partsFormatterCache.get(tz);
302997
+ if (cached2) return cached2;
302998
+ const fmt = new Intl.DateTimeFormat("en-US", {
302999
+ timeZone: tz,
303000
+ hourCycle: "h23",
303001
+ // 0-23, avoids "24" edge case at midnight
303002
+ year: "numeric",
303003
+ month: "2-digit",
303004
+ day: "2-digit",
303005
+ hour: "2-digit",
303006
+ minute: "2-digit",
303007
+ second: "2-digit",
303008
+ weekday: "long"
303009
+ });
303010
+ partsFormatterCache.set(tz, fmt);
303011
+ return fmt;
303012
+ }
303013
+ function getOffsetFormatter(tz) {
303014
+ const cached2 = offsetFormatterCache.get(tz);
303015
+ if (cached2) return cached2;
303016
+ const fmt = new Intl.DateTimeFormat("en-US", {
303017
+ timeZone: tz,
303018
+ timeZoneName: "longOffset"
303019
+ });
303020
+ offsetFormatterCache.set(tz, fmt);
303021
+ return fmt;
303022
+ }
303023
+ function getDateParts(epochMs, tz) {
303024
+ let parts;
303025
+ try {
303026
+ parts = getPartsFormatter(tz).formatToParts(epochMs);
303027
+ } catch {
303028
+ throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.`);
303029
+ }
303030
+ const out = {
303031
+ year: "",
303032
+ month: "",
303033
+ day: "",
303034
+ hour: "",
303035
+ minute: "",
303036
+ second: "",
303037
+ weekday: ""
303038
+ };
303039
+ for (const part of parts) {
303040
+ if (part.type === "year") out.year = part.value;
303041
+ else if (part.type === "month") out.month = part.value;
303042
+ else if (part.type === "day") out.day = part.value;
303043
+ else if (part.type === "hour") out.hour = part.value;
303044
+ else if (part.type === "minute") out.minute = part.value;
303045
+ else if (part.type === "second") out.second = part.value;
303046
+ else if (part.type === "weekday") out.weekday = part.value;
303047
+ }
303048
+ return out;
303049
+ }
303050
+ function getOffsetIso(epochMs, tz) {
303051
+ let parts;
303052
+ try {
303053
+ parts = getOffsetFormatter(tz).formatToParts(epochMs);
303054
+ } catch {
303055
+ throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.`);
303056
+ }
303057
+ const offsetPart = parts.find((p) => p.type === "timeZoneName");
303058
+ const raw = offsetPart?.value ?? "";
303059
+ if (raw === "GMT" || raw === "GMT+00:00" || raw === "GMT-00:00") {
303060
+ return "Z";
303061
+ }
303062
+ if (raw.startsWith("GMT")) {
303063
+ return raw.slice(3);
303064
+ }
303065
+ return raw;
303066
+ }
303067
+ function formatLocal(epochMs, tz) {
303068
+ validateIanaZone(tz);
303069
+ const parts = getDateParts(epochMs, tz);
303070
+ const offset = getOffsetIso(epochMs, tz);
303071
+ return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;
303072
+ }
303073
+ function bucketHourOfDay(epochMs, tz) {
303074
+ validateIanaZone(tz);
303075
+ const parts = getDateParts(epochMs, tz);
303076
+ return Number(parts.hour);
303077
+ }
303078
+ function bucketDayOfWeek(epochMs, tz) {
303079
+ validateIanaZone(tz);
303080
+ const parts = getDateParts(epochMs, tz);
303081
+ const index = WEEKDAY_TO_INDEX[parts.weekday];
303082
+ if (index === void 0) {
303083
+ throw new Error(`${ERROR_PREFIX}: could not resolve weekday for "${parts.weekday}" in "${tz}"`);
303084
+ }
303085
+ return index;
303086
+ }
303087
+ function bucketDayOfMonth(epochMs, tz) {
303088
+ validateIanaZone(tz);
303089
+ const parts = getDateParts(epochMs, tz);
303090
+ return Number(parts.day);
303091
+ }
303092
+
302907
303093
  // src/tools/events.ts
302908
303094
  var ActionSchema = external_exports.enum([
302909
303095
  "list",
@@ -302914,8 +303100,10 @@ var ActionSchema = external_exports.enum([
302914
303100
  "top",
302915
303101
  "timeseries",
302916
303102
  "incidents",
302917
- "discover"
303103
+ "discover",
303104
+ "histogram"
302918
303105
  ]);
303106
+ var HistogramBucketBySchema = external_exports.enum(["hour_of_day", "day_of_week", "day_of_month"]);
302919
303107
  var InputSchema = {
302920
303108
  action: ActionSchema.describe("Action to perform"),
302921
303109
  id: external_exports.string().optional().describe("Event ID (for get action)"),
@@ -302959,8 +303147,54 @@ var InputSchema = {
302959
303147
  ])
302960
303148
  ).optional().describe(
302961
303149
  '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.'
303150
+ ),
303151
+ // Histogram action (Requirement 3): bucket events by local hour/day-of-week/day-of-month.
303152
+ bucket_by: HistogramBucketBySchema.optional().describe(
303153
+ "Bucket dimension for histogram action: hour_of_day (0-23), day_of_week (0=Sun..6=Sat), day_of_month (1-31)."
303154
+ ),
303155
+ timezone: external_exports.string().optional().describe(
303156
+ '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.'
303157
+ ),
303158
+ // Projection — applied to search action only. Reduces response payload
303159
+ // when callers only need a subset of fields. Unknown field names are silently ignored so
303160
+ // older clients are not broken when the EventSummary shape evolves.
303161
+ fields: external_exports.array(external_exports.string()).optional().describe(
303162
+ "Search action only: return only these event fields. Allowed values: id, title, message, timestamp, priority, source, tags, alertType, host, monitorId, monitorInfo, monitorMetadata (only populated when enrich=true). Default: full event."
302962
303163
  )
302963
303164
  };
303165
+ var ALLOWED_EVENT_FIELDS = /* @__PURE__ */ new Set([
303166
+ "id",
303167
+ "title",
303168
+ "message",
303169
+ "timestamp",
303170
+ "priority",
303171
+ "source",
303172
+ "tags",
303173
+ "alertType",
303174
+ "host",
303175
+ "monitorId",
303176
+ "monitorInfo",
303177
+ "monitorMetadata"
303178
+ ]);
303179
+ function pickEventFields(event, fields) {
303180
+ if (!fields || fields.length === 0) {
303181
+ return event;
303182
+ }
303183
+ const projection = {};
303184
+ for (const key of fields) {
303185
+ if (ALLOWED_EVENT_FIELDS.has(key)) {
303186
+ ;
303187
+ projection[key] = event[key];
303188
+ }
303189
+ }
303190
+ return projection;
303191
+ }
303192
+ function annotateEventTimezone(event, tz) {
303193
+ if (!event.timestamp) return event;
303194
+ const ms = new Date(event.timestamp).getTime();
303195
+ if (!Number.isFinite(ms)) return event;
303196
+ return { ...event, timestampLocal: formatLocal(ms, tz) };
303197
+ }
302964
303198
  function extractMonitorInfo(title) {
302965
303199
  const priorityMatch = title.match(/^\[P(\d+)\]\s*/);
302966
303200
  const priority = priorityMatch ? `P${priorityMatch[1]}` : void 0;
@@ -303147,17 +303381,17 @@ async function listEventsV1(api, params, limits) {
303147
303381
  tags: params.tags?.join(","),
303148
303382
  unaggregated: true
303149
303383
  });
303150
- let events = response.events ?? [];
303384
+ let events2 = response.events ?? [];
303151
303385
  if (params.query) {
303152
303386
  const lowerQuery = params.query.toLowerCase();
303153
- events = events.filter(
303387
+ events2 = events2.filter(
303154
303388
  (e) => e.title?.toLowerCase().includes(lowerQuery) || e.text?.toLowerCase().includes(lowerQuery)
303155
303389
  );
303156
303390
  }
303157
- const result = events.slice(0, effectiveLimit).map(formatEventV1);
303391
+ const result = events2.slice(0, effectiveLimit).map(formatEventV1);
303158
303392
  return {
303159
303393
  events: result,
303160
- total: events.length
303394
+ total: events2.length
303161
303395
  };
303162
303396
  }
303163
303397
  async function getEventV1(api, id) {
@@ -303186,6 +303420,85 @@ async function createEventV1(api, params) {
303186
303420
  }
303187
303421
  };
303188
303422
  }
303423
+ var UNINDEXED_ALERT_TAG_PREFIXES = [
303424
+ "monitor_priority",
303425
+ "notification_preset",
303426
+ "monitor_tags",
303427
+ "alert_cycle_key",
303428
+ "monitor_group_key",
303429
+ "notification_method"
303430
+ ];
303431
+ var NARROW_TIME_RANGE_THRESHOLD_MS = 5 * 60 * 1e3;
303432
+ function extractTagPrefixes(query, tags) {
303433
+ const prefixes = [];
303434
+ if (query) {
303435
+ const re = /(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*):[^\s)]+/g;
303436
+ let match;
303437
+ while ((match = re.exec(query)) !== null) {
303438
+ if (match[1]) {
303439
+ prefixes.push(match[1]);
303440
+ }
303441
+ }
303442
+ }
303443
+ if (tags) {
303444
+ for (const tag of tags) {
303445
+ const colonIdx = tag.indexOf(":");
303446
+ if (colonIdx > 0) {
303447
+ prefixes.push(tag.slice(0, colonIdx));
303448
+ }
303449
+ }
303450
+ }
303451
+ return prefixes;
303452
+ }
303453
+ function countNonSourceTerms(query) {
303454
+ if (!query) return 0;
303455
+ const tokens = query.split(/\s+/).filter((t) => t.length > 0 && t !== "OR" && t !== "AND");
303456
+ let nonSource = 0;
303457
+ for (const token of tokens) {
303458
+ const stripped = token.replace(/[()]/g, "");
303459
+ if (stripped.length === 0) continue;
303460
+ if (stripped.startsWith("source:")) continue;
303461
+ nonSource++;
303462
+ }
303463
+ return nonSource;
303464
+ }
303465
+ function computeDiagnostics(input) {
303466
+ const diagnostics = [];
303467
+ const query = input.query ?? "";
303468
+ const queryHasSourceAlert = /(^|\s|\()source:alert(\s|\)|$)/.test(query) || // NOSONAR S5852: anchored alternation, bounded input, no nested quantifiers
303469
+ (input.sources?.includes("alert") ?? false) || (input.tags?.includes("source:alert") ?? false);
303470
+ if (queryHasSourceAlert) {
303471
+ const prefixes = extractTagPrefixes(input.query, input.tags);
303472
+ const unindexedHits = prefixes.filter((p) => UNINDEXED_ALERT_TAG_PREFIXES.includes(p));
303473
+ const uniqueHits = Array.from(new Set(unindexedHits));
303474
+ if (uniqueHits.length > 0) {
303475
+ diagnostics.push({
303476
+ code: "UNINDEXED_TAG_PREFIX",
303477
+ message: `Query filters on tag prefix(es) that Datadog does not index for source:alert events: ${uniqueHits.join(", ")}.`,
303478
+ hint: "Drop these filters and post-filter the results client-side, or aggregate via monitors/get + monitors.list with matching options."
303479
+ });
303480
+ }
303481
+ }
303482
+ if (typeof input.fromMs === "number" && typeof input.toMs === "number" && input.toMs > input.fromMs && input.toMs - input.fromMs < NARROW_TIME_RANGE_THRESHOLD_MS) {
303483
+ diagnostics.push({
303484
+ code: "NARROW_TIME_RANGE",
303485
+ message: "Time range is shorter than 5 minutes; alert events may not have been indexed yet.",
303486
+ hint: "Widen the range (e.g. last 1h) or retry after the indexing delay (~30s) has elapsed."
303487
+ });
303488
+ }
303489
+ if (queryHasSourceAlert) {
303490
+ const otherTerms = countNonSourceTerms(input.query);
303491
+ const otherTags = (input.tags ?? []).filter((t) => !t.startsWith("source:")).length;
303492
+ if (otherTerms === 0 && otherTags === 0) {
303493
+ diagnostics.push({
303494
+ code: "RESTRICTIVE_SOURCE_FILTER",
303495
+ message: "Only source:alert filter was applied; the matching event set may genuinely be empty.",
303496
+ hint: "Use events.aggregate or monitors.list to confirm no alerts fired in the window, or broaden sources (e.g. source:monitor, source:audit)."
303497
+ });
303498
+ }
303499
+ }
303500
+ return diagnostics;
303501
+ }
303189
303502
  function quoteIfNeeded(value) {
303190
303503
  return /^[A-Za-z0-9_.-]+$/.test(value) ? value : `"${value}"`;
303191
303504
  }
@@ -303213,6 +303526,9 @@ function buildEventQuery(params) {
303213
303526
  return parts.length > 0 ? parts.join(" ") : "*";
303214
303527
  }
303215
303528
  async function searchEventsV2(api, params, limits, site) {
303529
+ if (params.timezone !== void 0) {
303530
+ validateIanaZone(params.timezone);
303531
+ }
303216
303532
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303217
303533
  const defaultTo = now();
303218
303534
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303242,12 +303558,13 @@ async function searchEventsV2(api, params, limits, site) {
303242
303558
  }
303243
303559
  };
303244
303560
  const response = await api.searchEvents({ body });
303245
- const events = (response.data ?? []).map(formatEventV2);
303561
+ const rawEvents = (response.data ?? []).map(formatEventV2);
303562
+ const events2 = params.timezone !== void 0 ? rawEvents.map((e) => annotateEventTimezone(e, params.timezone)) : rawEvents;
303246
303563
  const nextCursor = response.meta?.page?.after;
303247
- return {
303248
- events,
303564
+ const baseResult = {
303565
+ events: events2,
303249
303566
  meta: {
303250
- count: events.length,
303567
+ count: events2.length,
303251
303568
  query: fullQuery,
303252
303569
  from: fromTime,
303253
303570
  to: toTime,
@@ -303255,8 +303572,131 @@ async function searchEventsV2(api, params, limits, site) {
303255
303572
  datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
303256
303573
  }
303257
303574
  };
303575
+ if (events2.length === 0) {
303576
+ const diagnostics = computeDiagnostics({
303577
+ query: params.query,
303578
+ tags: params.tags,
303579
+ sources: params.sources,
303580
+ fromMs: validFrom * 1e3,
303581
+ toMs: validTo * 1e3
303582
+ });
303583
+ return { ...baseResult, diagnostics };
303584
+ }
303585
+ return baseResult;
303586
+ }
303587
+ function bucketEvent(epochMs, bucketBy, tz) {
303588
+ switch (bucketBy) {
303589
+ case "hour_of_day":
303590
+ return bucketHourOfDay(epochMs, tz);
303591
+ case "day_of_week":
303592
+ return bucketDayOfWeek(epochMs, tz);
303593
+ case "day_of_month":
303594
+ return bucketDayOfMonth(epochMs, tz);
303595
+ default: {
303596
+ const exhaustive = bucketBy;
303597
+ throw new Error(`Unhandled bucket_by: ${String(exhaustive)}`);
303598
+ }
303599
+ }
303600
+ }
303601
+ function eventEpochMs(event) {
303602
+ const ts = event.attributes?.timestamp;
303603
+ if (ts === void 0 || ts === null) return null;
303604
+ if (ts instanceof Date) {
303605
+ const ms2 = ts.getTime();
303606
+ return Number.isFinite(ms2) ? ms2 : null;
303607
+ }
303608
+ const ms = new Date(String(ts)).getTime();
303609
+ return Number.isFinite(ms) ? ms : null;
303610
+ }
303611
+ async function histogramEventsV2(api, params, limits, site) {
303612
+ const timezone = params.timezone ?? "UTC";
303613
+ validateIanaZone(timezone);
303614
+ const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303615
+ const defaultTo = now();
303616
+ const [validFrom, validTo] = ensureValidTimeRange(
303617
+ parseTime(params.from, defaultFrom),
303618
+ parseTime(params.to, defaultTo)
303619
+ );
303620
+ const fromTime = new Date(validFrom * 1e3).toISOString();
303621
+ const toTime = new Date(validTo * 1e3).toISOString();
303622
+ const fullQuery = buildEventQuery({
303623
+ query: params.query,
303624
+ sources: params.sources,
303625
+ tags: params.tags
303626
+ });
303627
+ const cap = limits.maxEventsForHistogram;
303628
+ const perPage = Math.max(1, Math.min(1e3, cap));
303629
+ const buckets = {};
303630
+ let totalEvents = 0;
303631
+ let cursor = params.cursor;
303632
+ let bucketCountIncomplete = false;
303633
+ let exhaustedPages = false;
303634
+ const maxPages = 100;
303635
+ let pageCount = 0;
303636
+ while (pageCount < maxPages) {
303637
+ const body = {
303638
+ filter: {
303639
+ query: fullQuery,
303640
+ from: fromTime,
303641
+ to: toTime
303642
+ },
303643
+ sort: "timestamp",
303644
+ page: {
303645
+ limit: perPage,
303646
+ cursor
303647
+ }
303648
+ };
303649
+ const response = await api.searchEvents({ body });
303650
+ const data = response.data ?? [];
303651
+ const responseCursor = response.meta?.page?.after ?? void 0;
303652
+ for (const event of data) {
303653
+ const epochMs = eventEpochMs(event);
303654
+ if (epochMs === null) continue;
303655
+ const bucket = bucketEvent(epochMs, params.bucket_by, timezone);
303656
+ const key = String(bucket);
303657
+ buckets[key] = (buckets[key] ?? 0) + 1;
303658
+ totalEvents++;
303659
+ if (totalEvents >= cap) {
303660
+ bucketCountIncomplete = true;
303661
+ cursor = responseCursor;
303662
+ break;
303663
+ }
303664
+ }
303665
+ if (bucketCountIncomplete) break;
303666
+ if (data.length === 0 || !responseCursor) {
303667
+ exhaustedPages = true;
303668
+ break;
303669
+ }
303670
+ cursor = responseCursor;
303671
+ pageCount++;
303672
+ }
303673
+ const result = {
303674
+ buckets,
303675
+ bucketBy: params.bucket_by,
303676
+ timezone,
303677
+ totalEvents,
303678
+ meta: {
303679
+ query: fullQuery,
303680
+ from: fromTime,
303681
+ to: toTime,
303682
+ datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
303683
+ }
303684
+ };
303685
+ if (bucketCountIncomplete) {
303686
+ result.bucketCountIncomplete = true;
303687
+ if (cursor) {
303688
+ result.nextCursor = cursor;
303689
+ }
303690
+ } else if (!exhaustedPages && pageCount >= maxPages && cursor) {
303691
+ result.bucketCountIncomplete = true;
303692
+ result.nextCursor = cursor;
303693
+ }
303694
+ return result;
303258
303695
  }
303259
303696
  async function aggregateEventsV2(api, params, limits, site) {
303697
+ if (params.timezone !== void 0) {
303698
+ validateIanaZone(params.timezone);
303699
+ }
303260
303700
  const counts = /* @__PURE__ */ new Map();
303261
303701
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303262
303702
  const defaultTo = now();
@@ -303293,9 +303733,9 @@ async function aggregateEventsV2(api, params, limits, site) {
303293
303733
  while (pageCount < maxPages && eventCount < maxEventsToAggregate) {
303294
303734
  const pageBody = { ...body, page: { ...body.page, cursor } };
303295
303735
  const response = await api.searchEvents({ body: pageBody });
303296
- const events = response.data ?? [];
303297
- if (events.length === 0) break;
303298
- for (const event of events) {
303736
+ const events2 = response.data ?? [];
303737
+ if (events2.length === 0) break;
303738
+ for (const event of events2) {
303299
303739
  const formatted = formatEventV2(event);
303300
303740
  const groupKey = buildGroupKey(formatted, groupByFields);
303301
303741
  const existing = counts.get(groupKey);
@@ -303316,7 +303756,8 @@ async function aggregateEventsV2(api, params, limits, site) {
303316
303756
  const buckets = sorted.map(([key, data]) => ({
303317
303757
  key,
303318
303758
  count: data.count,
303319
- sample: data.sample
303759
+ // Requirement 4: annotate sample timestamps only when timezone is supplied.
303760
+ sample: params.timezone !== void 0 ? annotateEventTimezone(data.sample, params.timezone) : data.sample
303320
303761
  }));
303321
303762
  return {
303322
303763
  buckets,
@@ -303333,6 +303774,9 @@ async function aggregateEventsV2(api, params, limits, site) {
303333
303774
  };
303334
303775
  }
303335
303776
  async function topEventsV2(api, params, limits, site) {
303777
+ if (params.timezone !== void 0) {
303778
+ validateIanaZone(params.timezone);
303779
+ }
303336
303780
  if (params.contextTags !== void 0) {
303337
303781
  if (!Array.isArray(params.contextTags)) {
303338
303782
  throw new Error("contextTags must be an array");
@@ -303370,7 +303814,7 @@ async function topEventsV2(api, params, limits, site) {
303370
303814
  value = event.monitorInfo?.name ?? event.title;
303371
303815
  } else {
303372
303816
  const tag = event.tags.find((t) => t.startsWith(`${field}:`));
303373
- value = tag ? tag.split(":", 2)[1] : "unknown";
303817
+ value = tag ? tag.split(":", 2)[1] ?? "unknown" : "unknown";
303374
303818
  }
303375
303819
  groupValues[field] = value;
303376
303820
  keyParts.push(`${field}:${value}`);
@@ -303431,6 +303875,9 @@ function parseIntervalToMs(interval) {
303431
303875
  return ns ? Math.floor(ns / 1e6) : 36e5;
303432
303876
  }
303433
303877
  async function timeseriesEventsV2(api, params, limits, site) {
303878
+ if (params.timezone !== void 0) {
303879
+ validateIanaZone(params.timezone);
303880
+ }
303434
303881
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303435
303882
  const defaultTo = now();
303436
303883
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303465,9 +303912,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
303465
303912
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
303466
303913
  const pageBody = { ...body, page: { ...body.page, cursor } };
303467
303914
  const response = await api.searchEvents({ body: pageBody });
303468
- const events = response.data ?? [];
303469
- if (events.length === 0) break;
303470
- for (const event of events) {
303915
+ const events2 = response.data ?? [];
303916
+ if (events2.length === 0) break;
303917
+ for (const event of events2) {
303471
303918
  const formatted = formatEventV2(event);
303472
303919
  const groupKey = buildGroupKey(formatted, groupByFields);
303473
303920
  const eventTs = new Date(formatted.timestamp).getTime();
@@ -303484,6 +303931,7 @@ async function timeseriesEventsV2(api, params, limits, site) {
303484
303931
  if (!cursor) break;
303485
303932
  pageCount++;
303486
303933
  }
303934
+ const tz = params.timezone;
303487
303935
  const sortedBuckets = [...timeBuckets.entries()].sort((a, b) => a[0] - b[0]).map(([bucketTs, groupCounts]) => {
303488
303936
  const counts = {};
303489
303937
  let total = 0;
@@ -303491,12 +303939,16 @@ async function timeseriesEventsV2(api, params, limits, site) {
303491
303939
  counts[key] = count;
303492
303940
  total += count;
303493
303941
  }
303494
- return {
303942
+ const bucket = {
303495
303943
  timestamp: new Date(bucketTs).toISOString(),
303496
303944
  timestampMs: bucketTs,
303497
303945
  counts,
303498
303946
  total
303499
303947
  };
303948
+ if (tz !== void 0) {
303949
+ bucket.timestampLocal = formatLocal(bucketTs, tz);
303950
+ }
303951
+ return bucket;
303500
303952
  });
303501
303953
  const effectiveLimit = params.limit ?? 100;
303502
303954
  const limitedBuckets = sortedBuckets.slice(0, effectiveLimit);
@@ -303517,6 +303969,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
303517
303969
  };
303518
303970
  }
303519
303971
  async function incidentsEventsV2(api, params, limits, site) {
303972
+ if (params.timezone !== void 0) {
303973
+ validateIanaZone(params.timezone);
303974
+ }
303520
303975
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
303521
303976
  const defaultTo = now();
303522
303977
  const [validFrom, validTo] = ensureValidTimeRange(
@@ -303551,9 +304006,9 @@ async function incidentsEventsV2(api, params, limits, site) {
303551
304006
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
303552
304007
  const pageBody = { ...body, page: { ...body.page, cursor } };
303553
304008
  const response = await api.searchEvents({ body: pageBody });
303554
- const events = response.data ?? [];
303555
- if (events.length === 0) break;
303556
- for (const event of events) {
304009
+ const events2 = response.data ?? [];
304010
+ if (events2.length === 0) break;
304011
+ for (const event of events2) {
303557
304012
  const formatted = formatEventV2(event);
303558
304013
  const monitorName = formatted.monitorInfo?.name ?? formatted.title;
303559
304014
  if (!monitorName) {
@@ -303621,6 +304076,7 @@ async function incidentsEventsV2(api, params, limits, site) {
303621
304076
  if (!cursor) break;
303622
304077
  pageCount++;
303623
304078
  }
304079
+ const tz = params.timezone;
303624
304080
  const incidentList = [...incidents.values()].map((inc) => {
303625
304081
  let duration3;
303626
304082
  if (inc.recoveredAt) {
@@ -303633,7 +304089,7 @@ async function incidentsEventsV2(api, params, limits, site) {
303633
304089
  duration3 = `${(durationMs / 36e5).toFixed(1)}h`;
303634
304090
  }
303635
304091
  }
303636
- return {
304092
+ const base = {
303637
304093
  monitorName: inc.monitorName,
303638
304094
  firstTrigger: inc.firstTrigger.toISOString(),
303639
304095
  lastTrigger: inc.lastTrigger.toISOString(),
@@ -303641,8 +304097,17 @@ async function incidentsEventsV2(api, params, limits, site) {
303641
304097
  recovered: inc.recovered,
303642
304098
  recoveredAt: inc.recoveredAt?.toISOString(),
303643
304099
  duration: duration3,
303644
- sample: inc.sample
304100
+ // Requirement 4: annotate the nested sample event timestamp when tz is supplied.
304101
+ sample: tz !== void 0 ? annotateEventTimezone(inc.sample, tz) : inc.sample
303645
304102
  };
304103
+ if (tz !== void 0) {
304104
+ base.firstTriggerLocal = formatLocal(inc.firstTrigger.getTime(), tz);
304105
+ base.lastTriggerLocal = formatLocal(inc.lastTrigger.getTime(), tz);
304106
+ if (inc.recoveredAt) {
304107
+ base.recoveredAtLocal = formatLocal(inc.recoveredAt.getTime(), tz);
304108
+ }
304109
+ }
304110
+ return base;
303646
304111
  });
303647
304112
  incidentList.sort(
303648
304113
  (a, b) => new Date(b.firstTrigger).getTime() - new Date(a.firstTrigger).getTime()
@@ -303665,15 +304130,15 @@ async function incidentsEventsV2(api, params, limits, site) {
303665
304130
  }
303666
304131
  };
303667
304132
  }
303668
- async function enrichWithMonitorMetadata(events, monitorsApi) {
304133
+ async function enrichWithMonitorMetadata(events2, monitorsApi) {
303669
304134
  const monitorIds = /* @__PURE__ */ new Set();
303670
- for (const event of events) {
304135
+ for (const event of events2) {
303671
304136
  if (event.monitorId) {
303672
304137
  monitorIds.add(event.monitorId);
303673
304138
  }
303674
304139
  }
303675
304140
  if (monitorIds.size === 0) {
303676
- return events;
304141
+ return events2;
303677
304142
  }
303678
304143
  const monitorCache = /* @__PURE__ */ new Map();
303679
304144
  try {
@@ -303687,9 +304152,9 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
303687
304152
  }
303688
304153
  }
303689
304154
  } catch {
303690
- return events;
304155
+ return events2;
303691
304156
  }
303692
- return events.map((event) => {
304157
+ return events2.map((event) => {
303693
304158
  const enriched = { ...event };
303694
304159
  if (event.monitorId) {
303695
304160
  const monitor = monitorCache.get(event.monitorId);
@@ -303714,7 +304179,7 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
303714
304179
  function registerEventsTool(server, apiV1, apiV2, monitorsApi, limits, readOnly = false, site = "datadoghq.com") {
303715
304180
  server.tool(
303716
304181
  "events",
303717
- `Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents, discover.
304182
+ `Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents, discover, histogram.
303718
304183
  For monitor alerts, use tags: ["source:alert"].
303719
304184
 
303720
304185
  IMPORTANT \u2014 re-evaluation vs transition:
@@ -303732,7 +304197,8 @@ discover: Returns available tag prefixes from events.
303732
304197
  aggregate: Custom groupBy, returns pipe-delimited keys.
303733
304198
  search: Full event details.
303734
304199
  timeseries: Time-bucketed trends with interval.
303735
- incidents: Deduplicate alerts with dedupeWindow.`,
304200
+ incidents: Deduplicate alerts with dedupeWindow.
304201
+ 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
304202
  InputSchema,
303737
304203
  async ({
303738
304204
  action,
@@ -303754,7 +304220,10 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303754
304220
  enrich,
303755
304221
  contextTags,
303756
304222
  maxEvents,
303757
- transitionType
304223
+ transitionType,
304224
+ bucket_by,
304225
+ timezone,
304226
+ fields
303758
304227
  }) => {
303759
304228
  try {
303760
304229
  checkReadOnly(action, readOnly);
@@ -303804,16 +304273,19 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303804
304273
  priority,
303805
304274
  limit,
303806
304275
  cursor,
303807
- transitionType
304276
+ transitionType,
304277
+ timezone
303808
304278
  },
303809
304279
  limits,
303810
304280
  site
303811
304281
  );
303812
304282
  if (enrich && result.events.length > 0) {
303813
304283
  const enrichedEvents = await enrichWithMonitorMetadata(result.events, monitorsApi);
303814
- return toolResult({ ...result, events: enrichedEvents });
304284
+ const projected = fields?.length ? enrichedEvents.map((e) => pickEventFields(e, fields)) : enrichedEvents;
304285
+ return toolResult({ ...result, events: projected });
303815
304286
  }
303816
- return toolResult(result);
304287
+ const projectedEvents = fields?.length ? result.events.map((e) => pickEventFields(e, fields)) : result.events;
304288
+ return toolResult({ ...result, events: projectedEvents });
303817
304289
  }
303818
304290
  case "aggregate":
303819
304291
  return toolResult(
@@ -303827,7 +304299,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303827
304299
  tags,
303828
304300
  groupBy,
303829
304301
  limit,
303830
- transitionType
304302
+ transitionType,
304303
+ timezone
303831
304304
  },
303832
304305
  limits,
303833
304306
  site
@@ -303847,7 +304320,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303847
304320
  groupBy,
303848
304321
  contextTags,
303849
304322
  maxEvents,
303850
- transitionType
304323
+ transitionType,
304324
+ timezone
303851
304325
  },
303852
304326
  limits,
303853
304327
  site
@@ -303881,7 +304355,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303881
304355
  groupBy,
303882
304356
  interval,
303883
304357
  limit,
303884
- transitionType
304358
+ transitionType,
304359
+ timezone
303885
304360
  },
303886
304361
  limits,
303887
304362
  site
@@ -303899,12 +304374,33 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303899
304374
  tags,
303900
304375
  dedupeWindow,
303901
304376
  limit,
303902
- transitionType
304377
+ transitionType,
304378
+ timezone
303903
304379
  },
303904
304380
  limits,
303905
304381
  site
303906
304382
  )
303907
304383
  );
304384
+ case "histogram": {
304385
+ const histogramBucketBy = requireParam(bucket_by, "bucket_by", "histogram");
304386
+ return toolResult(
304387
+ await histogramEventsV2(
304388
+ apiV2,
304389
+ {
304390
+ query,
304391
+ from,
304392
+ to,
304393
+ sources,
304394
+ tags,
304395
+ bucket_by: histogramBucketBy,
304396
+ timezone,
304397
+ cursor
304398
+ },
304399
+ limits,
304400
+ site
304401
+ )
304402
+ );
304403
+ }
303908
304404
  default:
303909
304405
  throw new Error(`Unknown action: ${action}`);
303910
304406
  }
@@ -303915,6 +304411,177 @@ incidents: Deduplicate alerts with dedupeWindow.`,
303915
304411
  );
303916
304412
  }
303917
304413
 
304414
+ // src/utils/templatePreview.ts
304415
+ var SUPPORTED_CONDITIONALS = [
304416
+ "is_alert",
304417
+ "is_warning",
304418
+ "is_no_data",
304419
+ "is_recovery",
304420
+ "is_alert_to_warning",
304421
+ "is_warning_to_alert"
304422
+ ];
304423
+ var SUPPORTED_SET = new Set(SUPPORTED_CONDITIONALS);
304424
+ function unsupportedSyntaxError(detail) {
304425
+ const supportedList = SUPPORTED_CONDITIONALS.join(", ");
304426
+ return new Error(
304427
+ `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.`
304428
+ );
304429
+ }
304430
+ function lookupVariable(path, variables) {
304431
+ if (Object.prototype.hasOwnProperty.call(variables, path)) {
304432
+ const value = variables[path];
304433
+ return value === void 0 || value === null ? void 0 : String(value);
304434
+ }
304435
+ const segments = path.split(".");
304436
+ let cursor = variables;
304437
+ for (const segment of segments) {
304438
+ if (cursor === null || cursor === void 0) {
304439
+ return void 0;
304440
+ }
304441
+ if (typeof cursor !== "object") {
304442
+ return void 0;
304443
+ }
304444
+ const record2 = cursor;
304445
+ if (!Object.prototype.hasOwnProperty.call(record2, segment)) {
304446
+ return void 0;
304447
+ }
304448
+ cursor = record2[segment];
304449
+ }
304450
+ if (cursor === void 0 || cursor === null) {
304451
+ return void 0;
304452
+ }
304453
+ if (typeof cursor === "object") {
304454
+ return void 0;
304455
+ }
304456
+ return String(cursor);
304457
+ }
304458
+ var TAG_REGEX = /\{\{\s*([#^/>])?\s*([^}]*?)\s*\}\}/g;
304459
+ function parseBlocks(template) {
304460
+ const stack = [{ children: [] }];
304461
+ let cursor = 0;
304462
+ TAG_REGEX.lastIndex = 0;
304463
+ let match;
304464
+ while ((match = TAG_REGEX.exec(template)) !== null) {
304465
+ const [fullTag, prefix, rawName] = match;
304466
+ const tagStart = match.index;
304467
+ const name = rawName?.trim() ?? "";
304468
+ if (tagStart > cursor) {
304469
+ const top = stack[stack.length - 1];
304470
+ if (top) {
304471
+ top.children.push({ kind: "literal", text: template.slice(cursor, tagStart) });
304472
+ }
304473
+ }
304474
+ if (prefix === ">") {
304475
+ throw unsupportedSyntaxError(`partials are not supported (found {{> ${name}}})`);
304476
+ }
304477
+ if (prefix === "#" || prefix === "^") {
304478
+ if (name.startsWith("each") || /\s/.test(name)) {
304479
+ throw unsupportedSyntaxError(`loops are not supported (found {{${prefix}${name}}})`);
304480
+ }
304481
+ if (!SUPPORTED_SET.has(name)) {
304482
+ throw unsupportedSyntaxError(`unknown conditional '${name}' in {{${prefix}${name}}}`);
304483
+ }
304484
+ stack.push({
304485
+ children: [],
304486
+ closer: { conditional: name, negated: prefix === "^" }
304487
+ });
304488
+ } else if (prefix === "/") {
304489
+ const frame = stack.pop();
304490
+ if (!frame || !frame.closer) {
304491
+ throw unsupportedSyntaxError(`unmatched closing tag {{/${name}}}`);
304492
+ }
304493
+ if (frame.closer.conditional !== name) {
304494
+ throw unsupportedSyntaxError(
304495
+ `mismatched closing tag {{/${name}}} (expected {{/${frame.closer.conditional}}})`
304496
+ );
304497
+ }
304498
+ const parent = stack[stack.length - 1];
304499
+ if (!parent) {
304500
+ throw unsupportedSyntaxError("block stack underflow while closing tag");
304501
+ }
304502
+ parent.children.push({
304503
+ kind: "block",
304504
+ conditional: frame.closer.conditional,
304505
+ negated: frame.closer.negated,
304506
+ children: frame.children
304507
+ });
304508
+ } else {
304509
+ const top = stack[stack.length - 1];
304510
+ if (top) {
304511
+ top.children.push({ kind: "literal", text: fullTag });
304512
+ }
304513
+ }
304514
+ cursor = tagStart + fullTag.length;
304515
+ }
304516
+ if (cursor < template.length) {
304517
+ const top = stack[stack.length - 1];
304518
+ if (top) {
304519
+ top.children.push({ kind: "literal", text: template.slice(cursor) });
304520
+ }
304521
+ }
304522
+ if (stack.length !== 1) {
304523
+ const open = stack[stack.length - 1]?.closer?.conditional;
304524
+ throw unsupportedSyntaxError(`unclosed conditional block ${open ? `{{#${open}}}` : ""}`);
304525
+ }
304526
+ const root = stack[0];
304527
+ if (!root) {
304528
+ throw unsupportedSyntaxError("parser produced no root frame");
304529
+ }
304530
+ return root.children;
304531
+ }
304532
+ function renderBlocks(tokens, conditionals, resolved) {
304533
+ let out = "";
304534
+ for (const token of tokens) {
304535
+ if (token.kind === "literal") {
304536
+ out += token.text;
304537
+ continue;
304538
+ }
304539
+ const flag = conditionals[token.conditional] ?? false;
304540
+ resolved[token.conditional] = flag;
304541
+ const include = token.negated ? !flag : flag;
304542
+ if (include) {
304543
+ out += renderBlocks(token.children, conditionals, resolved);
304544
+ } else {
304545
+ renderBlocks(token.children, conditionals, resolved);
304546
+ }
304547
+ }
304548
+ return out;
304549
+ }
304550
+ var VARIABLE_TAG_REGEX = /\{\{\s*([^#^/>\s][^}]*?)\s*\}\}/g;
304551
+ function substituteVariables(text, variables) {
304552
+ const usedSet = /* @__PURE__ */ new Set();
304553
+ const missingSet = /* @__PURE__ */ new Set();
304554
+ const rendered = text.replace(VARIABLE_TAG_REGEX, (_match, captured) => {
304555
+ const name = captured.trim();
304556
+ const value = lookupVariable(name, variables);
304557
+ if (value === void 0) {
304558
+ missingSet.add(name);
304559
+ return `{{undefined:${name}}}`;
304560
+ }
304561
+ usedSet.add(name);
304562
+ return value;
304563
+ });
304564
+ return {
304565
+ rendered,
304566
+ used: [...usedSet],
304567
+ missing: [...missingSet]
304568
+ };
304569
+ }
304570
+ function renderMonitorTemplate(template, context) {
304571
+ const variables = context.variables ?? {};
304572
+ const conditionals = context.conditionals ?? {};
304573
+ const tree = parseBlocks(template);
304574
+ const conditionalsResolved = {};
304575
+ const afterConditionals = renderBlocks(tree, conditionals, conditionalsResolved);
304576
+ const { rendered, used, missing } = substituteVariables(afterConditionals, variables);
304577
+ return {
304578
+ rendered,
304579
+ variablesUsed: used,
304580
+ variablesMissing: missing,
304581
+ conditionalsResolved
304582
+ };
304583
+ }
304584
+
303918
304585
  // src/tools/monitors.ts
303919
304586
  var ActionSchema2 = external_exports.enum([
303920
304587
  "list",
@@ -303926,8 +304593,14 @@ var ActionSchema2 = external_exports.enum([
303926
304593
  "mute",
303927
304594
  "unmute",
303928
304595
  "top",
303929
- "history"
304596
+ "history",
304597
+ "preview",
304598
+ "test_notification"
303930
304599
  ]);
304600
+ var PreviewContextSchema = external_exports.object({
304601
+ variables: external_exports.record(external_exports.unknown()).optional(),
304602
+ conditionals: external_exports.record(external_exports.enum(SUPPORTED_CONDITIONALS), external_exports.boolean()).optional()
304603
+ }).optional().describe("Substitution context for monitors.preview (variables + conditionals).");
303931
304604
  var InputSchema2 = {
303932
304605
  action: ActionSchema2.describe("Action to perform"),
303933
304606
  id: external_exports.string().optional().describe("Monitor ID (required for get/update/delete/mute/unmute)"),
@@ -303939,8 +304612,14 @@ var InputSchema2 = {
303939
304612
  ),
303940
304613
  limit: external_exports.number().min(1).optional().describe("Maximum number of monitors to return (default: 50)"),
303941
304614
  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)"),
304615
+ message: external_exports.string().optional().describe(
304616
+ "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."
304617
+ ),
303943
304618
  end: external_exports.number().optional().describe("Mute end timestamp (for mute action)"),
304619
+ monitor_id: external_exports.number().optional().describe(
304620
+ "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."
304621
+ ),
304622
+ context: PreviewContextSchema,
303944
304623
  // Top action parameters
303945
304624
  from: external_exports.string().optional().describe('Start time (ISO 8601, relative like "1h", or Unix timestamp)'),
303946
304625
  to: external_exports.string().optional().describe('End time (ISO 8601, relative like "1h", or Unix timestamp)'),
@@ -303964,6 +304643,12 @@ var InputSchema2 = {
303964
304643
  ),
303965
304644
  group: external_exports.string().optional().describe(
303966
304645
  '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.'
304646
+ ),
304647
+ dry_run: external_exports.boolean().optional().describe(
304648
+ "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."
304649
+ ),
304650
+ timezone: external_exports.string().optional().describe(
304651
+ '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
304652
  )
303968
304653
  };
303969
304654
  var MonitorThresholdsSchema = external_exports.object({
@@ -304058,6 +304743,18 @@ function summarizeZodIssue(error2) {
304058
304743
  const expected = issue2.code === "invalid_type" && "expected" in issue2 ? `expected ${String(issue2.expected)}` : issue2.message;
304059
304744
  return `${path}: ${expected}`;
304060
304745
  }
304746
+ function annotateMonitorTimezone(monitor, tz) {
304747
+ const annotated = { ...monitor };
304748
+ if (monitor.created) {
304749
+ const ms = new Date(monitor.created).getTime();
304750
+ if (Number.isFinite(ms)) annotated.createdLocal = formatLocal(ms, tz);
304751
+ }
304752
+ if (monitor.modified) {
304753
+ const ms = new Date(monitor.modified).getTime();
304754
+ if (Number.isFinite(ms)) annotated.modifiedLocal = formatLocal(ms, tz);
304755
+ }
304756
+ return annotated;
304757
+ }
304061
304758
  function formatMonitor(m, site = "datadoghq.com") {
304062
304759
  const monitorId = m.id ?? 0;
304063
304760
  return {
@@ -304201,9 +304898,9 @@ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
304201
304898
  while (pageCount < maxPages && eventCount < maxEventsToProcess) {
304202
304899
  const pageBody = { ...body, page: { ...body.page, cursor } };
304203
304900
  const response = await eventsApi.searchEvents({ body: pageBody });
304204
- const events = response.data ?? [];
304205
- if (events.length === 0) break;
304206
- for (const event of events) {
304901
+ const events2 = response.data ?? [];
304902
+ if (events2.length === 0) break;
304903
+ for (const event of events2) {
304207
304904
  const transition = formatMonitorTransition(event);
304208
304905
  if (transition !== null) {
304209
304906
  transitions.push(transition);
@@ -304236,14 +304933,18 @@ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
304236
304933
  meta
304237
304934
  };
304238
304935
  }
304239
- async function listMonitors(api, params, limits, site) {
304936
+ async function listMonitors(api, params, limits, site, timezone) {
304937
+ if (timezone !== void 0) {
304938
+ validateIanaZone(timezone);
304939
+ }
304240
304940
  const effectiveLimit = params.limit ?? limits.defaultLimit;
304241
304941
  const response = await api.listMonitors({
304242
304942
  name: params.name,
304243
304943
  tags: params.tags?.join(","),
304244
304944
  groupStates: params.groupStates?.join(",")
304245
304945
  });
304246
- const monitors2 = response.slice(0, effectiveLimit).map((m) => formatMonitor(m, site));
304946
+ const baseMonitors = response.slice(0, effectiveLimit).map((m) => formatMonitor(m, site));
304947
+ const monitors2 = timezone !== void 0 ? baseMonitors.map((m) => annotateMonitorTimezone(m, timezone)) : baseMonitors;
304247
304948
  const statusCounts = {
304248
304949
  total: response.length,
304249
304950
  alert: response.filter((m) => m.overallState === "Alert").length,
@@ -304260,14 +304961,19 @@ async function listMonitors(api, params, limits, site) {
304260
304961
  )
304261
304962
  };
304262
304963
  }
304263
- async function getMonitor(api, id, site) {
304964
+ async function getMonitor(api, id, site, timezone) {
304965
+ if (timezone !== void 0) {
304966
+ validateIanaZone(timezone);
304967
+ }
304264
304968
  const monitorId = Number.parseInt(id, 10);
304265
304969
  if (Number.isNaN(monitorId)) {
304266
304970
  throw new Error(`Invalid monitor ID: ${id}`);
304267
304971
  }
304268
304972
  const monitor = await api.getMonitor({ monitorId });
304973
+ const baseDetail = formatMonitorDetail(monitor, site);
304974
+ const detail = timezone !== void 0 ? annotateMonitorTimezone(baseDetail, timezone) : baseDetail;
304269
304975
  return {
304270
- monitor: formatMonitorDetail(monitor, site),
304976
+ monitor: detail,
304271
304977
  datadog_url: buildMonitorUrl(monitorId, site)
304272
304978
  };
304273
304979
  }
@@ -304369,6 +305075,37 @@ async function createMonitor(api, config2, site = "datadoghq.com") {
304369
305075
  }
304370
305076
  return result;
304371
305077
  }
305078
+ async function dryRunMonitor(api, config2) {
305079
+ const normalized = normalizeMonitorConfig(config2);
305080
+ const body = normalized;
305081
+ await api.validateMonitor({ body });
305082
+ return {
305083
+ valid: true,
305084
+ dryRun: true,
305085
+ monitor: normalized
305086
+ };
305087
+ }
305088
+ async function previewMonitor(api, args, site = "datadoghq.com") {
305089
+ let template;
305090
+ let monitorId;
305091
+ if (args.inlineMessage !== void 0 && args.inlineMessage !== "") {
305092
+ template = args.inlineMessage;
305093
+ } else if (args.monitorIdSource !== void 0 && args.monitorIdSource !== "") {
305094
+ const loaded = await getMonitor(api, args.monitorIdSource, site);
305095
+ template = loaded.monitor.message ?? "";
305096
+ monitorId = loaded.monitor.id;
305097
+ } else {
305098
+ throw new Error(
305099
+ "Action 'preview' requires either an inline 'message' or a 'monitor_id' (or 'id') to load the template from."
305100
+ );
305101
+ }
305102
+ const result = renderMonitorTemplate(template, args.context ?? {});
305103
+ return monitorId !== void 0 ? { ...result, monitorId } : result;
305104
+ }
305105
+ 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/";
305106
+ function testNotificationMonitor(_args) {
305107
+ throw new McpError(ErrorCode.InvalidRequest, TEST_NOTIFICATION_NOT_SUPPORTED_MESSAGE);
305108
+ }
304372
305109
  async function updateMonitor(api, id, config2, site = "datadoghq.com") {
304373
305110
  const monitorId = Number.parseInt(id, 10);
304374
305111
  const normalized = normalizeMonitorConfig(config2, true);
@@ -304453,7 +305190,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304453
305190
  }
304454
305191
  });
304455
305192
  const rawEvents = searchResponse.data ?? [];
304456
- const events = rawEvents.map(formatEventV2);
305193
+ const events2 = rawEvents.map(formatEventV2);
304457
305194
  const contextPrefixes = new Set(
304458
305195
  params.contextTags ?? [
304459
305196
  "queue",
@@ -304465,7 +305202,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304465
305202
  ]
304466
305203
  );
304467
305204
  const monitorGroups = /* @__PURE__ */ new Map();
304468
- for (const event of events) {
305205
+ for (const event of events2) {
304469
305206
  const monitorId = event.monitorId;
304470
305207
  if (typeof monitorId !== "number") continue;
304471
305208
  let group = monitorGroups.get(monitorId);
@@ -304534,7 +305271,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304534
305271
  from: fromTime,
304535
305272
  to: toTime,
304536
305273
  totalMonitors: monitorGroups.size,
304537
- totalEvents: events.length,
305274
+ totalEvents: events2.length,
304538
305275
  contextPrefixes: Array.from(contextPrefixes),
304539
305276
  datadog_url: buildEventsUrl(query, validFrom, validTo, site)
304540
305277
  }
@@ -304543,7 +305280,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
304543
305280
  function registerMonitorsTool(server, api, eventsApi, limits, readOnly = false, site = "datadoghq.com") {
304544
305281
  server.tool(
304545
305282
  "monitors",
304546
- `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history.
305283
+ `Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history, preview, test_notification.
304547
305284
  Filters: name, tags, groupStates (alert/warn/ok/no data).
304548
305285
  get/create/update return the full options object so callers can safely read-then-patch.
304549
305286
 
@@ -304581,6 +305318,21 @@ history: Count and list real state transitions for one monitor over a time windo
304581
305318
  transition_type filter that excludes renotifies by default. To include renotifies, pass
304582
305319
  transitionType including "renotify".
304583
305320
 
305321
+ preview: Render a Datadog monitor message template against a context (read-only safe).
305322
+ - Inputs: either inline 'message' OR 'monitor_id' (or existing 'id'); plus optional 'context' { variables, conditionals }.
305323
+ - Supported syntax: {{variable.name}} substitution and conditional blocks {{#name}}...{{/name}} / {{^name}}...{{/name}}
305324
+ where name is one of: ${SUPPORTED_CONDITIONALS.join(", ")}.
305325
+ - Missing variables render as {{undefined:name}} markers and are reported in 'variablesMissing'.
305326
+ - Loops ({{#each ...}}) and partials ({{> ...}}) return EUNSUPPORTED_TEMPLATE_SYNTAX.
305327
+ - Allowed under --read-only (no mutation; at most a getMonitor load).
305328
+
305329
+ test_notification: KNOWN LIMITATION \u2014 always returns ENOT_SUPPORTED.
305330
+ - Datadog's public REST API exposes no monitor test-notification endpoint at v1 or v2
305331
+ (audited against the official OpenAPI specs). The v1 SDK has no notifyMonitor / testMonitor method.
305332
+ - Allowed under --read-only because no Datadog HTTP call is attempted.
305333
+ - If Datadog publishes such an endpoint in future, this action will be reimplemented to invoke it.
305334
+ - Workaround: use the 'Test Notifications' button in the Datadog monitor UI.
305335
+
304584
305336
  For generic event grouping (deployments, configs), use events tool instead. Note that the
304585
305337
  events tool's action=search with source:alert ALSO includes renotifies; use its
304586
305338
  transitionType filter (or this action=history) for fires-only counts.`,
@@ -304594,24 +305346,32 @@ transitionType filter (or this action=history) for fires-only counts.`,
304594
305346
  groupStates,
304595
305347
  limit,
304596
305348
  config: config2,
305349
+ message,
304597
305350
  end,
304598
305351
  from,
304599
305352
  to,
304600
305353
  contextTags,
304601
305354
  maxEvents,
304602
305355
  transitionType,
304603
- group
305356
+ group,
305357
+ dry_run: dryRun,
305358
+ monitor_id: monitorIdNum,
305359
+ context,
305360
+ timezone
304604
305361
  }) => {
304605
305362
  try {
304606
- checkReadOnly(action, readOnly);
305363
+ const isDryRunCreate = action === "create" && dryRun === true;
305364
+ if (!isDryRunCreate) {
305365
+ checkReadOnly(action, readOnly);
305366
+ }
304607
305367
  switch (action) {
304608
305368
  case "list":
304609
305369
  return toolResult(
304610
- await listMonitors(api, { name, tags, groupStates, limit }, limits, site)
305370
+ await listMonitors(api, { name, tags, groupStates, limit }, limits, site, timezone)
304611
305371
  );
304612
305372
  case "get": {
304613
305373
  const monitorId = requireParam(id, "id", "get");
304614
- return toolResult(await getMonitor(api, monitorId, site));
305374
+ return toolResult(await getMonitor(api, monitorId, site, timezone));
304615
305375
  }
304616
305376
  case "search": {
304617
305377
  const searchQuery = requireParam(query, "query", "search");
@@ -304619,6 +305379,9 @@ transitionType filter (or this action=history) for fires-only counts.`,
304619
305379
  }
304620
305380
  case "create": {
304621
305381
  const monitorConfig = requireParam(config2, "config", "create");
305382
+ if (dryRun) {
305383
+ return toolResult(await dryRunMonitor(api, monitorConfig));
305384
+ }
304622
305385
  return toolResult(await createMonitor(api, monitorConfig, site));
304623
305386
  }
304624
305387
  case "update": {
@@ -304671,6 +305434,24 @@ transitionType filter (or this action=history) for fires-only counts.`,
304671
305434
  )
304672
305435
  );
304673
305436
  }
305437
+ case "preview": {
305438
+ const monitorIdSource = monitorIdNum !== void 0 ? String(monitorIdNum) : id ?? void 0;
305439
+ return toolResult(
305440
+ await previewMonitor(
305441
+ api,
305442
+ {
305443
+ inlineMessage: message,
305444
+ monitorIdSource,
305445
+ context
305446
+ },
305447
+ site
305448
+ )
305449
+ );
305450
+ }
305451
+ case "test_notification": {
305452
+ const monitorIdSource = monitorIdNum !== void 0 ? String(monitorIdNum) : id ?? void 0;
305453
+ return testNotificationMonitor({ monitorId: monitorIdSource });
305454
+ }
304674
305455
  default:
304675
305456
  throw new Error(`Unknown action: ${action}`);
304676
305457
  }
@@ -305287,6 +306068,56 @@ var InputSchema5 = {
305287
306068
  "Maximum data points per timeseries (for query action). AI controls resolution vs token usage (default: 1000)."
305288
306069
  )
305289
306070
  };
306071
+ var ROLLUP_METHODS = /* @__PURE__ */ new Set(["avg", "max", "min", "sum", "count"]);
306072
+ var DEFAULT_ROLLUP_METHOD = "avg";
306073
+ function parseRollupFromQuery(query) {
306074
+ if (typeof query !== "string") return null;
306075
+ const matches = [...query.matchAll(/\.rollup\(\s*([^)]*?)\s*\)/g)];
306076
+ const lastMatch = matches[matches.length - 1];
306077
+ if (lastMatch === void 0) return null;
306078
+ const inner = lastMatch[1];
306079
+ if (inner === void 0 || inner.length === 0) return null;
306080
+ const parts = inner.split(",").map((p) => p.trim());
306081
+ if (parts.length === 1) {
306082
+ const raw = parts[0] ?? "";
306083
+ const interval = Number.parseInt(raw, 10);
306084
+ if (!Number.isFinite(interval) || interval <= 0 || String(interval) !== raw) {
306085
+ return null;
306086
+ }
306087
+ return { interval, method: DEFAULT_ROLLUP_METHOD, methodInferred: true };
306088
+ }
306089
+ if (parts.length === 2) {
306090
+ const method = parts[0] ?? "";
306091
+ const secondsRaw = parts[1] ?? "";
306092
+ if (!ROLLUP_METHODS.has(method)) return null;
306093
+ const interval = Number.parseInt(secondsRaw, 10);
306094
+ if (!Number.isFinite(interval) || interval <= 0 || String(interval) !== secondsRaw) {
306095
+ return null;
306096
+ }
306097
+ return { interval, method, methodInferred: false };
306098
+ }
306099
+ return null;
306100
+ }
306101
+ function computeEffectiveRollup(series) {
306102
+ const intervals = [];
306103
+ for (const s of series) {
306104
+ const pts = s.pointlist ?? [];
306105
+ if (pts.length < 2) continue;
306106
+ const first = pts[0];
306107
+ const second = pts[1];
306108
+ if (first === void 0 || second === void 0) continue;
306109
+ const deltaMs = (second[0] ?? 0) - (first[0] ?? 0);
306110
+ if (deltaMs <= 0) continue;
306111
+ intervals.push(Math.round(deltaMs / 1e3));
306112
+ }
306113
+ const primary = intervals[0];
306114
+ if (primary === void 0) return null;
306115
+ const unique = Array.from(new Set(intervals)).sort((a, b) => a - b);
306116
+ if (unique.length === 1) {
306117
+ return { interval: primary };
306118
+ }
306119
+ return { interval: primary, intervalsObserved: unique };
306120
+ }
305290
306121
  async function queryMetrics(api, params, limits, site) {
305291
306122
  const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
305292
306123
  const defaultTo = now();
@@ -305299,7 +306130,8 @@ async function queryMetrics(api, params, limits, site) {
305299
306130
  to: toTs,
305300
306131
  query: params.query
305301
306132
  });
305302
- const series = (response.series ?? []).map((s) => ({
306133
+ const rawSeries = response.series ?? [];
306134
+ const series = rawSeries.map((s) => ({
305303
306135
  metric: s.metric ?? "",
305304
306136
  points: (s.pointlist ?? []).slice(0, params.pointLimit ?? limits.defaultMetricDataPoints).map((p) => ({
305305
306137
  timestamp: p[0] ?? 0,
@@ -305308,6 +306140,13 @@ async function queryMetrics(api, params, limits, site) {
305308
306140
  scope: s.scope ?? "",
305309
306141
  tags: s.tagSet ?? []
305310
306142
  }));
306143
+ const rollupRequested = parseRollupFromQuery(params.query);
306144
+ const rollupEffective = computeEffectiveRollup(
306145
+ rawSeries.map((s) => ({
306146
+ pointlist: s.pointlist ?? []
306147
+ }))
306148
+ );
306149
+ const rollupOverridden = rollupRequested !== null && rollupEffective !== null && (rollupEffective.interval !== rollupRequested.interval || (rollupEffective.intervalsObserved?.some((i) => i !== rollupRequested.interval) ?? false));
305311
306150
  return {
305312
306151
  series,
305313
306152
  meta: {
@@ -305315,7 +306154,10 @@ async function queryMetrics(api, params, limits, site) {
305315
306154
  from: new Date(fromTs * 1e3).toISOString(),
305316
306155
  to: new Date(toTs * 1e3).toISOString(),
305317
306156
  seriesCount: series.length,
305318
- datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site)
306157
+ datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site),
306158
+ rollupRequested,
306159
+ rollupEffective,
306160
+ rollupOverridden
305319
306161
  }
305320
306162
  };
305321
306163
  }
@@ -305995,10 +306837,11 @@ var InputSchema8 = {
305995
306837
  from: external_exports.string().optional().describe('Start time for history (ISO 8601 or relative like "7d", "1w")'),
305996
306838
  to: external_exports.string().optional().describe("End time for history (ISO 8601 or relative, default: now)")
305997
306839
  };
305998
- function formatSlo(s) {
306840
+ function formatSlo(s, site = "datadoghq.com") {
305999
306841
  const primaryThreshold = s.thresholds?.[0];
306842
+ const id = s.id ?? "";
306000
306843
  const summary = {
306001
- id: s.id ?? "",
306844
+ id,
306002
306845
  name: s.name ?? "",
306003
306846
  description: s.description ?? null,
306004
306847
  type: String(s.type ?? "unknown"),
@@ -306013,7 +306856,8 @@ function formatSlo(s) {
306013
306856
  },
306014
306857
  overallStatus: [],
306015
306858
  createdAt: s.createdAt ? new Date(s.createdAt * 1e3).toISOString() : "",
306016
- modifiedAt: s.modifiedAt ? new Date(s.modifiedAt * 1e3).toISOString() : ""
306859
+ modifiedAt: s.modifiedAt ? new Date(s.modifiedAt * 1e3).toISOString() : "",
306860
+ url: id ? buildSloUrl(id, site) : ""
306017
306861
  };
306018
306862
  if (s.query?.numerator && s.query.denominator) {
306019
306863
  summary.query = { numerator: s.query.numerator, denominator: s.query.denominator };
@@ -306029,11 +306873,12 @@ function formatSlo(s) {
306029
306873
  }
306030
306874
  return summary;
306031
306875
  }
306032
- function formatSearchSlo(slo) {
306876
+ function formatSearchSlo(slo, site = "datadoghq.com") {
306033
306877
  const attrs = slo.data?.attributes;
306034
306878
  const primaryThreshold = attrs?.thresholds?.[0];
306879
+ const id = slo.data?.id ?? "";
306035
306880
  const summary = {
306036
- id: slo.data?.id ?? "",
306881
+ id,
306037
306882
  name: attrs?.name ?? "",
306038
306883
  description: attrs?.description ?? null,
306039
306884
  type: String(attrs?.sloType ?? "unknown"),
@@ -306054,7 +306899,8 @@ function formatSearchSlo(slo) {
306054
306899
  timeframe: String(os.timeframe ?? "")
306055
306900
  })),
306056
306901
  createdAt: attrs?.createdAt ? new Date(attrs.createdAt * 1e3).toISOString() : "",
306057
- modifiedAt: attrs?.modifiedAt ? new Date(attrs.modifiedAt * 1e3).toISOString() : ""
306902
+ modifiedAt: attrs?.modifiedAt ? new Date(attrs.modifiedAt * 1e3).toISOString() : "",
306903
+ url: id ? buildSloUrl(id, site) : ""
306058
306904
  };
306059
306905
  if (attrs?.query?.numerator && attrs.query.denominator) {
306060
306906
  summary.query = { numerator: attrs.query.numerator, denominator: attrs.query.denominator };
@@ -306073,14 +306919,14 @@ function buildSearchQuery(query, tags) {
306073
306919
  if (tags?.length) parts.push(...tags);
306074
306920
  return parts.join(" ");
306075
306921
  }
306076
- async function listSlos(api, params, limits) {
306922
+ async function listSlos(api, params, limits, site = "datadoghq.com") {
306077
306923
  const effectiveLimit = params.limit ?? limits.defaultLimit;
306078
306924
  if (params.ids?.length) {
306079
306925
  const response2 = await api.listSLOs({
306080
306926
  ids: params.ids.join(","),
306081
306927
  limit: effectiveLimit
306082
306928
  });
306083
- const slos3 = (response2.data ?? []).map(formatSlo);
306929
+ const slos3 = (response2.data ?? []).map((s) => formatSlo(s, site));
306084
306930
  return { slos: slos3, total: slos3.length };
306085
306931
  }
306086
306932
  const searchQuery = buildSearchQuery(params.query, params.tags);
@@ -306089,16 +306935,16 @@ async function listSlos(api, params, limits) {
306089
306935
  pageSize: effectiveLimit
306090
306936
  });
306091
306937
  const searchSlos = response.data?.attributes?.slos ?? [];
306092
- const slos2 = searchSlos.map(formatSearchSlo);
306938
+ const slos2 = searchSlos.map((s) => formatSearchSlo(s, site));
306093
306939
  return {
306094
306940
  slos: slos2,
306095
306941
  total: slos2.length
306096
306942
  };
306097
306943
  }
306098
- async function getSlo(api, id) {
306944
+ async function getSlo(api, id, site = "datadoghq.com") {
306099
306945
  const response = await api.getSLO({ sloId: id });
306100
306946
  return {
306101
- slo: response.data ? formatSlo(response.data) : null
306947
+ slo: response.data ? formatSlo(response.data, site) : null
306102
306948
  };
306103
306949
  }
306104
306950
  function normalizeSloConfig(config2) {
@@ -306114,20 +306960,20 @@ function normalizeSloConfig(config2) {
306114
306960
  }
306115
306961
  return normalized;
306116
306962
  }
306117
- async function createSlo(api, config2) {
306963
+ async function createSlo(api, config2, site = "datadoghq.com") {
306118
306964
  const body = normalizeSloConfig(config2);
306119
306965
  const response = await api.createSLO({ body });
306120
306966
  return {
306121
306967
  success: true,
306122
- slo: response.data?.[0] ? formatSlo(response.data[0]) : null
306968
+ slo: response.data?.[0] ? formatSlo(response.data[0], site) : null
306123
306969
  };
306124
306970
  }
306125
- async function updateSlo(api, id, config2) {
306971
+ async function updateSlo(api, id, config2, site = "datadoghq.com") {
306126
306972
  const body = normalizeConfigKeys(config2);
306127
306973
  const response = await api.updateSLO({ sloId: id, body });
306128
306974
  return {
306129
306975
  success: true,
306130
- slo: response.data?.[0] ? formatSlo(response.data[0]) : null
306976
+ slo: response.data?.[0] ? formatSlo(response.data[0], site) : null
306131
306977
  };
306132
306978
  }
306133
306979
  async function deleteSlo(api, id) {
@@ -306167,29 +307013,29 @@ async function getSloHistory(api, id, params) {
306167
307013
  }
306168
307014
  };
306169
307015
  }
306170
- function registerSlosTool(server, api, limits, readOnly = false, _site = "datadoghq.com") {
307016
+ function registerSlosTool(server, api, limits, readOnly = false, site = "datadoghq.com") {
306171
307017
  server.tool(
306172
307018
  "slos",
306173
- "Manage Datadog Service Level Objectives. Actions: list (with SLI status & error budget), get, create, update, delete, history. SLO types: metric-based, monitor-based. Use for: reliability tracking, error budgets, SLA compliance, performance targets.",
307019
+ "Manage Datadog Service Level Objectives. Actions: list (with SLI status & error budget), get, create, update, delete, history. SLO types: metric-based, monitor-based. Each list/get/create/update response includes a `url` field deep-linking to the Datadog UI. Use for: reliability tracking, error budgets, SLA compliance, performance targets.",
306174
307020
  InputSchema8,
306175
307021
  async ({ action, id, ids, query, tags, limit, config: config2, from, to }) => {
306176
307022
  try {
306177
307023
  checkReadOnly(action, readOnly);
306178
307024
  switch (action) {
306179
307025
  case "list":
306180
- return toolResult(await listSlos(api, { ids, query, tags, limit }, limits));
307026
+ return toolResult(await listSlos(api, { ids, query, tags, limit }, limits, site));
306181
307027
  case "get": {
306182
307028
  const sloId = requireParam(id, "id", "get");
306183
- return toolResult(await getSlo(api, sloId));
307029
+ return toolResult(await getSlo(api, sloId, site));
306184
307030
  }
306185
307031
  case "create": {
306186
307032
  const sloConfig = requireParam(config2, "config", "create");
306187
- return toolResult(await createSlo(api, sloConfig));
307033
+ return toolResult(await createSlo(api, sloConfig, site));
306188
307034
  }
306189
307035
  case "update": {
306190
307036
  const sloId = requireParam(id, "id", "update");
306191
307037
  const sloConfig = requireParam(config2, "config", "update");
306192
- return toolResult(await updateSlo(api, sloId, sloConfig));
307038
+ return toolResult(await updateSlo(api, sloId, sloConfig, site));
306193
307039
  }
306194
307040
  case "delete": {
306195
307041
  const sloId = requireParam(id, "id", "delete");
@@ -306856,11 +307702,11 @@ async function searchEvents(api, params, limits, site) {
306856
307702
  sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
306857
307703
  pageLimit: params.limit ?? limits.defaultLimit
306858
307704
  });
306859
- const events = (response.data ?? []).map(formatEvent);
307705
+ const events2 = (response.data ?? []).map(formatEvent);
306860
307706
  return {
306861
- events,
307707
+ events: events2,
306862
307708
  meta: {
306863
- totalCount: events.length,
307709
+ totalCount: events2.length,
306864
307710
  timeRange: {
306865
307711
  from: new Date(fromTime * 1e3).toISOString(),
306866
307712
  to: new Date(toTime * 1e3).toISOString()
@@ -307073,19 +307919,19 @@ async function getSessionWaterfall(api, params, limits, site) {
307073
307919
  sort: "timestamp",
307074
307920
  pageLimit: limits.defaultLimit
307075
307921
  });
307076
- const events = (response.data ?? []).map(formatWaterfallEvent);
307922
+ const events2 = (response.data ?? []).map(formatWaterfallEvent);
307077
307923
  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
307924
+ views: events2.filter((e) => e.type === "view").length,
307925
+ resources: events2.filter((e) => e.type === "resource").length,
307926
+ actions: events2.filter((e) => e.type === "action").length,
307927
+ errors: events2.filter((e) => e.type === "error").length,
307928
+ longTasks: events2.filter((e) => e.type === "long_task").length
307083
307929
  };
307084
307930
  return {
307085
- events,
307931
+ events: events2,
307086
307932
  summary,
307087
307933
  meta: {
307088
- totalCount: events.length,
307934
+ totalCount: events2.length,
307089
307935
  applicationId: params.applicationId,
307090
307936
  sessionId: params.sessionId,
307091
307937
  viewId: params.viewId ?? null,
@@ -308265,6 +309111,17 @@ var dashboards = {
308265
309111
  docsUrl: "https://docs.datadoghq.com/api/latest/dashboards/"
308266
309112
  };
308267
309113
 
309114
+ // src/schema/events.ts
309115
+ var events = {
309116
+ /**
309117
+ * Diagnostic codes emitted on zero-result `events.search` responses.
309118
+ * Each code is also documented inline in `src/tools/events.ts` next to the
309119
+ * heuristic that produces it.
309120
+ */
309121
+ diagnosticCodes: ["UNINDEXED_TAG_PREFIX", "NARROW_TIME_RANGE", "RESTRICTIVE_SOURCE_FILTER"],
309122
+ docsUrl: "https://docs.datadoghq.com/api/latest/events/"
309123
+ };
309124
+
308268
309125
  // src/schema/metrics.ts
308269
309126
  var metrics = {
308270
309127
  aggregators: ["avg", "max", "min", "sum", "count"],
@@ -308325,14 +309182,14 @@ var slos = {
308325
309182
  };
308326
309183
 
308327
309184
  // src/schema/index.ts
308328
- var schemas = { dashboards, metrics, monitors, slos };
309185
+ var schemas = { dashboards, events, metrics, monitors, slos };
308329
309186
  var schemaResources = Object.keys(schemas);
308330
309187
 
308331
309188
  // src/tools/schema.ts
308332
309189
  var ResourceSchema2 = external_exports.enum(schemaResources);
308333
309190
  var InputSchema20 = {
308334
309191
  resource: ResourceSchema2.describe(
308335
- "Datadog resource type to get schema for: dashboards, metrics, monitors, slos"
309192
+ "Datadog resource type to get schema for: dashboards, events, metrics, monitors, slos"
308336
309193
  )
308337
309194
  };
308338
309195
  function getSchema(resource) {