feat(datadog): extend to 40 tools and align every operation with the published OpenAPI specs (#6745)

* feat(datadog): add incidents, SLOs, dashboards, synthetics, Cloud SIEM, and APM tools

Extends the Datadog block from 12 to 39 operations, all verified against
Datadog's published OpenAPI specs:

- Incidents (v2, public beta): list, get, create, update, add todo
- SLOs (v1): list, get, create, update, delete, history
- Dashboards (v1): list, get, create, delete
- Synthetics (v1): list tests, get test, latest results, trigger, pause/resume
- Cloud SIEM (v2): search signals, get signal, update triage state, assign,
  list detection rules
- APM: search spans (v2), list Service Catalog definitions (v2)

Adds tools/datadog/utils.ts so every tool builds its URL from the configured
site/region and shares the JSON:API-aware error extraction, and handles the
v1 flat vs v2 envelope shapes and cursor pagination per endpoint.

* fix(datadog): align every operation with the published OpenAPI specs

Validated all 39 shipped operations (plus the 12 pre-existing ones that had
never been spec-checked) against the DataDog v1 and v2 OpenAPI schemas.

- `POST /api/v2/downtime` requires `monitor_identifier`, so a downtime created
  without a monitor id was rejected. Default to the `*` monitor tag.
- A one-time downtime schedule declares `additionalProperties: false` and
  accepts only `start`/`end`; the timezone moves to `display_timezone`.
- `GET /api/v2/downtime` has no `monitor_id` filter, and the response carries
  no `disabled` attribute. Downtime ids are UUID strings, not numbers.
- Drop scaffold types for operations that do not exist (metric metadata, event
  query, monitor update/delete/unmute, host listing) along with their fields.
- Note that monitor mute is no longer published in the v1 specification.
- Add browser Synthetic test results, which the browser-specific endpoint
  returns with its own camelCase step-count shape.
- Replace every `any` with a spec-derived interface, keeping the polymorphic
  service-definition schema opaque.

* fix(datadog): remove remaining any types and declare every returned output field

Replace the six surviving `Record<string, any>` request-body and response-cast
sites with concrete spec-derived shapes, and declare the output fields that
transformResponse already returned but outputs omitted:

- create_downtime / list_downtimes: timezone, created, modified
- create_monitor / get_monitor: options, creator
- list_monitors: message, priority, options, created, modified, creator
- query_logs: content.attributes, content.tags
- update_security_signal_state / _assignee: type; assignee also gained the
  archiveReason/archiveComment pair its sibling already declared
- query_timeseries: series gained the items shape it never described

* fix(datadog): stop dropping downtime targeting inputs in the block mapping

create_downtime accepts monitorTags, timezone and muteFirstRecoveryNotification,
but the block exposed no inputs for them and never forwarded them. Monitor-tag
targeting silently fell back to the `*` tag, so a downtime meant for one team's
monitors muted every monitor in scope. Adds the three advanced sub-blocks and
wires them through.

Also routes list_downtimes' currentOnly through toSwitchBoolean. A switch yields
the strings 'true'/'false', and 'false' is truthy, so turning the toggle off
still sent current_only=true. Every other switch in the block already used the
helper; this was the last raw one.

* fix(datadog): correct metric type codes, stop SLO update data loss, drop unpublished mute

Independent re-validation of all 39 operations against the DataDog/datadog-api-client-go
generator specs (v1 and v2 openapi.yaml) rather than the client-rendered docs site.

Correctness:
- submit_metrics sent inverted MetricIntakeType codes (gauge as 0/unspecified, rate as 1/count,
  count as 2/rate), silently changing how Datadog aggregated every submitted series. The spec
  enum is 0 unspecified, 1 count, 2 rate, 3 gauge; an unrecognized type is now omitted so
  Datadog infers it. Also stops stamping an invented `resources: [{name:'host'}]` default and
  now forwards `interval`, which Datadog requires for count and rate metrics.
- update_slo replaced the whole SLO with only the fields the caller filled in, so editing one
  field erased description, tags, query, monitor_ids, groups, thresholds, and timeframe.
  PUT /api/v1/slo/{slo_id} is a full replacement, so the stored SLO is now read first and the
  supplied edits are overlaid onto it, with the read-only fields stripped.
- update_incident admitted empty strings, so a blank input could blank a stored incident title
  or fail as an invalid date-time.
- query_timeseries reported a failed query as success: Datadog returns 200 with a non-ok
  `status` and the reason in `error`.
- create_monitor swallowed malformed options JSON and created a monitor with no thresholds.
- send_logs rebuilt each entry from a fixed field list, discarding the custom attributes
  Datadog accepts as additionalProperties, and padded absent optional fields with empty strings.

Removed:
- mute_monitor. /api/v1/monitor/{monitor_id}/mute is absent from the v1 spec entirely, there is
  no unmute counterpart to reverse it, and downtimes are the supported mechanism.

Contract accuracy:
- Security signal search advertised relative times ("now-1h"); the spec types filter.from/to as
  format: date-time. Descriptions, placeholders, and wand prompts now produce ISO-8601.
- list_incidents advertised an `include` value ("integrations") that is not in the spec enum,
  and neither incident tool trimmed the comma-separated list, so "users, attachments" 400d.
- Invalid "ok" group state dropped from both monitor descriptions.
- time_slice removed from SLO create input, which cannot build one without an SLI specification.
- DatadogSite gains ap2, uk1, and us2.ddog-gov.com.

Pagination and errors:
- list_downtimes silently truncated at Datadog's default 30 with no way to page; adds
  page[limit]/page[offset] and surfaces totalCount.
- query_logs returned a cursor it had no way to accept back.
- Error extraction consolidated onto datadogErrorMessage, which now also reads the
  dictionary-shaped errors of the SLO delete conflict. Ten tools were reading `.detail` off
  plain strings or the raw entry off objects, degrading every failure to a bare status line.
- Debug logging removed from list_monitors.

Adds 29 regression tests, each verified to fail when its fix is reverted.

* fix(datadog): add SEV-0, document page-size caps, drop unsourced output defaults

- The severity dropdown omitted SEV-0, which IncidentSeverity allows and both incident
  tool descriptions already advertised.
- Page-size descriptions now state Datadog's documented default of 10 and cap of 100
  instead of an arbitrary example, so an agent does not request an out-of-range page.
- trigger_synthetics_tests emitted an explicit null for a string-typed optional output,
  and update_synthetics_status reported 'live' on the error path regardless of what the
  caller actually requested.

* fix(datadog): keep mute_monitor and add the missing unmute counterpart

Reverses the removal in the previous commit. Absence from the datadog-api-client-go
generator spec showed the endpoint is unpublished there, not that it is retired:
Datadog's official Python client still implements it on master as
`Monitor.mute(id, scope=, end=)` and `Monitor.unmute(id, scope=, all_scopes=)`
(datadogpy datadog/api/monitors.py), which `_trigger_class_action` resolves to
`POST /api/v1/monitor/{id}/mute` and `/unmute` with exactly those body fields.

mute_monitor has also been in the block since #2175 in December, so dropping it would
have broken existing workflows for an endpoint that two independent sources agree is live.

The genuine defect was that muting was a one-way trapdoor: Sim could mute a monitor but
had no way to reverse it. Adds datadog_unmute_monitor, sharing the monitor ID and scope
inputs with mute, so the operation is recoverable from the same block.

Also: mute no longer discards the response body (it now reports the monitor id, name, and
state), routes errors through datadogErrorMessage, encodes the monitor ID in the path, and
stops dropping an explicit `end` of 0.

* fix(datadog): make downtime targeting explicit and reach downtime pagination from the block

Addresses the review findings on the previous round.

- create_downtime accepted both a monitor ID and monitor tags but `monitor_identifier` is a
  oneOf, so it silently kept the ID and dropped the tags, muting a different set of monitors
  than the caller asked for. It now rejects the ambiguous combination.
- create_downtime ran Number.parseInt on the monitor ID with no validation, so a non-numeric
  value became NaN and serialized as null inside monitor_identifier. It now uses the same
  parseMonitorIds guard the SLO path already had, naming the offending value.
- list_downtimes gained limit/offset in the tool but the block exposed neither, so no
  block-driven call could page past Datadog's default. Adds the two sub-blocks and wires them
  through the params mapper.
- The block did not declare the totalCount the tool now returns, so nothing downstream could
  bind to it.

* fix(datadog): tolerate non-string list inputs and keep the shipped mute subblock ids

Both defects were introduced by this branch.

- splitCommaList called .split on its argument, so routing create_downtime's monitorId
  through it turned a legitimate numeric input into a TypeError before the request was
  built. A <Block.output> reference to get_monitor or list_monitors resolves to a number,
  and an LLM tool call can pass a number or an array, so the helper now normalizes all
  three shapes. The previous Number.parseInt path had accepted a number by coercion.
- Adding the unmute operation renamed the mute subblock ids scope/end to muteScope/muteEnd.
  Workflow state is persisted by subblock id, so every existing Mute Monitor block would
  have kept the old keys and silently lost its scope and end time. Restored the shipped ids;
  both are still unique block-wide and no operation reads another operation's value.

* fix(datadog): compare downtime targets after parsing, not before

A whitespace-only Monitor ID is truthy as a raw string but parses to no monitor, so
the oneOf conflict guard rejected a valid tag-targeted downtime whenever the untouched
Monitor ID field carried blank text. Both sides are now compared after parsing.
This commit is contained in:
Waleed
2026-08-15 18:48:46 -07:00
committed by GitHub
parent be20df9257
commit cabd2e2fc1
52 changed files with 8202 additions and 519 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+118 -2
View File
@@ -4933,7 +4933,11 @@
},
{
"name": "Mute Monitor",
"description": "Mute a monitor to temporarily suppress notifications."
"description": "Mute a monitor to temporarily suppress its notifications. Use Unmute Monitor to reverse it, or schedule a downtime instead when you want a planned, auditable maintenance window."
},
{
"name": "Unmute Monitor",
"description": "Unmute a monitor so it resumes sending notifications. Reverses Mute Monitor, either for one scope or for every scope at once."
},
{
"name": "Query Logs",
@@ -4954,9 +4958,121 @@
{
"name": "Cancel Downtime",
"description": "Cancel a scheduled downtime."
},
{
"name": "List Incidents",
"description": "List incidents for the organization. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta."
},
{
"name": "Get Incident",
"description": "Get the details of a single incident by ID. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta."
},
{
"name": "Create Incident",
"description": "Declare a new incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta."
},
{
"name": "Update Incident",
"description": "Partially update an existing incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta."
},
{
"name": "Add Incident Todo",
"description": "Add a follow-up task (todo) to an incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta."
},
{
"name": "List SLOs",
"description": "List service level objectives, optionally filtered by IDs, name, tags, or underlying metrics query."
},
{
"name": "Get SLO",
"description": "Get the configuration of a single service level objective by ID."
},
{
"name": "Create SLO",
"description": "Create a service level objective from a metric query, monitors, or a time-slice condition."
},
{
"name": "Update SLO",
"description": "Update a service level objective. Reads the current SLO first and applies only the fields you supply, so anything left blank keeps its stored value."
},
{
"name": "Delete SLO",
"description": "Permanently delete a service level objective. Datadog returns a conflict when the SLO is still referenced by a dashboard."
},
{
"name": "Get SLO History",
"description": "Get an SLOs history over a time window, including the overall SLI value and remaining error budget."
},
{
"name": "List Dashboards",
"description": "List custom created or cloned dashboards. Datadog preset dashboards are not returned."
},
{
"name": "Get Dashboard",
"description": "Get the full definition of a dashboard, including its widgets."
},
{
"name": "Create Dashboard",
"description": "Create a dashboard from a title, layout type, and widget definitions."
},
{
"name": "Delete Dashboard",
"description": "Delete a dashboard by ID."
},
{
"name": "List Synthetic Tests",
"description": "List all Synthetic tests (API, browser, and mobile) with their current status."
},
{
"name": "Get Synthetic Test",
"description": "Get the configuration of a Synthetic test by public ID. Browser test steps are not included by this type-agnostic endpoint."
},
{
"name": "Get Synthetic Test Results",
"description": "Get the latest result summaries (up to the last 150 runs) for a Synthetic API test."
},
{
"name": "Get Browser Synthetic Test Results",
"description": "Get the latest result summaries (up to the last 150 runs) for a Synthetic browser test, including step counts and errors."
},
{
"name": "Trigger Synthetic Tests",
"description": "Trigger an immediate run of one or more Synthetic tests by public ID."
},
{
"name": "Pause Or Start Synthetic Test",
"description": "Pause or resume a Synthetic test by setting its status to \"paused\" or \"live\"."
},
{
"name": "List Security Signals",
"description": "Search Cloud SIEM security signals by query and time range. Requires the `security_monitoring_signals_read` permission."
},
{
"name": "Get Security Signal",
"description": "Get the details of a single Cloud SIEM security signal. Requires the `security_monitoring_signals_read` permission."
},
{
"name": "Update Security Signal State",
"description": "Change the triage state of a Cloud SIEM security signal to open, under_review, or archived. Requires the `security_monitoring_signals_write` permission."
},
{
"name": "Assign Security Signal",
"description": "Assign a Cloud SIEM security signal to a Datadog user by UUID. Requires the `security_monitoring_signals_write` permission."
},
{
"name": "List Security Rules",
"description": "List Cloud SIEM detection rules. Requires the `security_monitoring_rules_read` permission."
},
{
"name": "Search Spans",
"description": "Search indexed APM spans using the span query syntax, with cursor pagination."
},
{
"name": "List Services",
"description": "List service definitions from the Datadog Service Catalog. Requires the `apm_service_catalog_read` permission."
}
],
"operationCount": 12,
"operationCount": 41,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
+127
View File
@@ -0,0 +1,127 @@
import type { AddIncidentTodoParams, AddIncidentTodoResponse } from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const addIncidentTodoTool: ToolConfig<AddIncidentTodoParams, AddIncidentTodoResponse> = {
id: 'datadog_add_incident_todo',
name: 'Datadog Add Incident Todo',
description:
'Add a follow-up task (todo) to an incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta.',
version: '1.0.0',
params: {
incidentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the incident the todo belongs to',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The follow-up task content (e.g., "Restore lost data")',
},
assignees: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated assignee handles (e.g., "@jane@example.com,@on-call"). Datadog requires at least one assignee',
},
dueDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO-8601 timestamp for when the todo should be completed',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(
params.site,
`/api/v2/incidents/${encodeURIComponent(params.incidentId)}/relationships/todos`
),
method: 'POST',
headers: datadogHeaders,
body: (params) => {
const attributes: Record<string, unknown> = {
content: params.content,
assignees: splitCommaList(params.assignees) ?? [],
incident_id: params.incidentId,
}
if (params.dueDate) attributes.due_date = params.dueDate
return { data: { type: 'incident_todos', attributes } }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { todo: { attributes: {} } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
todo: {
id: data.data?.id,
type: data.data?.type,
attributes: data.data?.attributes ?? {},
},
},
}
},
outputs: {
todo: {
type: 'object',
description: 'The created incident todo',
properties: {
id: { type: 'string', description: 'Todo UUID' },
type: { type: 'string', description: 'Resource type (incident_todos)' },
attributes: {
type: 'object',
description: 'Todo attributes',
properties: {
content: { type: 'string', description: 'Task content' },
assignees: { type: 'array', description: 'Assignee handles' },
due_date: { type: 'string', description: 'Due date' },
completed: { type: 'string', description: 'Completion timestamp' },
incident_id: { type: 'string', description: 'UUID of the parent incident' },
},
},
},
},
},
}
+3 -2
View File
@@ -1,4 +1,5 @@
import type { CancelDowntimeParams, CancelDowntimeResponse } from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const cancelDowntimeTool: ToolConfig<CancelDowntimeParams, CancelDowntimeResponse> = {
@@ -49,13 +50,13 @@ export const cancelDowntimeTool: ToolConfig<CancelDowntimeParams, CancelDowntime
transformResponse: async (response: Response) => {
if (!response.ok && response.status !== 204) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
success: false,
},
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
+149
View File
@@ -0,0 +1,149 @@
import type { CreateDashboardParams, CreateDashboardResponse } from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
parseJsonParam,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const createDashboardTool: ToolConfig<CreateDashboardParams, CreateDashboardResponse> = {
id: 'datadog_create_dashboard',
name: 'Datadog Create Dashboard',
description: 'Create a dashboard from a title, layout type, and widget definitions.',
version: '1.0.0',
params: {
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the dashboard',
},
layoutType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Layout type: "ordered" or "free"',
},
widgets: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of widget definitions, e.g. [{"definition": {"type": "timeseries", "requests": [{"q": "avg:system.cpu.user{*}"}]}}]',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description of the dashboard',
},
notifyList: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated user handles to notify on dashboard changes',
},
templateVariables: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of template variable definitions, e.g. [{"name": "env", "prefix": "env", "available_values": ["prod"]}]',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated dashboard tags in the form "team:<name>" (max 5)',
},
reflowType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Reflow type for ordered layouts: "auto" or "fixed"',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, '/api/v1/dashboard'),
method: 'POST',
headers: datadogHeaders,
body: (params) => {
const body: Record<string, unknown> = {
title: params.title,
layout_type: params.layoutType,
widgets: parseJsonParam<unknown[]>(params.widgets, 'widgets parameter') ?? [],
}
if (params.description) body.description = params.description
if (params.reflowType) body.reflow_type = params.reflowType
const notifyList = splitCommaList(params.notifyList)
if (notifyList) body.notify_list = notifyList
const tags = splitCommaList(params.tags)
if (tags) body.tags = tags
const templateVariables = parseJsonParam<unknown[]>(
params.templateVariables,
'templateVariables parameter'
)
if (templateVariables) body.template_variables = templateVariables
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { dashboard: {} },
error: await datadogErrorMessage(response),
}
}
return {
success: true,
output: { dashboard: await response.json() },
}
},
outputs: {
dashboard: {
type: 'object',
description: 'The created dashboard',
properties: {
id: { type: 'string', description: 'Dashboard ID' },
title: { type: 'string', description: 'Dashboard title' },
layout_type: { type: 'string', description: 'Layout type: ordered or free' },
url: { type: 'string', description: 'Dashboard URL path' },
author_handle: { type: 'string', description: 'Handle of the dashboard author' },
created_at: { type: 'string', description: 'Creation timestamp' },
modified_at: { type: 'string', description: 'Modification timestamp' },
widgets: { type: 'array', description: 'Widget definitions' },
},
},
},
}
+46 -34
View File
@@ -1,4 +1,9 @@
import type { CreateDowntimeParams, CreateDowntimeResponse } from '@/tools/datadog/types'
import type {
CreateDowntimeParams,
CreateDowntimeResponse,
DowntimeAttributes,
} from '@/tools/datadog/types'
import { datadogErrorMessage, parseMonitorIds, splitCommaList } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntimeResponse> = {
@@ -90,57 +95,62 @@ export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntime
'DD-APPLICATION-KEY': params.applicationKey,
}),
body: (params) => {
const schedule: Record<string, any> = {}
// A one-time schedule accepts only `start` and `end` (`additionalProperties: false`);
// the timezone is a display-only attribute on the downtime itself.
const schedule: { start?: string; end?: string } = {}
if (params.start) schedule.start = new Date(params.start * 1000).toISOString()
if (params.end) schedule.end = new Date(params.end * 1000).toISOString()
if (params.timezone) schedule.timezone = params.timezone
const body: Record<string, any> = {
data: {
type: 'downtime',
attributes: {
scope: params.scope,
schedule: Object.keys(schedule).length > 0 ? schedule : undefined,
},
},
const monitorTags = splitCommaList(params.monitorTags)
const monitorId = parseMonitorIds(params.monitorId)?.[0]
/**
* `monitor_identifier` is required and is a `oneOf`: a downtime targets either a
* single monitor or a tag set, never both. Accepting both silently would drop one
* of them and mute a different set of monitors than the caller asked for. Both
* sides are compared after parsing so a blank or whitespace-only input, which is
* how an untouched field arrives, does not read as a chosen target.
*/
if (monitorId !== undefined && monitorTags) {
throw new Error(
'Supply either a monitor ID or monitor tags, not both — a downtime targets one or the other'
)
}
if (params.message) body.data.attributes.message = params.message
// Datadog expresses "every monitor in scope" as the `*` monitor tag, which is the
// fallback when no monitor is named.
const monitorIdentifier =
monitorId !== undefined ? { monitor_id: monitorId } : { monitor_tags: monitorTags ?? ['*'] }
const attributes: Record<string, unknown> = {
scope: params.scope,
monitor_identifier: monitorIdentifier,
}
if (Object.keys(schedule).length > 0) attributes.schedule = schedule
if (params.timezone) attributes.display_timezone = params.timezone
if (params.message) attributes.message = params.message
if (params.muteFirstRecoveryNotification !== undefined) {
body.data.attributes.mute_first_recovery_notification = params.muteFirstRecoveryNotification
attributes.mute_first_recovery_notification = params.muteFirstRecoveryNotification
}
if (params.monitorId) {
body.data.attributes.monitor_identifier = {
monitor_id: Number.parseInt(params.monitorId, 10),
}
} else if (params.monitorTags) {
body.data.attributes.monitor_identifier = {
monitor_tags: params.monitorTags
.split(',')
.map((t: string) => t.trim())
.filter((t: string) => t.length > 0),
}
}
return body
return { data: { type: 'downtime', attributes } }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
downtime: {} as any,
downtime: { scope: [] },
},
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
const data = await response.json()
const attrs = data.data?.attributes || {}
const attrs: DowntimeAttributes = data.data?.attributes || {}
return {
success: true,
output: {
@@ -152,8 +162,7 @@ export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntime
? new Date(attrs.schedule.start).getTime() / 1000
: undefined,
end: attrs.schedule?.end ? new Date(attrs.schedule.end).getTime() / 1000 : undefined,
timezone: attrs.schedule?.timezone,
disabled: attrs.disabled,
timezone: attrs.display_timezone ?? undefined,
active: attrs.status === 'active',
created: attrs.created ? new Date(attrs.created).getTime() / 1000 : undefined,
modified: attrs.modified ? new Date(attrs.modified).getTime() / 1000 : undefined,
@@ -167,12 +176,15 @@ export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntime
type: 'object',
description: 'The created downtime details',
properties: {
id: { type: 'number', description: 'Downtime ID' },
id: { type: 'string', description: 'Downtime UUID' },
scope: { type: 'array', description: 'Downtime scope' },
message: { type: 'string', description: 'Downtime message' },
start: { type: 'number', description: 'Start time (Unix timestamp)' },
end: { type: 'number', description: 'End time (Unix timestamp)' },
timezone: { type: 'string', description: 'Display timezone for the downtime' },
active: { type: 'boolean', description: 'Whether downtime is currently active' },
created: { type: 'number', description: 'Creation time (Unix timestamp)' },
modified: { type: 'number', description: 'Last modification time (Unix timestamp)' },
},
},
},
+21 -5
View File
@@ -1,4 +1,10 @@
import type { CreateEventParams, CreateEventResponse } from '@/tools/datadog/types'
import type {
CreateEventParams,
CreateEventResponse,
EventAlertType,
EventPriority,
} from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const createEventTool: ToolConfig<CreateEventParams, CreateEventResponse> = {
@@ -91,7 +97,17 @@ export const createEventTool: ToolConfig<CreateEventParams, CreateEventResponse>
'DD-API-KEY': params.apiKey,
}),
body: (params) => {
const body: Record<string, any> = {
const body: {
title: string
text: string
alert_type?: EventAlertType
priority?: EventPriority
host?: string
aggregation_key?: string
source_type_name?: string
date_happened?: number
tags?: string[]
} = {
title: params.title,
text: params.text,
}
@@ -116,13 +132,13 @@ export const createEventTool: ToolConfig<CreateEventParams, CreateEventResponse>
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
event: {} as any,
event: {},
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
+162
View File
@@ -0,0 +1,162 @@
import type { CreateIncidentParams, CreateIncidentResponse } from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
parseJsonParam,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const createIncidentTool: ToolConfig<CreateIncidentParams, CreateIncidentResponse> = {
id: 'datadog_create_incident',
name: 'Datadog Create Incident',
description:
'Declare a new incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta.',
version: '1.0.0',
params: {
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the incident summarizing what happened',
},
customerImpacted: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Whether the incident caused customer impact',
},
severity: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Incident severity: UNKNOWN, SEV-0, SEV-1, SEV-2, SEV-3, SEV-4, or SEV-5',
},
customerImpactScope: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Summary of the customer impact. Required when customerImpacted is true',
},
incidentTypeUuid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the incident type. The default incident type is used when omitted',
},
isTest: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether this is a test incident',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON object of user-defined incident fields, e.g. {"severity": {"type": "dropdown", "value": "SEV-2"}}',
},
notificationHandles: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated handles to notify on creation (e.g., "@slack-incidents,@user@example.com")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, '/api/v2/incidents'),
method: 'POST',
headers: datadogHeaders,
body: (params) => {
const fields =
parseJsonParam<Record<string, unknown>>(params.fields, 'fields parameter') ?? {}
if (params.severity) {
fields.severity = { type: 'dropdown', value: params.severity }
}
const attributes: Record<string, unknown> = {
title: params.title,
customer_impacted: params.customerImpacted,
}
if (params.customerImpactScope) attributes.customer_impact_scope = params.customerImpactScope
if (params.incidentTypeUuid) attributes.incident_type_uuid = params.incidentTypeUuid
if (params.isTest !== undefined) attributes.is_test = params.isTest
if (Object.keys(fields).length > 0) attributes.fields = fields
const handles = splitCommaList(params.notificationHandles)
if (handles) {
attributes.notification_handles = handles.map((handle) => ({ handle }))
}
return { data: { type: 'incidents', attributes } }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { incident: { id: '', attributes: {} } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
incident: {
id: data.data?.id,
type: data.data?.type,
attributes: data.data?.attributes ?? {},
},
},
}
},
outputs: {
incident: {
type: 'object',
description: 'The created incident',
properties: {
id: { type: 'string', description: 'Incident UUID' },
type: { type: 'string', description: 'Resource type (incidents)' },
attributes: {
type: 'object',
description: 'Incident attributes',
properties: {
title: { type: 'string', description: 'Incident title' },
public_id: { type: 'number', description: 'Incremental public incident ID' },
customer_impacted: { type: 'boolean', description: 'Whether customers were impacted' },
created: { type: 'string', description: 'Creation timestamp' },
modified: { type: 'string', description: 'Last modification timestamp' },
},
},
},
},
},
}
+21 -13
View File
@@ -1,4 +1,5 @@
import type { CreateMonitorParams, CreateMonitorResponse } from '@/tools/datadog/types'
import type { CreateMonitorParams, CreateMonitorResponse, MonitorType } from '@/tools/datadog/types'
import { datadogErrorMessage, parseJsonParam } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorResponse> = {
@@ -86,7 +87,15 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
'DD-APPLICATION-KEY': params.applicationKey,
}),
body: (params) => {
const body: Record<string, any> = {
const body: {
name: string
type: MonitorType
query: string
message?: string
priority?: number
tags?: string[]
options?: unknown
} = {
name: params.name,
type: params.type,
query: params.query,
@@ -102,14 +111,8 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
.filter((t: string) => t.length > 0)
}
if (params.options) {
try {
body.options =
typeof params.options === 'string' ? JSON.parse(params.options) : params.options
} catch {
// If options parsing fails, skip it
}
}
const options = parseJsonParam<Record<string, unknown>>(params.options, 'options parameter')
if (options) body.options = options
return body
},
@@ -117,13 +120,13 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
monitor: {} as any,
monitor: {},
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
@@ -164,6 +167,11 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
overall_state: { type: 'string', description: 'Current monitor state' },
created: { type: 'string', description: 'Creation timestamp' },
modified: { type: 'string', description: 'Last modification timestamp' },
options: {
type: 'json',
description: 'Monitor options (thresholds, notification settings)',
},
creator: { type: 'json', description: 'Monitor creator (email, handle, name)' },
},
},
},
+148
View File
@@ -0,0 +1,148 @@
import type { CreateSloParams, CreateSloResponse } from '@/tools/datadog/types'
import {
buildSloPayload,
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const createSloTool: ToolConfig<CreateSloParams, CreateSloResponse> = {
id: 'datadog_create_slo',
name: 'Datadog Create SLO',
description:
'Create a service level objective from a metric query, monitors, or a time-slice condition.',
version: '1.0.0',
params: {
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the SLO (e.g., "Checkout API availability")',
},
type: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'SLO type: "metric" (supply query) or "monitor" (supply monitorIds). Time-slice SLOs are not supported here because they need an SLI specification this tool does not send.',
},
thresholds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of thresholds, e.g. [{"timeframe": "30d", "target": 99.9, "warning": 99.95}]',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description of the SLO',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated tags (e.g., "env:prod,team:core")',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'For metric SLOs, JSON with numerator and denominator, e.g. {"numerator": "sum:requests{status:ok}.as_count()", "denominator": "sum:requests{*}.as_count()"}',
},
monitorIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'For monitor SLOs, comma-separated monitor IDs (e.g., "123,456")',
},
groups: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'For monitor SLOs with a single monitor, comma-separated monitor groups (e.g., "env:prod,role:mysql")',
},
targetThreshold: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Primary target threshold (e.g., 99.9)',
},
warningThreshold: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Primary warning threshold, must be greater than the target (e.g., 99.95)',
},
timeframe: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Primary timeframe: "7d", "30d", or "90d"',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, '/api/v1/slo'),
method: 'POST',
headers: datadogHeaders,
body: buildSloPayload,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { slo: { id: '', name: '', type: '' } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: { slo: data.data?.[0] ?? { id: '', name: '', type: '' } },
}
},
outputs: {
slo: {
type: 'object',
description: 'The created service level objective',
properties: {
id: { type: 'string', description: 'SLO ID' },
name: { type: 'string', description: 'SLO name' },
type: { type: 'string', description: 'SLO type' },
description: { type: 'string', description: 'SLO description' },
tags: { type: 'array', description: 'SLO tags' },
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
created_at: { type: 'number', description: 'Creation timestamp (Unix seconds)' },
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
},
},
},
}
+456
View File
@@ -0,0 +1,456 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createDowntimeTool } from '@/tools/datadog/create_downtime'
import { createEventTool } from '@/tools/datadog/create_event'
import { createMonitorTool } from '@/tools/datadog/create_monitor'
import { getIncidentTool } from '@/tools/datadog/get_incident'
import { listDowntimesTool } from '@/tools/datadog/list_downtimes'
import { listIncidentsTool } from '@/tools/datadog/list_incidents'
import { muteMonitorTool } from '@/tools/datadog/mute_monitor'
import { queryLogsTool } from '@/tools/datadog/query_logs'
import { queryTimeseriesTool } from '@/tools/datadog/query_timeseries'
import { sendLogsTool } from '@/tools/datadog/send_logs'
import { submitMetricsTool } from '@/tools/datadog/submit_metrics'
import { unmuteMonitorTool } from '@/tools/datadog/unmute_monitor'
import { updateIncidentTool } from '@/tools/datadog/update_incident'
import { updateSloTool } from '@/tools/datadog/update_slo'
import {
buildSloPayload,
datadogErrorMessage,
mergeSloUpdatePayload,
splitCommaList,
} from '@/tools/datadog/utils'
const auth = { apiKey: 'key', applicationKey: 'app-key' } as const
function jsonResponse(body: unknown, init?: { status?: number; statusText?: string }): Response {
return new Response(JSON.stringify(body), {
status: init?.status ?? 200,
statusText: init?.statusText ?? 'OK',
headers: { 'Content-Type': 'application/json' },
})
}
function callBody<TParams>(
tool: { request: { body?: (params: TParams) => unknown } },
params: TParams
): any {
return tool.request.body?.(params)
}
function callUrl<TParams>(tool: { request: { url: unknown } }, params: TParams): string {
const url = tool.request.url
return typeof url === 'function' ? url(params) : (url as string)
}
describe('submit_metrics metric intake types', () => {
/**
* Datadog `MetricIntakeType` is 0 unspecified, 1 count, 2 rate, 3 gauge. Swapping
* these silently changes how Datadog aggregates the series.
*/
it.each([
['count', 1],
['rate', 2],
['gauge', 3],
])('encodes %s as %i', (type, code) => {
const body = callBody(submitMetricsTool, {
...auth,
series: JSON.stringify([{ metric: 'm', type, points: [{ timestamp: 1, value: 2 }] }]),
} as any)
expect(body.series[0].type).toBe(code)
})
it('omits type when the caller did not supply one', () => {
const body = callBody(submitMetricsTool, {
...auth,
series: JSON.stringify([{ metric: 'm', points: [{ timestamp: 1, value: 2 }] }]),
} as any)
expect(body.series[0]).not.toHaveProperty('type')
})
it('does not invent a resources entry', () => {
const body = callBody(submitMetricsTool, {
...auth,
series: JSON.stringify([{ metric: 'm', points: [{ timestamp: 1, value: 2 }] }]),
} as any)
expect(body.series[0]).not.toHaveProperty('resources')
})
it('forwards interval, which Datadog requires for count and rate metrics', () => {
const body = callBody(submitMetricsTool, {
...auth,
series: JSON.stringify([
{ metric: 'm', type: 'count', interval: 20, points: [{ timestamp: 1, value: 2 }] },
]),
} as any)
expect(body.series[0].interval).toBe(20)
})
})
describe('SLO payloads', () => {
it('rejects blank thresholds instead of sending an empty array', () => {
expect(() =>
buildSloPayload({ ...auth, name: 'n', type: 'metric', thresholds: '' } as any)
).toThrow(/thresholds must be a non-empty JSON array/)
})
it('rejects a non-numeric monitor id instead of sending null', () => {
expect(() =>
buildSloPayload({
...auth,
name: 'n',
type: 'monitor',
thresholds: '[{"timeframe":"30d","target":99.9}]',
monitorIds: '123,abc',
} as any)
).toThrow(/monitorIds must be a comma-separated list of whole numbers/)
})
/**
* `PUT /api/v1/slo/{slo_id}` is a full replacement, so every stored field the user
* did not edit has to be replayed or Datadog erases it.
*/
it('preserves stored fields the caller left blank', () => {
const stored = {
id: 'slo-1',
created_at: 1,
modified_at: 2,
creator: { email: 'a@b.c' },
monitor_tags: ['env:prod'],
name: 'Old name',
type: 'monitor',
thresholds: [{ timeframe: '30d', target: 99.9 }],
description: 'keep me',
tags: ['team:core'],
monitor_ids: [123],
groups: ['env:prod'],
target_threshold: 99.9,
timeframe: '30d',
}
const body = mergeSloUpdatePayload(stored, { ...auth, sloId: 'slo-1', name: 'New name' } as any)
expect(body.name).toBe('New name')
expect(body.description).toBe('keep me')
expect(body.tags).toEqual(['team:core'])
expect(body.monitor_ids).toEqual([123])
expect(body.groups).toEqual(['env:prod'])
expect(body.thresholds).toEqual([{ timeframe: '30d', target: 99.9 }])
expect(body.target_threshold).toBe(99.9)
expect(body.timeframe).toBe('30d')
})
it('strips fields Datadog computes and rejects on update', () => {
const body = mergeSloUpdatePayload(
{ id: 'slo-1', created_at: 1, modified_at: 2, creator: {}, monitor_tags: [], name: 'n' },
{ ...auth, sloId: 'slo-1' } as any
)
for (const field of ['id', 'created_at', 'modified_at', 'creator', 'monitor_tags']) {
expect(body).not.toHaveProperty(field)
}
})
})
describe('update_slo read-modify-write', () => {
beforeEach(() => vi.restoreAllMocks())
it('reads the stored SLO before replacing it', async () => {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(
jsonResponse({ data: { id: 'slo-1', name: 'Old', type: 'metric', description: 'keep' } })
)
.mockResolvedValueOnce(jsonResponse({ data: [{ id: 'slo-1', name: 'New' }] }))
const result = await updateSloTool.directExecution!(
{ ...auth, sloId: 'slo-1', name: 'New' } as any,
undefined
)
expect(result.success).toBe(true)
expect(fetchMock).toHaveBeenCalledTimes(2)
expect(fetchMock.mock.calls[0][1]?.method).toBe('GET')
const putBody = JSON.parse(String(fetchMock.mock.calls[1][1]?.body))
expect(putBody.name).toBe('New')
expect(putBody.description).toBe('keep')
})
it('does not write when the stored SLO cannot be read', async () => {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(jsonResponse({ errors: ['SLO not found'] }, { status: 404 }))
const result = await updateSloTool.directExecution!(
{ ...auth, sloId: 'missing', name: 'New' } as any,
undefined
)
expect(result.success).toBe(false)
expect(result.error).toContain('SLO not found')
expect(fetchMock).toHaveBeenCalledTimes(1)
})
})
describe('datadogErrorMessage', () => {
it('reads v1 string errors', async () => {
await expect(
datadogErrorMessage(jsonResponse({ errors: ['bad query'] }, { status: 400 }))
).resolves.toBe('bad query')
})
it('reads v2 JSON:API error objects', async () => {
await expect(
datadogErrorMessage(jsonResponse({ errors: [{ detail: 'forbidden' }] }, { status: 403 }))
).resolves.toBe('forbidden')
})
/** The SLO delete conflict returns errors as a map of resource id to reason. */
it('reads dictionary-shaped errors', async () => {
await expect(
datadogErrorMessage(
jsonResponse({ errors: { 'slo-1': 'still referenced by a dashboard' } }, { status: 409 })
)
).resolves.toBe('still referenced by a dashboard')
})
it('falls back to the status line when no message is present', async () => {
await expect(
datadogErrorMessage(jsonResponse({}, { status: 500, statusText: 'Server Error' }))
).resolves.toBe('HTTP 500: Server Error')
})
})
describe('send_logs', () => {
/** Datadog treats unrecognized keys as the log's structured attributes. */
it('preserves custom attributes', () => {
const body = callBody(sendLogsTool, {
...auth,
logs: JSON.stringify([{ message: 'boom', status: 'error', orderId: 42 }]),
} as any)
expect(body[0].status).toBe('error')
expect(body[0].orderId).toBe(42)
})
it('does not pad absent optional fields with empty strings', () => {
const body = callBody(sendLogsTool, {
...auth,
logs: JSON.stringify([{ message: 'boom' }]),
} as any)
expect(body[0]).not.toHaveProperty('hostname')
expect(body[0]).not.toHaveProperty('service')
expect(body[0]).not.toHaveProperty('ddtags')
})
})
describe('incident include parameter', () => {
/** The spec enum is exact, so a space from a comma-separated input 400s. */
it('trims spaces in get_incident', () => {
const url = callUrl(getIncidentTool, {
...auth,
incidentId: 'abc',
include: 'users, attachments',
} as any)
expect(url).toContain('include=users%2Cattachments')
expect(url).not.toContain('%20')
})
it('trims spaces in list_incidents', () => {
const url = callUrl(listIncidentsTool, { ...auth, include: 'users, attachments' } as any)
expect(url).toContain('include=users%2Cattachments')
expect(url).not.toContain('%20')
})
})
describe('update_incident blank handling', () => {
/** A blank input must not blank the stored incident under a partial update. */
it('omits empty strings rather than overwriting stored values', () => {
const body = callBody(updateIncidentTool, {
...auth,
incidentId: 'abc',
title: '',
customerImpactScope: '',
detected: '',
} as any)
expect(body.data.attributes).not.toHaveProperty('title')
expect(body.data.attributes).not.toHaveProperty('customer_impact_scope')
expect(body.data.attributes).not.toHaveProperty('detected')
})
it('still sends supplied values', () => {
const body = callBody(updateIncidentTool, {
...auth,
incidentId: 'abc',
title: 'Real title',
} as any)
expect(body.data.id).toBe('abc')
expect(body.data.type).toBe('incidents')
expect(body.data.attributes.title).toBe('Real title')
})
})
describe('create_monitor options', () => {
/** Silently dropping malformed options created monitors with no thresholds. */
it('fails loudly on malformed options JSON', () => {
expect(() =>
callBody(createMonitorTool, {
...auth,
name: 'n',
type: 'metric alert',
query: 'q',
options: '{not json',
} as any)
).toThrow(/options parameter must be valid JSON/)
})
})
describe('query_timeseries', () => {
/** Datadog reports query failures with a 200 and a non-ok status. */
it('surfaces a non-ok status as a failure', async () => {
const result = await queryTimeseriesTool.transformResponse!(
jsonResponse({ status: 'error', error: 'invalid query', series: [] })
)
expect(result.success).toBe(false)
expect(result.error).toContain('invalid query')
})
it('passes through a successful query', async () => {
const result = await queryTimeseriesTool.transformResponse!(
jsonResponse({ status: 'ok', series: [{ metric: 'm', tag_set: [], pointlist: [[1000, 5]] }] })
)
expect(result.success).toBe(true)
expect(result.output.series[0].points[0]).toEqual({ timestamp: 1, value: 5 })
})
})
describe('pagination wiring', () => {
it('sends downtime page params', () => {
const url = callUrl(listDowntimesTool, { ...auth, limit: 50, offset: 100 } as any)
expect(url).toContain('page%5Blimit%5D=50')
expect(url).toContain('page%5Boffset%5D=100')
})
it('feeds a log search cursor back into the request', () => {
const body = callBody(queryLogsTool, {
...auth,
query: '*',
from: 'now-1h',
to: 'now',
cursor: 'abc123',
} as any)
expect(body.page.cursor).toBe('abc123')
})
})
describe('monitor mute and unmute', () => {
it('mutes with the scope and end datadogpy documents', () => {
const body = callBody(muteMonitorTool, {
...auth,
monitorId: '123',
scope: 'host:web-1',
end: 1705323600,
} as any)
expect(body).toEqual({ scope: 'host:web-1', end: 1705323600 })
expect(callUrl(muteMonitorTool, { ...auth, monitorId: '123' } as any)).toContain(
'/api/v1/monitor/123/mute'
)
})
/** An indefinite mute sends no `end`, so the monitor stays muted until unmuted. */
it('omits end when the caller wants an indefinite mute', () => {
const body = callBody(muteMonitorTool, { ...auth, monitorId: '123' } as any)
expect(body).not.toHaveProperty('end')
})
/** Muting is only safe to ship because it can be reversed from Sim. */
it('ships an unmute counterpart that can clear every scope', () => {
const body = callBody(unmuteMonitorTool, {
...auth,
monitorId: '123',
allScopes: true,
} as any)
expect(body).toEqual({ all_scopes: true })
expect(callUrl(unmuteMonitorTool, { ...auth, monitorId: '123' } as any)).toContain(
'/api/v1/monitor/123/unmute'
)
})
})
describe('create_downtime monitor targeting', () => {
/** monitor_identifier is a oneOf, so accepting both would silently drop one. */
it('rejects a monitor ID and monitor tags together', () => {
expect(() =>
callBody(createDowntimeTool, {
...auth,
scope: '*',
monitorId: '123',
monitorTags: 'team:backend',
} as any)
).toThrow(/either a monitor ID or monitor tags, not both/)
})
it('rejects a non-numeric monitor ID instead of sending null', () => {
expect(() =>
callBody(createDowntimeTool, { ...auth, scope: '*', monitorId: 'abc' } as any)
).toThrow(/monitorIds must be a comma-separated list of whole numbers/)
})
it('falls back to the wildcard monitor tag when no monitor is named', () => {
const body = callBody(createDowntimeTool, { ...auth, scope: '*' } as any)
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_tags: ['*'] })
})
it('targets a single monitor by numeric id', () => {
const body = callBody(createDowntimeTool, { ...auth, scope: '*', monitorId: '123' } as any)
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_id: 123 })
})
/**
* A `<Block.output>` reference to get_monitor resolves to a number, and an LLM tool
* call can pass one too, so the parser must not assume a string.
*/
it('accepts a monitor id that arrives as a number', () => {
const body = callBody(createDowntimeTool, { ...auth, scope: '*', monitorId: 123 } as any)
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_id: 123 })
})
/** A blank monitor ID is how an untouched field arrives; it is not a chosen target. */
it('does not treat a whitespace-only monitor id as a conflicting target', () => {
const body = callBody(createDowntimeTool, {
...auth,
scope: '*',
monitorId: ' ',
monitorTags: 'team:backend',
} as any)
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_tags: ['team:backend'] })
})
it('accepts monitor tags that arrive as an array', () => {
const body = callBody(createDowntimeTool, {
...auth,
scope: '*',
monitorTags: ['team:backend', 'priority:high'],
} as any)
expect(body.data.attributes.monitor_identifier).toEqual({
monitor_tags: ['team:backend', 'priority:high'],
})
})
})
describe('splitCommaList input tolerance', () => {
it('handles strings, numbers, and arrays without throwing', () => {
expect(splitCommaList('a, b')).toEqual(['a', 'b'])
expect(splitCommaList(123)).toEqual(['123'])
expect(splitCommaList([1, 2])).toEqual(['1', '2'])
expect(splitCommaList(undefined)).toBeUndefined()
expect(splitCommaList('')).toBeUndefined()
})
})
describe('registry surface', () => {
it('keeps create_event on api-key-only auth', () => {
expect(createEventTool.params.applicationKey).toBeUndefined()
})
})
@@ -0,0 +1,76 @@
import type { DeleteDashboardParams, DeleteDashboardResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteDashboardTool: ToolConfig<DeleteDashboardParams, DeleteDashboardResponse> = {
id: 'datadog_delete_dashboard',
name: 'Datadog Delete Dashboard',
description: 'Delete a dashboard by ID.',
version: '1.0.0',
params: {
dashboardId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the dashboard to delete',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(params.site, `/api/v1/dashboard/${encodeURIComponent(params.dashboardId)}`),
method: 'DELETE',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { success: false },
error: await datadogErrorMessage(response),
}
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
success: true,
deletedDashboardId: data.deleted_dashboard_id,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the dashboard was deleted',
},
deletedDashboardId: {
type: 'string',
description: 'ID of the deleted dashboard',
optional: true,
},
},
}
+88
View File
@@ -0,0 +1,88 @@
import type { DeleteSloParams, DeleteSloResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteSloTool: ToolConfig<DeleteSloParams, DeleteSloResponse> = {
id: 'datadog_delete_slo',
name: 'Datadog Delete SLO',
description:
'Permanently delete a service level objective. Datadog returns a conflict when the SLO is still referenced by a dashboard.',
version: '1.0.0',
params: {
sloId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the service level objective to delete',
},
force: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Delete even when the SLO is referenced by other resources',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryString = params.force ? '?force=true' : ''
return datadogApiUrl(
params.site,
`/api/v1/slo/${encodeURIComponent(params.sloId)}${queryString}`
)
},
method: 'DELETE',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { success: false, deletedIds: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
success: true,
deletedIds: data.data ?? [],
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the SLO was deleted',
},
deletedIds: {
type: 'array',
description: 'IDs of the deleted service level objectives',
items: { type: 'string' },
},
},
}
@@ -0,0 +1,145 @@
import type {
GetBrowserSyntheticsResultsParams,
GetBrowserSyntheticsResultsResponse,
} from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getBrowserSyntheticsResultsTool: ToolConfig<
GetBrowserSyntheticsResultsParams,
GetBrowserSyntheticsResultsResponse
> = {
id: 'datadog_get_browser_synthetics_results',
name: 'Datadog Get Browser Synthetic Test Results',
description:
'Get the latest result summaries (up to the last 150 runs) for a Synthetic browser test, including step counts and errors.',
version: '1.0.0',
params: {
publicId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The public ID of the Synthetic browser test',
},
fromTs: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds from which to start querying results',
},
toTs: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds up to which to query results',
},
probeDc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated locations to query results for (e.g., "aws:eu-west-3")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.fromTs !== undefined) queryParams.set('from_ts', String(params.fromTs))
if (params.toTs !== undefined) queryParams.set('to_ts', String(params.toTs))
for (const location of splitCommaList(params.probeDc) ?? []) {
queryParams.append('probe_dc', location)
}
const queryString = queryParams.toString()
return datadogApiUrl(
params.site,
`/api/v1/synthetics/tests/browser/${encodeURIComponent(params.publicId)}/results${
queryString ? `?${queryString}` : ''
}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { results: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
results: data.results ?? [],
lastTimestampFetched: data.last_timestamp_fetched,
},
}
},
outputs: {
results: {
type: 'array',
description: 'Latest browser test result summaries',
items: {
type: 'object',
properties: {
result_id: { type: 'string', description: 'ID of the browser test result' },
check_time: { type: 'number', description: 'Time the browser test ran' },
probe_dc: { type: 'string', description: 'Location the browser test ran from' },
status: {
type: 'number',
description: 'Monitor status: 0 not triggered, 1 triggered, 2 no data',
},
result: {
type: 'object',
description: 'Run outcome',
properties: {
duration: { type: 'number', description: 'Length of the run in milliseconds' },
errorCount: { type: 'number', description: 'Number of errors collected in the run' },
stepCountCompleted: {
type: 'number',
description: 'Steps completed before failing',
},
stepCountTotal: { type: 'number', description: 'Total number of steps' },
device: { type: 'object', description: 'Device the run was performed on' },
},
},
},
},
},
lastTimestampFetched: {
type: 'number',
description: 'Timestamp of the latest browser test run',
optional: true,
},
},
}
+81
View File
@@ -0,0 +1,81 @@
import type { GetDashboardParams, GetDashboardResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getDashboardTool: ToolConfig<GetDashboardParams, GetDashboardResponse> = {
id: 'datadog_get_dashboard',
name: 'Datadog Get Dashboard',
description: 'Get the full definition of a dashboard, including its widgets.',
version: '1.0.0',
params: {
dashboardId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the dashboard (e.g., "abc-def-ghi")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(params.site, `/api/v1/dashboard/${encodeURIComponent(params.dashboardId)}`),
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { dashboard: {} },
error: await datadogErrorMessage(response),
}
}
return {
success: true,
output: { dashboard: await response.json() },
}
},
outputs: {
dashboard: {
type: 'object',
description: 'The dashboard definition',
properties: {
id: { type: 'string', description: 'Dashboard ID' },
title: { type: 'string', description: 'Dashboard title' },
description: { type: 'string', description: 'Dashboard description' },
layout_type: { type: 'string', description: 'Layout type: ordered or free' },
url: { type: 'string', description: 'Dashboard URL path' },
author_handle: { type: 'string', description: 'Handle of the dashboard author' },
author_name: { type: 'string', description: 'Name of the dashboard author' },
created_at: { type: 'string', description: 'Creation timestamp' },
modified_at: { type: 'string', description: 'Modification timestamp' },
tags: { type: 'array', description: 'Dashboard tags' },
notify_list: { type: 'array', description: 'Handles notified on dashboard changes' },
template_variables: { type: 'array', description: 'Template variable definitions' },
widgets: { type: 'array', description: 'Widget definitions' },
},
},
},
}
+115
View File
@@ -0,0 +1,115 @@
import type { GetIncidentParams, GetIncidentResponse } from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getIncidentTool: ToolConfig<GetIncidentParams, GetIncidentResponse> = {
id: 'datadog_get_incident',
name: 'Datadog Get Incident',
description:
'Get the details of a single incident by ID. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta.',
version: '1.0.0',
params: {
incidentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the incident (e.g., "00000000-0000-0000-1234-000000000000")',
},
include: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated related resources to include (e.g., "users", "attachments")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const include = splitCommaList(params.include)?.join(',')
const queryString = include ? `?${new URLSearchParams({ include }).toString()}` : ''
return datadogApiUrl(
params.site,
`/api/v2/incidents/${encodeURIComponent(params.incidentId)}${queryString}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { incident: { id: '', attributes: {} } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
incident: {
id: data.data?.id,
type: data.data?.type,
attributes: data.data?.attributes ?? {},
},
},
}
},
outputs: {
incident: {
type: 'object',
description: 'The incident',
properties: {
id: { type: 'string', description: 'Incident UUID' },
type: { type: 'string', description: 'Resource type (incidents)' },
attributes: {
type: 'object',
description: 'Incident attributes',
properties: {
title: { type: 'string', description: 'Incident title' },
state: { type: 'string', description: 'Incident state' },
severity: { type: 'string', description: 'Incident severity' },
public_id: { type: 'number', description: 'Incremental public incident ID' },
customer_impacted: { type: 'boolean', description: 'Whether customers were impacted' },
customer_impact_scope: {
type: 'string',
description: 'Summary of the customer impact',
},
created: { type: 'string', description: 'Creation timestamp' },
modified: { type: 'string', description: 'Last modification timestamp' },
resolved: { type: 'string', description: 'Resolution timestamp' },
time_to_resolve: { type: 'number', description: 'Seconds from creation to resolution' },
},
},
},
},
},
}
+10 -4
View File
@@ -1,4 +1,5 @@
import type { GetMonitorParams, GetMonitorResponse } from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> = {
@@ -19,7 +20,7 @@ export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> =
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated group states to include (e.g., "alert,warn", "alert,warn,no data,ok")',
'Comma-separated group states to include. Valid values are "all", "alert", "warn", and "no data" (e.g., "alert,warn").',
},
withDowntimes: {
type: 'boolean',
@@ -68,13 +69,13 @@ export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> =
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
monitor: {} as any,
monitor: {},
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
@@ -115,6 +116,11 @@ export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> =
overall_state: { type: 'string', description: 'Current monitor state' },
created: { type: 'string', description: 'Creation timestamp' },
modified: { type: 'string', description: 'Last modification timestamp' },
options: {
type: 'json',
description: 'Monitor options (thresholds, notification settings)',
},
creator: { type: 'json', description: 'Monitor creator (email, handle, name)' },
},
},
},
@@ -0,0 +1,93 @@
import type { GetSecuritySignalParams, GetSecuritySignalResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getSecuritySignalTool: ToolConfig<GetSecuritySignalParams, GetSecuritySignalResponse> =
{
id: 'datadog_get_security_signal',
name: 'Datadog Get Security Signal',
description:
'Get the details of a single Cloud SIEM security signal. Requires the `security_monitoring_signals_read` permission.',
version: '1.0.0',
params: {
signalId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the security signal',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(
params.site,
`/api/v2/security_monitoring/signals/${encodeURIComponent(params.signalId)}`
),
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { signal: { attributes: {} } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
signal: {
id: data.data?.id,
type: data.data?.type,
attributes: data.data?.attributes ?? {},
},
},
}
},
outputs: {
signal: {
type: 'object',
description: 'The security signal',
properties: {
id: { type: 'string', description: 'Signal ID' },
type: { type: 'string', description: 'Resource type (signal)' },
attributes: {
type: 'object',
description: 'Signal attributes',
properties: {
message: { type: 'string', description: 'Message from the detection rule' },
timestamp: { type: 'string', description: 'Signal timestamp' },
tags: { type: 'array', description: 'Tags on the signal' },
custom: { type: 'object', description: 'Signal-specific attributes' },
},
},
},
},
},
}
+99
View File
@@ -0,0 +1,99 @@
import type { GetSloParams, GetSloResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getSloTool: ToolConfig<GetSloParams, GetSloResponse> = {
id: 'datadog_get_slo',
name: 'Datadog Get SLO',
description: 'Get the configuration of a single service level objective by ID.',
version: '1.0.0',
params: {
sloId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the service level objective',
},
withConfiguredAlertIds: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include the IDs of SLO monitors that reference this SLO',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryString = params.withConfiguredAlertIds ? '?with_configured_alert_ids=true' : ''
return datadogApiUrl(
params.site,
`/api/v1/slo/${encodeURIComponent(params.sloId)}${queryString}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { slo: { id: '', name: '', type: '' } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: { slo: data.data ?? { id: '', name: '', type: '' } },
}
},
outputs: {
slo: {
type: 'object',
description: 'The service level objective',
properties: {
id: { type: 'string', description: 'SLO ID' },
name: { type: 'string', description: 'SLO name' },
type: { type: 'string', description: 'SLO type: metric, monitor, or time_slice' },
description: { type: 'string', description: 'SLO description' },
tags: { type: 'array', description: 'SLO tags' },
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
target_threshold: { type: 'number', description: 'Primary target threshold' },
warning_threshold: { type: 'number', description: 'Primary warning threshold' },
timeframe: { type: 'string', description: 'Primary timeframe' },
monitor_ids: { type: 'array', description: 'Monitor IDs for monitor-based SLOs' },
groups: { type: 'array', description: 'Monitor groups narrowing the SLO scope' },
configured_alert_ids: {
type: 'array',
description: 'SLO monitor IDs referencing this SLO',
optional: true,
},
created_at: { type: 'number', description: 'Creation timestamp (Unix seconds)' },
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
},
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type { GetSloHistoryParams, GetSloHistoryResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getSloHistoryTool: ToolConfig<GetSloHistoryParams, GetSloHistoryResponse> = {
id: 'datadog_get_slo_history',
name: 'Datadog Get SLO History',
description:
'Get an SLOs history over a time window, including the overall SLI value and remaining error budget.',
version: '1.0.0',
params: {
sloId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the service level objective',
},
fromTs: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Start of the query window as a Unix timestamp in seconds',
},
toTs: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'End of the query window as a Unix timestamp in seconds',
},
target: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'SLO target between 0 and 100. When supplied, the response includes the remaining error budget for a custom timeframe',
},
applyCorrection: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to apply SLO corrections (defaults to true)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
from_ts: String(params.fromTs),
to_ts: String(params.toTs),
})
if (params.target !== undefined) queryParams.set('target', String(params.target))
if (params.applyCorrection !== undefined)
queryParams.set('apply_correction', String(params.applyCorrection))
return datadogApiUrl(
params.site,
`/api/v1/slo/${encodeURIComponent(params.sloId)}/history?${queryParams.toString()}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { history: {} },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
const history = data.data ?? {}
return {
success: true,
output: {
history,
sliValue: history.overall?.sli_value ?? null,
},
}
},
outputs: {
history: {
type: 'object',
description: 'SLO history for the requested window',
properties: {
from_ts: { type: 'number', description: 'Window start (Unix seconds)' },
to_ts: { type: 'number', description: 'Window end (Unix seconds)' },
type: { type: 'string', description: 'SLO type' },
overall: {
type: 'object',
description: 'Overall SLI data for the window',
properties: {
sli_value: { type: 'number', description: 'SLI value over the window' },
span_precision: { type: 'number', description: 'Decimal precision of the SLI value' },
error_budget_remaining: {
type: 'object',
description: 'Remaining error budget keyed by timeframe',
},
},
},
groups: { type: 'array', description: 'Per-group SLI data for grouped SLOs' },
monitors: { type: 'array', description: 'Per-monitor SLI data for multi-monitor SLOs' },
thresholds: { type: 'object', description: 'Thresholds keyed by timeframe' },
},
},
sliValue: {
type: 'number',
description: 'Overall SLI value over the window',
optional: true,
},
},
}
@@ -0,0 +1,139 @@
import type {
GetSyntheticsResultsParams,
GetSyntheticsResultsResponse,
} from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getSyntheticsResultsTool: ToolConfig<
GetSyntheticsResultsParams,
GetSyntheticsResultsResponse
> = {
id: 'datadog_get_synthetics_results',
name: 'Datadog Get Synthetic Test Results',
description:
'Get the latest result summaries (up to the last 150 runs) for a Synthetic API test.',
version: '1.0.0',
params: {
publicId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The public ID of the Synthetic API test',
},
fromTs: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds from which to start querying results',
},
toTs: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds up to which to query results',
},
probeDc: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated locations to query results for (e.g., "aws:eu-west-3")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.fromTs !== undefined) queryParams.set('from_ts', String(params.fromTs))
if (params.toTs !== undefined) queryParams.set('to_ts', String(params.toTs))
for (const location of splitCommaList(params.probeDc) ?? []) {
queryParams.append('probe_dc', location)
}
const queryString = queryParams.toString()
return datadogApiUrl(
params.site,
`/api/v1/synthetics/tests/${encodeURIComponent(params.publicId)}/results${
queryString ? `?${queryString}` : ''
}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { results: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
results: data.results ?? [],
lastTimestampFetched: data.last_timestamp_fetched,
},
}
},
outputs: {
results: {
type: 'array',
description: 'Latest test result summaries',
items: {
type: 'object',
properties: {
result_id: { type: 'string', description: 'ID of the test result' },
check_time: { type: 'number', description: 'Time the test ran' },
probe_dc: { type: 'string', description: 'Location the test ran from' },
status: {
type: 'number',
description: 'Monitor status: 0 not triggered, 1 triggered, 2 no data',
},
result: {
type: 'object',
description: 'Run outcome',
properties: {
passed: { type: 'boolean', description: 'Whether the run passed' },
timings: { type: 'object', description: 'Request timing breakdown' },
},
},
},
},
},
lastTimestampFetched: {
type: 'number',
description: 'Timestamp of the latest test run',
optional: true,
},
},
}
@@ -0,0 +1,85 @@
import type { GetSyntheticsTestParams, GetSyntheticsTestResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const getSyntheticsTestTool: ToolConfig<GetSyntheticsTestParams, GetSyntheticsTestResponse> =
{
id: 'datadog_get_synthetics_test',
name: 'Datadog Get Synthetic Test',
description:
'Get the configuration of a Synthetic test by public ID. Browser test steps are not included by this type-agnostic endpoint.',
version: '1.0.0',
params: {
publicId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The public ID of the Synthetic test (e.g., "abc-def-ghi")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(
params.site,
`/api/v1/synthetics/tests/${encodeURIComponent(params.publicId)}`
),
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { test: {} },
error: await datadogErrorMessage(response),
}
}
return {
success: true,
output: { test: await response.json() },
}
},
outputs: {
test: {
type: 'object',
description: 'The Synthetic test configuration',
properties: {
public_id: { type: 'string', description: 'Public ID of the test' },
name: { type: 'string', description: 'Test name' },
status: { type: 'string', description: 'Pause status: live or paused' },
type: { type: 'string', description: 'Test type: api, browser, mobile, or network' },
subtype: { type: 'string', description: 'Test subtype, such as http or ssl' },
message: { type: 'string', description: 'Notification message' },
monitor_id: { type: 'number', description: 'Associated monitor ID' },
tags: { type: 'array', description: 'Tags attached to the test' },
locations: { type: 'array', description: 'Locations the test runs from' },
config: { type: 'object', description: 'Test request, assertions, and variables' },
options: { type: 'object', description: 'Scheduling, retry, and monitor options' },
creator: { type: 'object', description: 'User who created the test' },
},
},
},
}
+58
View File
@@ -1,15 +1,44 @@
import { addIncidentTodoTool } from '@/tools/datadog/add_incident_todo'
import { cancelDowntimeTool } from '@/tools/datadog/cancel_downtime'
import { createDashboardTool } from '@/tools/datadog/create_dashboard'
import { createDowntimeTool } from '@/tools/datadog/create_downtime'
import { createEventTool } from '@/tools/datadog/create_event'
import { createIncidentTool } from '@/tools/datadog/create_incident'
import { createMonitorTool } from '@/tools/datadog/create_monitor'
import { createSloTool } from '@/tools/datadog/create_slo'
import { deleteDashboardTool } from '@/tools/datadog/delete_dashboard'
import { deleteSloTool } from '@/tools/datadog/delete_slo'
import { getBrowserSyntheticsResultsTool } from '@/tools/datadog/get_browser_synthetics_results'
import { getDashboardTool } from '@/tools/datadog/get_dashboard'
import { getIncidentTool } from '@/tools/datadog/get_incident'
import { getMonitorTool } from '@/tools/datadog/get_monitor'
import { getSecuritySignalTool } from '@/tools/datadog/get_security_signal'
import { getSloTool } from '@/tools/datadog/get_slo'
import { getSloHistoryTool } from '@/tools/datadog/get_slo_history'
import { getSyntheticsResultsTool } from '@/tools/datadog/get_synthetics_results'
import { getSyntheticsTestTool } from '@/tools/datadog/get_synthetics_test'
import { listDashboardsTool } from '@/tools/datadog/list_dashboards'
import { listDowntimesTool } from '@/tools/datadog/list_downtimes'
import { listIncidentsTool } from '@/tools/datadog/list_incidents'
import { listMonitorsTool } from '@/tools/datadog/list_monitors'
import { listSecurityRulesTool } from '@/tools/datadog/list_security_rules'
import { listSecuritySignalsTool } from '@/tools/datadog/list_security_signals'
import { listServicesTool } from '@/tools/datadog/list_services'
import { listSlosTool } from '@/tools/datadog/list_slos'
import { listSyntheticsTestsTool } from '@/tools/datadog/list_synthetics_tests'
import { muteMonitorTool } from '@/tools/datadog/mute_monitor'
import { queryLogsTool } from '@/tools/datadog/query_logs'
import { queryTimeseriesTool } from '@/tools/datadog/query_timeseries'
import { searchSpansTool } from '@/tools/datadog/search_spans'
import { sendLogsTool } from '@/tools/datadog/send_logs'
import { submitMetricsTool } from '@/tools/datadog/submit_metrics'
import { triggerSyntheticsTestsTool } from '@/tools/datadog/trigger_synthetics_tests'
import { unmuteMonitorTool } from '@/tools/datadog/unmute_monitor'
import { updateIncidentTool } from '@/tools/datadog/update_incident'
import { updateSecuritySignalAssigneeTool } from '@/tools/datadog/update_security_signal_assignee'
import { updateSecuritySignalStateTool } from '@/tools/datadog/update_security_signal_state'
import { updateSloTool } from '@/tools/datadog/update_slo'
import { updateSyntheticsStatusTool } from '@/tools/datadog/update_synthetics_status'
export const datadogSubmitMetricsTool = submitMetricsTool
export const datadogQueryTimeseriesTool = queryTimeseriesTool
@@ -18,8 +47,37 @@ export const datadogCreateMonitorTool = createMonitorTool
export const datadogGetMonitorTool = getMonitorTool
export const datadogListMonitorsTool = listMonitorsTool
export const datadogMuteMonitorTool = muteMonitorTool
export const datadogUnmuteMonitorTool = unmuteMonitorTool
export const datadogQueryLogsTool = queryLogsTool
export const datadogSendLogsTool = sendLogsTool
export const datadogCreateDowntimeTool = createDowntimeTool
export const datadogListDowntimesTool = listDowntimesTool
export const datadogCancelDowntimeTool = cancelDowntimeTool
export const datadogListIncidentsTool = listIncidentsTool
export const datadogGetIncidentTool = getIncidentTool
export const datadogCreateIncidentTool = createIncidentTool
export const datadogUpdateIncidentTool = updateIncidentTool
export const datadogAddIncidentTodoTool = addIncidentTodoTool
export const datadogListSlosTool = listSlosTool
export const datadogGetSloTool = getSloTool
export const datadogCreateSloTool = createSloTool
export const datadogUpdateSloTool = updateSloTool
export const datadogDeleteSloTool = deleteSloTool
export const datadogGetSloHistoryTool = getSloHistoryTool
export const datadogListDashboardsTool = listDashboardsTool
export const datadogGetDashboardTool = getDashboardTool
export const datadogCreateDashboardTool = createDashboardTool
export const datadogDeleteDashboardTool = deleteDashboardTool
export const datadogListSyntheticsTestsTool = listSyntheticsTestsTool
export const datadogGetSyntheticsTestTool = getSyntheticsTestTool
export const datadogGetSyntheticsResultsTool = getSyntheticsResultsTool
export const datadogGetBrowserSyntheticsResultsTool = getBrowserSyntheticsResultsTool
export const datadogTriggerSyntheticsTestsTool = triggerSyntheticsTestsTool
export const datadogUpdateSyntheticsStatusTool = updateSyntheticsStatusTool
export const datadogListSecuritySignalsTool = listSecuritySignalsTool
export const datadogGetSecuritySignalTool = getSecuritySignalTool
export const datadogUpdateSecuritySignalStateTool = updateSecuritySignalStateTool
export const datadogUpdateSecuritySignalAssigneeTool = updateSecuritySignalAssigneeTool
export const datadogListSecurityRulesTool = listSecurityRulesTool
export const datadogSearchSpansTool = searchSpansTool
export const datadogListServicesTool = listServicesTool
+110
View File
@@ -0,0 +1,110 @@
import type { ListDashboardsParams, ListDashboardsResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listDashboardsTool: ToolConfig<ListDashboardsParams, ListDashboardsResponse> = {
id: 'datadog_list_dashboards',
name: 'Datadog List Dashboards',
description:
'List custom created or cloned dashboards. Datadog preset dashboards are not returned.',
version: '1.0.0',
params: {
filterShared: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return only shared dashboards',
},
filterDeleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return only deleted dashboards. Incompatible with filterShared',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of dashboards to return (default: 100)',
},
start: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Offset of the first dashboard returned (e.g., 0, 100)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.filterShared !== undefined)
queryParams.set('filter[shared]', String(params.filterShared))
if (params.filterDeleted !== undefined)
queryParams.set('filter[deleted]', String(params.filterDeleted))
if (params.count !== undefined) queryParams.set('count', String(params.count))
if (params.start !== undefined) queryParams.set('start', String(params.start))
const queryString = queryParams.toString()
return datadogApiUrl(params.site, `/api/v1/dashboard${queryString ? `?${queryString}` : ''}`)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { dashboards: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: { dashboards: data.dashboards ?? [] },
}
},
outputs: {
dashboards: {
type: 'array',
description: 'List of dashboard summaries',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Dashboard ID' },
title: { type: 'string', description: 'Dashboard title' },
description: { type: 'string', description: 'Dashboard description' },
layout_type: { type: 'string', description: 'Layout type: ordered or free' },
url: { type: 'string', description: 'Dashboard URL path' },
author_handle: { type: 'string', description: 'Handle of the dashboard author' },
created_at: { type: 'string', description: 'Creation timestamp' },
modified_at: { type: 'string', description: 'Modification timestamp' },
is_read_only: { type: 'boolean', description: 'Whether the dashboard is read-only' },
},
},
},
},
}
+33 -12
View File
@@ -1,4 +1,10 @@
import type { ListDowntimesParams, ListDowntimesResponse } from '@/tools/datadog/types'
import type {
DatadogV2Resource,
DowntimeAttributes,
ListDowntimesParams,
ListDowntimesResponse,
} from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesResponse> = {
@@ -14,11 +20,17 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
visibility: 'user-or-llm',
description: 'Only return currently active downtimes',
},
monitorId: {
type: 'string',
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Filter by monitor ID (e.g., "12345678")',
description: 'Number of downtimes to return per page (default: 30, max: 100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Index of the first downtime to return (e.g., 0, 30, 60)',
},
apiKey: {
type: 'string',
@@ -46,7 +58,8 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
const queryParams = new URLSearchParams()
if (params.currentOnly) queryParams.set('current_only', 'true')
if (params.monitorId) queryParams.set('monitor_id', params.monitorId)
if (params.limit !== undefined) queryParams.set('page[limit]', String(params.limit))
if (params.offset !== undefined) queryParams.set('page[offset]', String(params.offset))
const queryString = queryParams.toString()
return `https://api.${site}/api/v2/downtime${queryString ? `?${queryString}` : ''}`
@@ -61,27 +74,26 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
downtimes: [],
},
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
const data = await response.json()
const downtimes = (data.data || []).map((d: any) => {
const attrs = d.attributes || {}
const downtimes = (data.data || []).map((d: DatadogV2Resource<DowntimeAttributes>) => {
const attrs: DowntimeAttributes = d.attributes || {}
return {
id: d.id,
scope: attrs.scope ? [attrs.scope] : [],
message: attrs.message,
start: attrs.schedule?.start ? new Date(attrs.schedule.start).getTime() / 1000 : undefined,
end: attrs.schedule?.end ? new Date(attrs.schedule.end).getTime() / 1000 : undefined,
timezone: attrs.schedule?.timezone,
disabled: attrs.disabled,
timezone: attrs.display_timezone ?? undefined,
active: attrs.status === 'active',
created: attrs.created ? new Date(attrs.created).getTime() / 1000 : undefined,
modified: attrs.modified ? new Date(attrs.modified).getTime() / 1000 : undefined,
@@ -92,23 +104,32 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
success: true,
output: {
downtimes,
totalCount: data.meta?.page?.total_filtered_count,
},
}
},
outputs: {
totalCount: {
type: 'number',
description: 'Total number of downtimes matching the filter, across all pages',
optional: true,
},
downtimes: {
type: 'array',
description: 'List of downtimes',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Downtime ID' },
id: { type: 'string', description: 'Downtime UUID' },
scope: { type: 'array', description: 'Downtime scope' },
message: { type: 'string', description: 'Downtime message' },
start: { type: 'number', description: 'Start time (Unix timestamp)' },
end: { type: 'number', description: 'End time (Unix timestamp)' },
timezone: { type: 'string', description: 'Display timezone for the downtime' },
active: { type: 'boolean', description: 'Whether downtime is currently active' },
created: { type: 'number', description: 'Creation time (Unix timestamp)' },
modified: { type: 'number', description: 'Last modification time (Unix timestamp)' },
},
},
},
+135
View File
@@ -0,0 +1,135 @@
import type {
DatadogV2Resource,
IncidentAttributes,
ListIncidentsParams,
ListIncidentsResponse,
} from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listIncidentsTool: ToolConfig<ListIncidentsParams, ListIncidentsResponse> = {
id: 'datadog_list_incidents',
name: 'Datadog List Incidents',
description:
'List incidents for the organization. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta.',
version: '1.0.0',
params: {
include: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated related resources to include: "users" and/or "attachments"',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of incidents to return per page (default: 10, max: 100)',
},
pageOffset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Index of the first incident to return (e.g., 0, 10, 20)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
const include = splitCommaList(params.include)?.join(',')
if (include) queryParams.set('include', include)
if (params.pageSize !== undefined) queryParams.set('page[size]', String(params.pageSize))
if (params.pageOffset !== undefined)
queryParams.set('page[offset]', String(params.pageOffset))
const queryString = queryParams.toString()
return datadogApiUrl(params.site, `/api/v2/incidents${queryString ? `?${queryString}` : ''}`)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { incidents: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
incidents: (data.data ?? []).map((incident: DatadogV2Resource<IncidentAttributes>) => ({
id: incident.id,
type: incident.type,
attributes: incident.attributes ?? {},
})),
nextOffset: data.meta?.pagination?.next_offset,
},
}
},
outputs: {
incidents: {
type: 'array',
description: 'List of incidents',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Incident UUID' },
type: { type: 'string', description: 'Resource type (incidents)' },
attributes: {
type: 'object',
description: 'Incident attributes',
properties: {
title: { type: 'string', description: 'Incident title' },
state: { type: 'string', description: 'Incident state' },
severity: { type: 'string', description: 'Incident severity' },
public_id: { type: 'number', description: 'Incremental public incident ID' },
customer_impacted: {
type: 'boolean',
description: 'Whether customers were impacted',
},
created: { type: 'string', description: 'Creation timestamp' },
modified: { type: 'string', description: 'Last modification timestamp' },
resolved: { type: 'string', description: 'Resolution timestamp' },
},
},
},
},
},
nextOffset: {
type: 'number',
description: 'Offset to use for the next page of results',
optional: true,
},
},
}
+17 -21
View File
@@ -1,9 +1,7 @@
import { createLogger } from '@sim/logger'
import type { ListMonitorsParams, ListMonitorsResponse } from '@/tools/datadog/types'
import type { ListMonitorsParams, ListMonitorsResponse, MonitorData } from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('DatadogListMonitors')
export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsResponse> = {
id: 'datadog_list_monitors',
name: 'Datadog List Monitors',
@@ -16,7 +14,7 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated group states to filter by (e.g., "alert,warn", "alert,warn,no data,ok")',
'Comma-separated group states to filter by. Valid values are "all", "alert", "warn", and "no data" (e.g., "alert,warn").',
},
name: {
type: 'string',
@@ -89,18 +87,7 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
if (params.pageSize) queryParams.set('page_size', String(params.pageSize))
const queryString = queryParams.toString()
const url = `https://api.${site}/api/v1/monitor${queryString ? `?${queryString}` : ''}`
logger.info(
'[Datadog List Monitors] URL:',
url,
'Site param:',
params.site,
'API Key present:',
!!params.apiKey,
'App Key present:',
!!params.applicationKey
)
return url
return `https://api.${site}/api/v1/monitor${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params) => ({
@@ -112,18 +99,18 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
monitors: [],
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
const text = await response.text()
let data: any
let data: unknown
try {
data = JSON.parse(text)
} catch (e) {
@@ -142,7 +129,7 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
}
}
const monitors = data.map((m: any) => ({
const monitors = (data as MonitorData[]).map((m) => ({
id: m.id,
name: m.name,
type: m.type,
@@ -176,8 +163,17 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
name: { type: 'string', description: 'Monitor name' },
type: { type: 'string', description: 'Monitor type' },
query: { type: 'string', description: 'Monitor query' },
message: { type: 'string', description: 'Notification message' },
overall_state: { type: 'string', description: 'Current state' },
tags: { type: 'array', description: 'Tags' },
priority: { type: 'number', description: 'Monitor priority' },
options: {
type: 'json',
description: 'Monitor options (thresholds, notification settings)',
},
created: { type: 'string', description: 'Creation timestamp' },
modified: { type: 'string', description: 'Last modification timestamp' },
creator: { type: 'json', description: 'Monitor creator (email, handle, name)' },
},
},
},
@@ -0,0 +1,134 @@
import type {
ListSecurityRulesParams,
ListSecurityRulesResponse,
SecurityRuleData,
} from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listSecurityRulesTool: ToolConfig<ListSecurityRulesParams, ListSecurityRulesResponse> =
{
id: 'datadog_list_security_rules',
name: 'Datadog List Security Rules',
description:
'List Cloud SIEM detection rules. Requires the `security_monitoring_rules_read` permission.',
version: '1.0.0',
params: {
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Search query filtering rules by attributes such as type, source, or tags (e.g., "type:log_detection source:cloudtrail")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort attribute, prefix with "-" for descending: name, creation_date, update_date, enabled, type, highest_severity, or source',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of rules per page (default: 10, max: 100)',
},
pageNumber: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page to retrieve, starting at zero',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.query) queryParams.set('query', params.query)
if (params.sort) queryParams.set('sort', params.sort)
if (params.pageSize !== undefined) queryParams.set('page[size]', String(params.pageSize))
if (params.pageNumber !== undefined)
queryParams.set('page[number]', String(params.pageNumber))
const queryString = queryParams.toString()
return datadogApiUrl(
params.site,
`/api/v2/security_monitoring/rules${queryString ? `?${queryString}` : ''}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { rules: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
rules: (data.data ?? []).map((rule: SecurityRuleData) => ({
id: rule.id,
name: rule.name,
type: rule.type,
message: rule.message,
tags: rule.tags ?? [],
isEnabled: rule.isEnabled,
isDefault: rule.isDefault,
createdAt: rule.createdAt,
version: rule.version,
})),
},
}
},
outputs: {
rules: {
type: 'array',
description: 'List of detection rules',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Rule ID' },
name: { type: 'string', description: 'Rule name' },
type: { type: 'string', description: 'Rule type' },
message: { type: 'string', description: 'Message attached to generated signals' },
tags: { type: 'array', description: 'Rule tags' },
isEnabled: { type: 'boolean', description: 'Whether the rule is enabled' },
isDefault: {
type: 'boolean',
description: 'Whether the rule is a Datadog default rule',
},
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
version: { type: 'number', description: 'Rule version' },
},
},
},
},
}
@@ -0,0 +1,154 @@
import type {
DatadogV2Resource,
ListSecuritySignalsParams,
ListSecuritySignalsResponse,
SecuritySignalAttributes,
} from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listSecuritySignalsTool: ToolConfig<
ListSecuritySignalsParams,
ListSecuritySignalsResponse
> = {
id: 'datadog_list_security_signals',
name: 'Datadog List Security Signals',
description:
'Search Cloud SIEM security signals by query and time range. Requires the `security_monitoring_signals_read` permission.',
version: '1.0.0',
params: {
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Signal search query (e.g., "security:attack status:high")',
},
from: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Minimum timestamp as an ISO-8601 date-time (e.g., "2026-01-02T09:42:36.320Z"). Signal search does not accept relative expressions like "now-1h".',
},
to: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Maximum timestamp as an ISO-8601 date-time (e.g., "2026-01-03T09:42:36.320Z"). Signal search does not accept relative expressions like "now".',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: "timestamp" for oldest first, "-timestamp" for newest first',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of signals to return (default: 10, max: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, '/api/v2/security_monitoring/signals/search'),
method: 'POST',
headers: datadogHeaders,
body: (params) => {
const filter: Record<string, string> = {}
if (params.query) filter.query = params.query
if (params.from) filter.from = params.from
if (params.to) filter.to = params.to
const page: Record<string, string | number> = {}
if (params.cursor) page.cursor = params.cursor
if (params.limit !== undefined) page.limit = params.limit
const body: Record<string, unknown> = {}
if (Object.keys(filter).length > 0) body.filter = filter
if (Object.keys(page).length > 0) body.page = page
if (params.sort) body.sort = params.sort
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { signals: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
signals: (data.data ?? []).map((signal: DatadogV2Resource<SecuritySignalAttributes>) => ({
id: signal.id,
type: signal.type,
attributes: signal.attributes ?? {},
})),
nextCursor: data.meta?.page?.after,
},
}
},
outputs: {
signals: {
type: 'array',
description: 'List of security signals',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Signal ID' },
type: { type: 'string', description: 'Resource type (signal)' },
attributes: {
type: 'object',
description: 'Signal attributes',
properties: {
message: { type: 'string', description: 'Message from the detection rule' },
timestamp: { type: 'string', description: 'Signal timestamp' },
tags: { type: 'array', description: 'Tags on the signal' },
custom: { type: 'object', description: 'Signal-specific attributes' },
},
},
},
},
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page of signals',
optional: true,
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type {
DatadogV2Resource,
ListServicesParams,
ListServicesResponse,
ServiceDefinitionAttributes,
} from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listServicesTool: ToolConfig<ListServicesParams, ListServicesResponse> = {
id: 'datadog_list_services',
name: 'Datadog List Services',
description:
'List service definitions from the Datadog Service Catalog. Requires the `apm_service_catalog_read` permission.',
version: '1.0.0',
params: {
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of service definitions per page (default: 10, max: 100)',
},
pageNumber: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page to retrieve, starting at zero',
},
schemaVersion: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Schema version to return (e.g., "v2", "v2.1", "v2.2")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.pageSize !== undefined) queryParams.set('page[size]', String(params.pageSize))
if (params.pageNumber !== undefined)
queryParams.set('page[number]', String(params.pageNumber))
if (params.schemaVersion) queryParams.set('schema_version', params.schemaVersion)
const queryString = queryParams.toString()
return datadogApiUrl(
params.site,
`/api/v2/services/definitions${queryString ? `?${queryString}` : ''}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { services: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
services: (data.data ?? []).map(
(service: DatadogV2Resource<ServiceDefinitionAttributes>) => ({
id: service.id,
type: service.type,
schema: service.attributes?.schema ?? {},
meta: service.attributes?.meta ?? {},
})
),
},
}
},
outputs: {
services: {
type: 'array',
description: 'List of service definitions',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Service definition ID' },
type: { type: 'string', description: 'Resource type (service_definitions)' },
schema: {
type: 'object',
description:
'The service definition schema. Its shape depends on the requested schema version',
},
meta: {
type: 'object',
description: 'Ingestion metadata such as origin and last modified time',
},
},
},
},
},
}
+135
View File
@@ -0,0 +1,135 @@
import type { ListSlosParams, ListSlosResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listSlosTool: ToolConfig<ListSlosParams, ListSlosResponse> = {
id: 'datadog_list_slos',
name: 'Datadog List SLOs',
description:
'List service level objectives, optionally filtered by IDs, name, tags, or underlying metrics query.',
version: '1.0.0',
params: {
ids: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated SLO IDs to fetch (e.g., "id1,id2")',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter results by SLO name (e.g., "checkout latency")',
},
tagsQuery: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter results by a single SLO tag (e.g., "env:prod")',
},
metricsQuery: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter results by SLO numerator and denominator (e.g., "aws.elb.request_count")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of SLOs to return (default: 1000)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Offset of the first SLO returned (e.g., 0, 50)',
},
isDeleted: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Return only deleted SLOs',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.ids) queryParams.set('ids', params.ids)
if (params.query) queryParams.set('query', params.query)
if (params.tagsQuery) queryParams.set('tags_query', params.tagsQuery)
if (params.metricsQuery) queryParams.set('metrics_query', params.metricsQuery)
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
if (params.offset !== undefined) queryParams.set('offset', String(params.offset))
if (params.isDeleted) queryParams.set('is_deleted', 'true')
const queryString = queryParams.toString()
return datadogApiUrl(params.site, `/api/v1/slo${queryString ? `?${queryString}` : ''}`)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { slos: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
slos: data.data ?? [],
},
}
},
outputs: {
slos: {
type: 'array',
description: 'List of service level objectives',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'SLO ID' },
name: { type: 'string', description: 'SLO name' },
type: { type: 'string', description: 'SLO type: metric, monitor, or time_slice' },
description: { type: 'string', description: 'SLO description' },
tags: { type: 'array', description: 'SLO tags' },
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
target_threshold: { type: 'number', description: 'Primary target threshold' },
warning_threshold: { type: 'number', description: 'Primary warning threshold' },
timeframe: { type: 'string', description: 'Primary timeframe' },
monitor_ids: { type: 'array', description: 'Monitor IDs for monitor-based SLOs' },
created_at: { type: 'number', description: 'Creation timestamp (Unix seconds)' },
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
},
},
},
},
}
@@ -0,0 +1,115 @@
import type {
ListSyntheticsTestsParams,
ListSyntheticsTestsResponse,
SyntheticsTestData,
} from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const listSyntheticsTestsTool: ToolConfig<
ListSyntheticsTestsParams,
ListSyntheticsTestsResponse
> = {
id: 'datadog_list_synthetics_tests',
name: 'Datadog List Synthetic Tests',
description: 'List all Synthetic tests (API, browser, and mobile) with their current status.',
version: '1.0.0',
params: {
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of tests returned per page (default: 100)',
},
pageNumber: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page to retrieve, starting at zero',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.pageSize !== undefined) queryParams.set('page_size', String(params.pageSize))
if (params.pageNumber !== undefined) queryParams.set('page_number', String(params.pageNumber))
const queryString = queryParams.toString()
return datadogApiUrl(
params.site,
`/api/v1/synthetics/tests${queryString ? `?${queryString}` : ''}`
)
},
method: 'GET',
headers: datadogHeaders,
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { tests: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
tests: (data.tests ?? []).map((test: SyntheticsTestData) => ({
public_id: test.public_id,
name: test.name,
status: test.status,
type: test.type,
subtype: test.subtype,
message: test.message,
monitor_id: test.monitor_id,
tags: test.tags ?? [],
locations: test.locations ?? [],
})),
},
}
},
outputs: {
tests: {
type: 'array',
description: 'List of Synthetic tests',
items: {
type: 'object',
properties: {
public_id: { type: 'string', description: 'Public ID of the test' },
name: { type: 'string', description: 'Test name' },
status: { type: 'string', description: 'Pause status: live or paused' },
type: { type: 'string', description: 'Test type: api, browser, mobile, or network' },
subtype: { type: 'string', description: 'Test subtype, such as http or ssl' },
message: { type: 'string', description: 'Notification message' },
monitor_id: { type: 'number', description: 'Associated monitor ID' },
tags: { type: 'array', description: 'Tags attached to the test' },
locations: { type: 'array', description: 'Locations the test runs from' },
},
},
},
},
}
+31 -18
View File
@@ -1,10 +1,12 @@
import type { MuteMonitorParams, MuteMonitorResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse> = {
id: 'datadog_mute_monitor',
name: 'Datadog Mute Monitor',
description: 'Mute a monitor to temporarily suppress notifications.',
description:
'Mute a monitor to temporarily suppress its notifications. Use Unmute Monitor to reverse it, or schedule a downtime instead when you want a planned, auditable maintenance window.',
version: '1.0.0',
params: {
@@ -26,7 +28,7 @@ export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse>
required: false,
visibility: 'user-or-llm',
description:
'Unix timestamp in seconds when the mute should end (e.g., 1705323600). If not specified, mutes indefinitely.',
'Unix timestamp in seconds when the mute should end (e.g., 1705323600). If not specified, the monitor stays muted until it is unmuted.',
},
apiKey: {
type: 'string',
@@ -49,40 +51,36 @@ export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse>
},
request: {
url: (params) => {
const site = params.site || 'datadoghq.com'
return `https://api.${site}/api/v1/monitor/${params.monitorId}/mute`
},
url: (params) =>
datadogApiUrl(params.site, `/api/v1/monitor/${encodeURIComponent(params.monitorId)}/mute`),
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
'DD-API-KEY': params.apiKey,
'DD-APPLICATION-KEY': params.applicationKey,
}),
headers: datadogHeaders,
body: (params) => {
const body: Record<string, any> = {}
const body: { scope?: string; end?: number } = {}
if (params.scope) body.scope = params.scope
if (params.end) body.end = params.end
if (params.end !== undefined) body.end = params.end
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
return {
success: false,
output: {
success: false,
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
output: { success: false },
error: await datadogErrorMessage(response),
}
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
success: true,
monitorId: data.id,
name: data.name,
overallState: data.overall_state,
},
}
},
@@ -92,5 +90,20 @@ export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse>
type: 'boolean',
description: 'Whether the monitor was successfully muted',
},
monitorId: {
type: 'number',
description: 'ID of the muted monitor',
optional: true,
},
name: {
type: 'string',
description: 'Name of the muted monitor',
optional: true,
},
overallState: {
type: 'string',
description: 'Monitor state after muting',
optional: true,
},
},
}
+28 -5
View File
@@ -1,4 +1,10 @@
import type { QueryLogsParams, QueryLogsResponse } from '@/tools/datadog/types'
import type {
DatadogV2Resource,
LogAttributes,
QueryLogsParams,
QueryLogsResponse,
} from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
@@ -36,6 +42,13 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
visibility: 'user-or-llm',
description: 'Maximum number of logs to return (e.g., 50, 100, max: 1000)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Pagination cursor from a previous call, taken from its nextLogId output. Omit for the first page.',
},
sort: {
type: 'string',
required: false,
@@ -80,7 +93,11 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
'DD-APPLICATION-KEY': params.applicationKey,
}),
body: (params) => {
const body: Record<string, any> = {
const body: {
filter: { query: string; from: string; to: string; indexes?: string[] }
page: { limit: number; cursor?: string }
sort?: 'timestamp' | '-timestamp'
} = {
filter: {
query: params.query,
from: params.from,
@@ -91,6 +108,10 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
},
}
if (params.cursor) {
body.page.cursor = params.cursor
}
if (params.sort) {
body.sort = params.sort
}
@@ -108,18 +129,18 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
logs: [],
},
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
const data = await response.json()
const logs = (data.data || []).map((log: any) => ({
const logs = (data.data || []).map((log: DatadogV2Resource<LogAttributes>) => ({
id: log.id,
content: {
timestamp: log.attributes?.timestamp,
@@ -158,6 +179,8 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
service: { type: 'string', description: 'Service name' },
message: { type: 'string', description: 'Log message' },
status: { type: 'string', description: 'Log status/level' },
attributes: { type: 'json', description: 'Free-form log attributes' },
tags: { type: 'array', description: 'Log tags' },
},
},
},
+42 -5
View File
@@ -1,4 +1,9 @@
import type { QueryTimeseriesParams, QueryTimeseriesResponse } from '@/tools/datadog/types'
import type {
MetricsQuerySeries,
QueryTimeseriesParams,
QueryTimeseriesResponse,
} from '@/tools/datadog/types'
import { datadogErrorMessage } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeseriesResponse> = {
@@ -68,19 +73,33 @@ export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeser
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
series: [],
status: 'error',
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
const data = await response.json()
const series = (data.series || []).map((s: any) => ({
/**
* A failed metrics query still returns 200; Datadog signals it with a non-`ok`
* `status` and puts the reason in `error`. Without this the caller gets an empty
* series and no indication anything went wrong.
*/
if (data.status && data.status !== 'ok') {
return {
success: false,
output: { series: [], status: data.status },
error: data.error || `Datadog returned query status "${data.status}"`,
}
}
const series = (data.series || []).map((s: MetricsQuerySeries) => ({
metric: s.metric || s.expression,
tags: s.tag_set || [],
points: (s.pointlist || []).map((p: [number, number]) => ({
@@ -93,7 +112,7 @@ export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeser
success: true,
output: {
series,
status: data.status || 'ok',
status: data.status,
},
}
},
@@ -102,6 +121,24 @@ export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeser
series: {
type: 'array',
description: 'Array of timeseries data with metric name, tags, and data points',
items: {
type: 'object',
properties: {
metric: { type: 'string', description: 'Metric name' },
tags: { type: 'array', description: 'Tags attached to the series' },
points: {
type: 'array',
description: 'Data points',
items: {
type: 'object',
properties: {
timestamp: { type: 'number', description: 'Point timestamp (Unix seconds)' },
value: { type: 'number', description: 'Point value' },
},
},
},
},
},
},
status: {
type: 'string',
+163
View File
@@ -0,0 +1,163 @@
import type {
DatadogV2Resource,
SearchSpansParams,
SearchSpansResponse,
SpanAttributes,
} from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const searchSpansTool: ToolConfig<SearchSpansParams, SearchSpansResponse> = {
id: 'datadog_search_spans',
name: 'Datadog Search Spans',
description: 'Search indexed APM spans using the span query syntax, with cursor pagination.',
version: '1.0.0',
params: {
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Span search query (e.g., "service:web* AND @http.status_code:[500 TO 599]"). Defaults to "*"',
},
from: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Minimum time, ISO-8601, date math, or milliseconds (default: "now-15m")',
},
to: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum time, ISO-8601, date math, or milliseconds (default: "now")',
},
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: "timestamp" for oldest first, "-timestamp" for newest first',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned as nextCursor by a previous call',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of spans to return (default: 10, max: 1000)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, '/api/v2/spans/events/search'),
method: 'POST',
headers: datadogHeaders,
body: (params) => {
const filter: Record<string, string> = {}
if (params.query) filter.query = params.query
if (params.from) filter.from = params.from
if (params.to) filter.to = params.to
const page: Record<string, string | number> = {}
if (params.cursor) page.cursor = params.cursor
if (params.limit !== undefined) page.limit = params.limit
const attributes: Record<string, unknown> = {}
if (Object.keys(filter).length > 0) attributes.filter = filter
if (Object.keys(page).length > 0) attributes.page = page
if (params.sort) attributes.sort = params.sort
return { data: { type: 'search_request', attributes } }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { spans: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
spans: (data.data ?? []).map((span: DatadogV2Resource<SpanAttributes>) => ({
id: span.id,
type: span.type,
attributes: span.attributes ?? {},
})),
nextCursor: data.meta?.page?.after,
elapsed: data.meta?.elapsed,
},
}
},
outputs: {
spans: {
type: 'array',
description: 'List of matching spans',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Unique span event ID' },
type: { type: 'string', description: 'Resource type (spans)' },
attributes: {
type: 'object',
description: 'Span attributes',
properties: {
service: { type: 'string', description: 'Service that emitted the span' },
resource_name: { type: 'string', description: 'Resource name' },
env: { type: 'string', description: 'Environment' },
host: { type: 'string', description: 'Host that emitted the span' },
type: { type: 'string', description: 'Span type, such as web or db' },
trace_id: { type: 'string', description: 'Trace ID' },
span_id: { type: 'string', description: 'Span ID' },
parent_id: { type: 'string', description: 'Parent span ID' },
start_timestamp: { type: 'string', description: 'Span start timestamp' },
end_timestamp: { type: 'string', description: 'Span end timestamp' },
tags: { type: 'array', description: 'Tags on the span' },
custom: { type: 'object', description: 'Custom span data' },
},
},
},
},
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page of spans',
optional: true,
},
elapsed: {
type: 'number',
description: 'Query time in milliseconds',
optional: true,
},
},
}
+16 -16
View File
@@ -1,4 +1,6 @@
import type { SendLogsParams, SendLogsResponse } from '@/tools/datadog/types'
import { filterUndefined } from '@sim/utils/object'
import type { LogEntry, SendLogsParams, SendLogsResponse } from '@/tools/datadog/types'
import { datadogErrorMessage, parseJsonParam } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const sendLogsTool: ToolConfig<SendLogsParams, SendLogsResponse> = {
@@ -47,33 +49,31 @@ export const sendLogsTool: ToolConfig<SendLogsParams, SendLogsResponse> = {
'DD-API-KEY': params.apiKey,
}),
body: (params) => {
let logs: any[]
try {
logs = typeof params.logs === 'string' ? JSON.parse(params.logs) : params.logs
} catch {
throw new Error('Invalid JSON in logs parameter')
const logs = parseJsonParam<LogEntry[]>(params.logs, 'logs parameter')
if (!Array.isArray(logs) || logs.length === 0) {
throw new Error('logs must be a non-empty JSON array of log entries')
}
// Ensure each log entry has the required format
return logs.map((log: any) => ({
ddsource: log.ddsource || 'custom',
ddtags: log.ddtags || '',
hostname: log.hostname || '',
message: log.message,
service: log.service || '',
}))
/**
* Every extra key is preserved: Datadog's log intake accepts arbitrary
* additional properties as the log's structured attributes, so rebuilding each
* entry from a fixed field list would silently discard them. Empty optional
* fields are dropped rather than sent blank, which would otherwise suppress
* Datadog's own host inference and write an empty reserved `service` attribute.
*/
return logs.map((log) => filterUndefined({ ...log, ddsource: log.ddsource || 'custom' }))
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
success: false,
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
error: message,
}
}
+37 -25
View File
@@ -1,6 +1,23 @@
import type { SubmitMetricsParams, SubmitMetricsResponse } from '@/tools/datadog/types'
import { filterUndefined } from '@sim/utils/object'
import type {
MetricSeries,
SubmitMetricsParams,
SubmitMetricsResponse,
} from '@/tools/datadog/types'
import { datadogErrorMessage, parseJsonParam } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Datadog `MetricIntakeType` codes: 0 unspecified, 1 count, 2 rate, 3 gauge.
* A metric with no recognized type is submitted without a `type`, letting Datadog
* infer it rather than silently mislabeling the series.
*/
const METRIC_INTAKE_TYPE = {
count: 1,
rate: 2,
gauge: 3,
} as const
export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsResponse> = {
id: 'datadog_submit_metrics',
name: 'Datadog Submit Metrics',
@@ -14,7 +31,7 @@ export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsRes
required: true,
visibility: 'user-or-llm',
description:
'JSON array of metric series to submit. Each series should include metric name, type (gauge/rate/count), points (timestamp/value pairs), and optional tags.',
'JSON array of metric series to submit. Each entry needs "metric" and "points" (objects with "timestamp" in POSIX seconds and a numeric "value"); timestamps cannot be more than 10 minutes in the future or 1 hour in the past. Optional per entry: "type" ("count", "rate", or "gauge"; omit to let Datadog infer), "interval" in seconds (required by Datadog for count and rate), "tags", "unit", "sourceTypeName", and "resources".',
},
apiKey: {
type: 'string',
@@ -41,25 +58,23 @@ export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsRes
'DD-API-KEY': params.apiKey,
}),
body: (params) => {
let series: any[]
try {
series = typeof params.series === 'string' ? JSON.parse(params.series) : params.series
} catch {
throw new Error('Invalid JSON in series parameter')
const series = parseJsonParam<MetricSeries[]>(params.series, 'series parameter')
if (!Array.isArray(series) || series.length === 0) {
throw new Error('series must be a non-empty JSON array of metric series')
}
// Transform to Datadog API v2 format
const formattedSeries = series.map((s: any) => ({
metric: s.metric,
type: s.type === 'gauge' ? 0 : s.type === 'rate' ? 1 : s.type === 'count' ? 2 : 3,
points: s.points.map((p: any) => ({
timestamp: p.timestamp,
value: p.value,
})),
tags: s.tags || [],
unit: s.unit,
resources: s.resources || [{ name: 'host', type: 'host' }],
}))
const formattedSeries = series.map((s) =>
filterUndefined({
metric: s.metric,
type: METRIC_INTAKE_TYPE[s.type as keyof typeof METRIC_INTAKE_TYPE],
points: s.points?.map((p) => ({ timestamp: p.timestamp, value: p.value })),
tags: s.tags,
unit: s.unit,
interval: s.interval,
source_type_name: s.sourceTypeName,
resources: s.resources,
})
)
return { series: formattedSeries }
},
@@ -67,14 +82,11 @@ export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsRes
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const message = await datadogErrorMessage(response)
return {
success: false,
output: {
success: false,
errors: [errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`],
},
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
output: { success: false, errors: [message] },
error: message,
}
}
@@ -0,0 +1,118 @@
import type {
TriggerSyntheticsTestsParams,
TriggerSyntheticsTestsResponse,
} from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const triggerSyntheticsTestsTool: ToolConfig<
TriggerSyntheticsTestsParams,
TriggerSyntheticsTestsResponse
> = {
id: 'datadog_trigger_synthetics_tests',
name: 'Datadog Trigger Synthetic Tests',
description: 'Trigger an immediate run of one or more Synthetic tests by public ID.',
version: '1.0.0',
params: {
publicIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated public IDs of the Synthetic tests to trigger',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, '/api/v1/synthetics/tests/trigger'),
method: 'POST',
headers: datadogHeaders,
body: (params) => ({
tests: (splitCommaList(params.publicIds) ?? []).map((publicId) => ({
public_id: publicId,
})),
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { triggeredCheckIds: [], results: [], locations: [] },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
batchId: data.batch_id,
triggeredCheckIds: data.triggered_check_ids ?? [],
results: data.results ?? [],
locations: data.locations ?? [],
},
}
},
outputs: {
batchId: {
type: 'string',
description: 'Public ID of the triggered batch',
optional: true,
},
triggeredCheckIds: {
type: 'array',
description: 'Public IDs of the triggered Synthetic tests',
items: { type: 'string' },
},
results: {
type: 'array',
description: 'Information about each triggered test run',
items: {
type: 'object',
properties: {
public_id: { type: 'string', description: 'Public ID of the test' },
result_id: { type: 'string', description: 'ID of the run result' },
location: { type: 'number', description: 'Location ID of the run' },
device: { type: 'string', description: 'Device ID used for browser tests' },
},
},
},
locations: {
type: 'array',
description: 'Locations the tests were triggered from',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Location ID' },
name: { type: 'string', description: 'Location name' },
},
},
},
},
}
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
import type { UnmuteMonitorParams, UnmuteMonitorResponse } from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const unmuteMonitorTool: ToolConfig<UnmuteMonitorParams, UnmuteMonitorResponse> = {
id: 'datadog_unmute_monitor',
name: 'Datadog Unmute Monitor',
description:
'Unmute a monitor so it resumes sending notifications. Reverses Mute Monitor, either for one scope or for every scope at once.',
version: '1.0.0',
params: {
monitorId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the monitor to unmute (e.g., "12345678")',
},
scope: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Scope to unmute (e.g., "host:myhost"). Leave blank to unmute the monitor itself rather than a single scope.',
},
allScopes: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Clear the mute settings for every scope on this monitor',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(params.site, `/api/v1/monitor/${encodeURIComponent(params.monitorId)}/unmute`),
method: 'POST',
headers: datadogHeaders,
body: (params) => {
const body: { scope?: string; all_scopes?: boolean } = {}
if (params.scope) body.scope = params.scope
if (params.allScopes !== undefined) body.all_scopes = params.allScopes
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { success: false },
error: await datadogErrorMessage(response),
}
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
success: true,
monitorId: data.id,
name: data.name,
overallState: data.overall_state,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the monitor was successfully unmuted',
},
monitorId: {
type: 'number',
description: 'ID of the unmuted monitor',
optional: true,
},
name: {
type: 'string',
description: 'Name of the unmuted monitor',
optional: true,
},
overallState: {
type: 'string',
description: 'Monitor state after unmuting',
optional: true,
},
},
}
+180
View File
@@ -0,0 +1,180 @@
import type { UpdateIncidentParams, UpdateIncidentResponse } from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
parseJsonParam,
splitCommaList,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const updateIncidentTool: ToolConfig<UpdateIncidentParams, UpdateIncidentResponse> = {
id: 'datadog_update_incident',
name: 'Datadog Update Incident',
description:
'Partially update an existing incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta.',
version: '1.0.0',
params: {
incidentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the incident to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the incident',
},
severity: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Incident severity: UNKNOWN, SEV-0, SEV-1, SEV-2, SEV-3, SEV-4, or SEV-5',
},
customerImpacted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the incident caused customer impact',
},
customerImpactScope: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Summary of the customer impact',
},
customerImpactStart: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO-8601 timestamp when customers began being impacted',
},
customerImpactEnd: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO-8601 timestamp when customers were no longer impacted',
},
detected: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ISO-8601 timestamp when the incident was detected',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON object of user-defined incident fields to update, e.g. {"state": {"type": "dropdown", "value": "resolved"}}',
},
notificationHandles: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated handles to notify about the update',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(params.site, `/api/v2/incidents/${encodeURIComponent(params.incidentId)}`),
method: 'PATCH',
headers: datadogHeaders,
body: (params) => {
const fields =
parseJsonParam<Record<string, unknown>>(params.fields, 'fields parameter') ?? {}
if (params.severity) {
fields.severity = { type: 'dropdown', value: params.severity }
}
/**
* Only non-empty values are sent. An empty string is how an untouched input
* arrives, and Datadog would take it literally blanking the stored title or
* rejecting the request as an invalid `date-time`.
*/
const attributes: Record<string, unknown> = {}
if (params.title) attributes.title = params.title
if (params.customerImpacted !== undefined)
attributes.customer_impacted = params.customerImpacted
if (params.customerImpactScope) attributes.customer_impact_scope = params.customerImpactScope
if (params.customerImpactStart) attributes.customer_impact_start = params.customerImpactStart
if (params.customerImpactEnd) attributes.customer_impact_end = params.customerImpactEnd
if (params.detected) attributes.detected = params.detected
if (Object.keys(fields).length > 0) attributes.fields = fields
const handles = splitCommaList(params.notificationHandles)
if (handles) {
attributes.notification_handles = handles.map((handle) => ({ handle }))
}
return { data: { type: 'incidents', id: params.incidentId, attributes } }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { incident: { id: '', attributes: {} } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: {
incident: {
id: data.data?.id,
type: data.data?.type,
attributes: data.data?.attributes ?? {},
},
},
}
},
outputs: {
incident: {
type: 'object',
description: 'The updated incident',
properties: {
id: { type: 'string', description: 'Incident UUID' },
type: { type: 'string', description: 'Resource type (incidents)' },
attributes: {
type: 'object',
description: 'Incident attributes',
properties: {
title: { type: 'string', description: 'Incident title' },
state: { type: 'string', description: 'Incident state' },
severity: { type: 'string', description: 'Incident severity' },
modified: { type: 'string', description: 'Last modification timestamp' },
resolved: { type: 'string', description: 'Resolution timestamp' },
},
},
},
},
},
}
@@ -0,0 +1,104 @@
import type {
UpdateSecuritySignalAssigneeParams,
UpdateSecuritySignalAssigneeResponse,
} from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
mapSignalTriageData,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const updateSecuritySignalAssigneeTool: ToolConfig<
UpdateSecuritySignalAssigneeParams,
UpdateSecuritySignalAssigneeResponse
> = {
id: 'datadog_update_security_signal_assignee',
name: 'Datadog Assign Security Signal',
description:
'Assign a Cloud SIEM security signal to a Datadog user by UUID. Requires the `security_monitoring_signals_write` permission.',
version: '1.0.0',
params: {
signalId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the security signal',
},
assigneeUuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'UUID of the Datadog user to assign the signal to (e.g., "773b045d-ccf8-4808-bd3b-955ef6a8c940")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(
params.site,
`/api/v2/security_monitoring/signals/${encodeURIComponent(params.signalId)}/assignee`
),
method: 'PATCH',
headers: datadogHeaders,
body: (params) => ({
data: { attributes: { assignee: { uuid: params.assigneeUuid } } },
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { signal: {} },
error: await datadogErrorMessage(response),
}
}
return {
success: true,
output: { signal: mapSignalTriageData(await response.json()) },
}
},
outputs: {
signal: {
type: 'object',
description: 'The updated signal triage data',
properties: {
id: { type: 'string', description: 'Signal ID' },
type: { type: 'string', description: 'Resource type of the signal' },
state: { type: 'string', description: 'Current triage state' },
assignee: { type: 'object', description: 'User the signal is assigned to' },
incidentIds: { type: 'array', description: 'IDs of incidents linked to the signal' },
archiveReason: { type: 'string', description: 'Archive reason, when archived' },
archiveComment: { type: 'string', description: 'Archive comment, when archived' },
stateUpdateTimestamp: {
type: 'number',
description: 'Timestamp of the last state update',
},
},
},
},
}
@@ -0,0 +1,119 @@
import type {
UpdateSecuritySignalStateParams,
UpdateSecuritySignalStateResponse,
} from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
mapSignalTriageData,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const updateSecuritySignalStateTool: ToolConfig<
UpdateSecuritySignalStateParams,
UpdateSecuritySignalStateResponse
> = {
id: 'datadog_update_security_signal_state',
name: 'Datadog Update Security Signal State',
description:
'Change the triage state of a Cloud SIEM security signal to open, under_review, or archived. Requires the `security_monitoring_signals_write` permission.',
version: '1.0.0',
params: {
signalId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the security signal',
},
state: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New triage state: "open", "under_review", or "archived"',
},
archiveReason: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Reason when archiving: none, false_positive, testing_or_maintenance, remediated, investigated_case_opened, true_positive_benign, true_positive_malicious, or other',
},
archiveComment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comment explaining why the signal was archived',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(
params.site,
`/api/v2/security_monitoring/signals/${encodeURIComponent(params.signalId)}/state`
),
method: 'PATCH',
headers: datadogHeaders,
body: (params) => {
const attributes: Record<string, unknown> = { state: params.state }
if (params.archiveReason) attributes.archive_reason = params.archiveReason
if (params.archiveComment) attributes.archive_comment = params.archiveComment
return { data: { attributes } }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
return {
success: false,
output: { signal: {} },
error: await datadogErrorMessage(response),
}
}
return {
success: true,
output: { signal: mapSignalTriageData(await response.json()) },
}
},
outputs: {
signal: {
type: 'object',
description: 'The updated signal triage data',
properties: {
id: { type: 'string', description: 'Signal ID' },
type: { type: 'string', description: 'Resource type of the signal' },
state: { type: 'string', description: 'Current triage state' },
assignee: { type: 'object', description: 'User the signal is assigned to' },
incidentIds: { type: 'array', description: 'IDs of incidents linked to the signal' },
archiveReason: { type: 'string', description: 'Archive reason, when archived' },
archiveComment: { type: 'string', description: 'Archive comment, when archived' },
stateUpdateTimestamp: {
type: 'number',
description: 'Timestamp of the last state update',
},
},
},
},
}
+185
View File
@@ -0,0 +1,185 @@
import type { UpdateSloParams, UpdateSloResponse } from '@/tools/datadog/types'
import {
datadogApiUrl,
datadogErrorMessage,
datadogHeaders,
mergeSloUpdatePayload,
} from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const updateSloTool: ToolConfig<UpdateSloParams, UpdateSloResponse> = {
id: 'datadog_update_slo',
name: 'Datadog Update SLO',
description:
'Update a service level objective. Reads the current SLO first and applies only the fields you supply, so anything left blank keeps its stored value.',
version: '1.0.0',
params: {
sloId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the service level objective to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New name for the SLO. Leave blank to keep the current name.',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'SLO type: "metric" or "monitor". Leave blank to keep the current type. Changing type requires supplying the matching query or monitorIds.',
},
thresholds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of thresholds replacing the stored ones, e.g. [{"timeframe": "30d", "target": 99.9, "warning": 99.95}]. Leave blank to keep the current thresholds.',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description of the SLO',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated tags (e.g., "env:prod,team:core")',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'For metric SLOs, JSON with numerator and denominator, e.g. {"numerator": "sum:requests{status:ok}.as_count()", "denominator": "sum:requests{*}.as_count()"}',
},
monitorIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'For monitor SLOs, comma-separated monitor IDs (e.g., "123,456")',
},
groups: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated monitor groups (e.g., "env:prod,role:mysql")',
},
targetThreshold: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Primary target threshold (e.g., 99.9)',
},
warningThreshold: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Primary warning threshold, must be greater than the target (e.g., 99.95)',
},
timeframe: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Primary timeframe: "7d", "30d", or "90d"',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) => datadogApiUrl(params.site, `/api/v1/slo/${encodeURIComponent(params.sloId)}`),
method: 'PUT',
headers: datadogHeaders,
},
/**
* Datadog's SLO update is a full replacement, so the stored SLO is read first and
* the supplied edits are overlaid onto it. Sending only the filled-in fields would
* erase every field the caller left blank.
*/
directExecution: async (params, signal) => {
const url = datadogApiUrl(params.site, `/api/v1/slo/${encodeURIComponent(params.sloId)}`)
const headers = datadogHeaders(params)
const existingResponse = await fetch(url, { method: 'GET', headers, signal })
if (!existingResponse.ok) {
return {
success: false,
output: { slo: { id: '', name: '', type: '' } },
error: `Could not load SLO ${params.sloId} before updating it: ${await datadogErrorMessage(existingResponse)}`,
}
}
const existing = await existingResponse.json()
const stored = existing.data
if (!stored || typeof stored !== 'object') {
return {
success: false,
output: { slo: { id: '', name: '', type: '' } },
error: `Datadog returned no SLO for id ${params.sloId}`,
}
}
const response = await fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(mergeSloUpdatePayload(stored, params)),
signal,
})
if (!response.ok) {
return {
success: false,
output: { slo: { id: '', name: '', type: '' } },
error: await datadogErrorMessage(response),
}
}
const data = await response.json()
return {
success: true,
output: { slo: data.data?.[0] ?? { id: '', name: '', type: '' } },
}
},
outputs: {
slo: {
type: 'object',
description: 'The updated service level objective',
properties: {
id: { type: 'string', description: 'SLO ID' },
name: { type: 'string', description: 'SLO name' },
type: { type: 'string', description: 'SLO type' },
description: { type: 'string', description: 'SLO description' },
tags: { type: 'array', description: 'SLO tags' },
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
},
},
},
}
@@ -0,0 +1,93 @@
import type {
UpdateSyntheticsStatusParams,
UpdateSyntheticsStatusResponse,
} from '@/tools/datadog/types'
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
import type { ToolConfig } from '@/tools/types'
export const updateSyntheticsStatusTool: ToolConfig<
UpdateSyntheticsStatusParams,
UpdateSyntheticsStatusResponse
> = {
id: 'datadog_update_synthetics_status',
name: 'Datadog Pause Or Start Synthetic Test',
description: 'Pause or resume a Synthetic test by setting its status to "paused" or "live".',
version: '1.0.0',
params: {
publicId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The public ID of the Synthetic test to update',
},
newStatus: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New status: "live" to start the test or "paused" to pause it',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog API key',
},
applicationKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Datadog Application key',
},
site: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Datadog site/region (default: datadoghq.com)',
},
},
request: {
url: (params) =>
datadogApiUrl(
params.site,
`/api/v1/synthetics/tests/${encodeURIComponent(params.publicId)}/status`
),
method: 'PUT',
headers: datadogHeaders,
body: (params) => ({ new_status: params.newStatus }),
},
transformResponse: async (response: Response, params) => {
const status = params?.newStatus
if (!response.ok) {
return {
success: false,
output: { success: false, status },
error: await datadogErrorMessage(response),
}
}
const updated = await response.json().catch(() => false)
return {
success: true,
output: {
success: updated === true,
status,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether Datadog reported the status update as successful',
},
status: {
type: 'string',
description: 'The status that was requested: live or paused',
},
},
}
+235
View File
@@ -0,0 +1,235 @@
import type {
CreateSloParams,
DatadogSite,
SecuritySignalTriageData,
UpdateSloParams,
} from '@/tools/datadog/types'
/**
* Builds a fully-qualified Datadog API URL for the caller's site/region.
* Datadog serves each region from its own host (`datadoghq.com`, `datadoghq.eu`,
* `ddog-gov.com`, ...), so every request must be built from the configured site.
*/
export function datadogApiUrl(site: DatadogSite | undefined, path: string): string {
return `https://api.${site || 'datadoghq.com'}${path}`
}
/** Standard Datadog authentication headers for API + application key auth. */
export function datadogHeaders(params: {
apiKey: string
applicationKey: string
}): Record<string, string> {
return {
'Content-Type': 'application/json',
'DD-API-KEY': params.apiKey,
'DD-APPLICATION-KEY': params.applicationKey,
}
}
/** Reads one Datadog error entry, which is a plain string (v1) or a JSON:API object (v2). */
function errorEntryMessage(entry: unknown): string | null {
if (typeof entry === 'string') return entry
if (entry && typeof entry === 'object') {
const record = entry as { detail?: unknown; title?: unknown }
if (typeof record.detail === 'string') return record.detail
if (typeof record.title === 'string') return record.title
}
return null
}
/**
* Extracts a human-readable message from a failed Datadog response.
*
* Datadog returns `{ errors: ... }` in three shapes: an array of plain strings (v1),
* an array of JSON:API error objects carrying `detail`/`title` (v2), and on the SLO
* delete conflict a dictionary keyed by resource ID whose values are the reasons.
*/
export async function datadogErrorMessage(response: Response): Promise<string> {
const fallback = `HTTP ${response.status}: ${response.statusText}`
const body = await response.json().catch(() => null)
const errors = (body as { errors?: unknown })?.errors
const entries = Array.isArray(errors)
? errors
: errors && typeof errors === 'object'
? Object.values(errors as Record<string, unknown>)
: []
const messages = entries
.map(errorEntryMessage)
.filter((message): message is string => Boolean(message))
return messages.length > 0 ? messages.join('; ') : fallback
}
/**
* Splits a comma-separated user input into a trimmed, non-empty list.
*
* Accepts `unknown` because a `<Block.output>` reference can resolve to a non-string:
* a monitor ID read from `get_monitor` arrives as a number, and an LLM tool call can
* pass an array. Calling `.split` on those would throw before the request is built.
*/
export function splitCommaList(value: unknown): string[] | undefined {
if (value === undefined || value === null || value === '') return undefined
const items = (Array.isArray(value) ? value : String(value).split(','))
.map((item) => String(item).trim())
.filter((item) => item.length > 0)
return items.length > 0 ? items : undefined
}
/**
* Parses a JSON param, throwing a descriptive error when it is malformed.
* Block inputs arrive as strings, but an upstream block reference can resolve to
* an already-parsed object, so both shapes are accepted.
*/
export function parseJsonParam<T>(value: unknown, fieldName: string): T | undefined {
if (value === undefined || value === null || value === '') return undefined
if (typeof value !== 'string') return value as T
try {
return JSON.parse(value) as T
} catch {
throw new Error(`${fieldName} must be valid JSON`)
}
}
/**
* Parses a comma-separated monitor ID list into the integers Datadog expects.
* `Number('abc')` yields `NaN`, which `JSON.stringify` writes as `null` and Datadog
* rejects with a message that names nothing the user typed, so bad input is rejected here.
*/
export function parseMonitorIds(value: unknown): number[] | undefined {
const items = splitCommaList(value)
if (!items) return undefined
return items.map((id) => {
const parsed = Number(id)
if (!Number.isInteger(parsed)) {
throw new Error(`monitorIds must be a comma-separated list of whole numbers (got "${id}")`)
}
return parsed
})
}
/**
* Builds the `ServiceLevelObjective` request body for SLO creation.
*/
export function buildSloPayload(params: CreateSloParams): Record<string, unknown> {
const thresholds = parseJsonParam<unknown[]>(params.thresholds, 'thresholds parameter')
if (!Array.isArray(thresholds) || thresholds.length === 0) {
throw new Error('thresholds must be a non-empty JSON array')
}
const body: Record<string, unknown> = {
name: params.name,
type: params.type,
thresholds,
}
if (params.description) body.description = params.description
const tags = splitCommaList(params.tags)
if (tags) body.tags = tags
const query = parseJsonParam<Record<string, unknown>>(params.query, 'query parameter')
if (query) body.query = query
const monitorIds = parseMonitorIds(params.monitorIds)
if (monitorIds) body.monitor_ids = monitorIds
const groups = splitCommaList(params.groups)
if (groups) body.groups = groups
if (params.targetThreshold !== undefined) body.target_threshold = params.targetThreshold
if (params.warningThreshold !== undefined) body.warning_threshold = params.warningThreshold
if (params.timeframe) body.timeframe = params.timeframe
return body
}
/**
* Fields Datadog computes and rejects or ignores on an SLO update request.
* They must be stripped from a stored SLO before it is replayed into `PUT /api/v1/slo/{slo_id}`.
*/
const SLO_READ_ONLY_FIELDS = ['id', 'created_at', 'modified_at', 'creator', 'monitor_tags'] as const
/**
* Merges user-supplied SLO edits onto the SLO Datadog currently stores.
*
* `PUT /api/v1/slo/{slo_id}` is a full replacement, not a patch, so a request built
* only from the fields the user filled in silently erases every field they left
* blank. Reading the stored SLO first and overlaying only the supplied fields keeps
* an edit to one field from destroying the rest.
*/
export function mergeSloUpdatePayload(
stored: Record<string, unknown>,
params: UpdateSloParams
): Record<string, unknown> {
const body: Record<string, unknown> = { ...stored }
for (const field of SLO_READ_ONLY_FIELDS) delete body[field]
const thresholds = parseJsonParam<unknown[]>(params.thresholds, 'thresholds parameter')
if (thresholds !== undefined) {
if (!Array.isArray(thresholds) || thresholds.length === 0) {
throw new Error('thresholds must be a non-empty JSON array')
}
body.thresholds = thresholds
}
if (params.name) body.name = params.name
if (params.type) body.type = params.type
if (params.description !== undefined && params.description !== '') {
body.description = params.description
}
const tags = splitCommaList(params.tags)
if (tags) body.tags = tags
const query = parseJsonParam<Record<string, unknown>>(params.query, 'query parameter')
if (query) body.query = query
const monitorIds = parseMonitorIds(params.monitorIds)
if (monitorIds) body.monitor_ids = monitorIds
const groups = splitCommaList(params.groups)
if (groups) body.groups = groups
if (params.targetThreshold !== undefined) body.target_threshold = params.targetThreshold
if (params.warningThreshold !== undefined) body.warning_threshold = params.warningThreshold
if (params.timeframe) body.timeframe = params.timeframe
return body
}
/**
* Projects a security signal triage response (`PATCH .../state` and `.../assignee`
* both return `SecurityMonitoringSignalTriageUpdateResponse`) onto a flat shape.
*/
export function mapSignalTriageData(data: unknown): SecuritySignalTriageData {
const payload =
(
data as {
data?: {
id?: string
type?: string
attributes?: {
state?: string
assignee?: SecuritySignalTriageData['assignee']
incident_ids?: number[]
archive_reason?: string
archive_comment?: string
state_update_timestamp?: number
}
}
}
)?.data ?? {}
const attributes = payload.attributes ?? {}
return {
id: payload.id,
type: payload.type,
state: attributes.state,
assignee: attributes.assignee,
incidentIds: attributes.incident_ids,
archiveReason: attributes.archive_reason,
archiveComment: attributes.archive_comment,
stateUpdateTimestamp: attributes.state_update_timestamp,
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+58
View File
@@ -805,18 +805,47 @@ import {
databricksRunJobTool,
} from '@/tools/databricks'
import {
datadogAddIncidentTodoTool,
datadogCancelDowntimeTool,
datadogCreateDashboardTool,
datadogCreateDowntimeTool,
datadogCreateEventTool,
datadogCreateIncidentTool,
datadogCreateMonitorTool,
datadogCreateSloTool,
datadogDeleteDashboardTool,
datadogDeleteSloTool,
datadogGetBrowserSyntheticsResultsTool,
datadogGetDashboardTool,
datadogGetIncidentTool,
datadogGetMonitorTool,
datadogGetSecuritySignalTool,
datadogGetSloHistoryTool,
datadogGetSloTool,
datadogGetSyntheticsResultsTool,
datadogGetSyntheticsTestTool,
datadogListDashboardsTool,
datadogListDowntimesTool,
datadogListIncidentsTool,
datadogListMonitorsTool,
datadogListSecurityRulesTool,
datadogListSecuritySignalsTool,
datadogListServicesTool,
datadogListSlosTool,
datadogListSyntheticsTestsTool,
datadogMuteMonitorTool,
datadogQueryLogsTool,
datadogQueryTimeseriesTool,
datadogSearchSpansTool,
datadogSendLogsTool,
datadogSubmitMetricsTool,
datadogTriggerSyntheticsTestsTool,
datadogUnmuteMonitorTool,
datadogUpdateIncidentTool,
datadogUpdateSecuritySignalAssigneeTool,
datadogUpdateSecuritySignalStateTool,
datadogUpdateSloTool,
datadogUpdateSyntheticsStatusTool,
} from '@/tools/datadog'
import {
datagmaEnrichCompanyTool,
@@ -8661,6 +8690,35 @@ export const tools: Record<string, ToolConfig> = {
datadog_create_downtime: datadogCreateDowntimeTool,
datadog_list_downtimes: datadogListDowntimesTool,
datadog_cancel_downtime: datadogCancelDowntimeTool,
datadog_list_incidents: datadogListIncidentsTool,
datadog_get_incident: datadogGetIncidentTool,
datadog_create_incident: datadogCreateIncidentTool,
datadog_update_incident: datadogUpdateIncidentTool,
datadog_add_incident_todo: datadogAddIncidentTodoTool,
datadog_list_slos: datadogListSlosTool,
datadog_get_slo: datadogGetSloTool,
datadog_create_slo: datadogCreateSloTool,
datadog_unmute_monitor: datadogUnmuteMonitorTool,
datadog_update_slo: datadogUpdateSloTool,
datadog_delete_slo: datadogDeleteSloTool,
datadog_get_slo_history: datadogGetSloHistoryTool,
datadog_list_dashboards: datadogListDashboardsTool,
datadog_get_dashboard: datadogGetDashboardTool,
datadog_create_dashboard: datadogCreateDashboardTool,
datadog_delete_dashboard: datadogDeleteDashboardTool,
datadog_list_synthetics_tests: datadogListSyntheticsTestsTool,
datadog_get_synthetics_test: datadogGetSyntheticsTestTool,
datadog_get_synthetics_results: datadogGetSyntheticsResultsTool,
datadog_get_browser_synthetics_results: datadogGetBrowserSyntheticsResultsTool,
datadog_trigger_synthetics_tests: datadogTriggerSyntheticsTestsTool,
datadog_update_synthetics_status: datadogUpdateSyntheticsStatusTool,
datadog_list_security_signals: datadogListSecuritySignalsTool,
datadog_get_security_signal: datadogGetSecuritySignalTool,
datadog_update_security_signal_state: datadogUpdateSecuritySignalStateTool,
datadog_update_security_signal_assignee: datadogUpdateSecuritySignalAssigneeTool,
datadog_list_security_rules: datadogListSecurityRulesTool,
datadog_search_spans: datadogSearchSpansTool,
datadog_list_services: datadogListServicesTool,
image_generate: imageGenerateTool,
openai_image: openAIImageTool,
microsoft_ad_list_users: microsoftAdListUsersTool,