# Directory Structure
```
├── .github
│ ├── CODEOWNERS
│ └── workflows
│ ├── ci.yml
│ └── publish.yml
├── .gitignore
├── .husky
│ └── pre-commit
├── .prettierignore
├── .prettierrc
├── Dockerfile
├── eslint.config.js
├── jest.config.ts
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── smithery.yaml
├── src
│ ├── index.ts
│ ├── tools
│ │ ├── dashboards
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── downtimes
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── hosts
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── incident
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── logs
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── metrics
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── monitors
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ ├── rum
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ └── tool.ts
│ │ └── traces
│ │ ├── index.ts
│ │ ├── schema.ts
│ │ └── tool.ts
│ └── utils
│ ├── datadog.ts
│ ├── helper.ts
│ ├── tool.ts
│ └── types.ts
├── tests
│ ├── helpers
│ │ ├── datadog.ts
│ │ ├── mock.ts
│ │ └── msw.ts
│ ├── setup.ts
│ ├── tools
│ │ ├── dashboards.test.ts
│ │ ├── downtimes.test.ts
│ │ ├── hosts.test.ts
│ │ ├── incident.test.ts
│ │ ├── logs.test.ts
│ │ ├── metrics.test.ts
│ │ ├── monitors.test.ts
│ │ ├── rum.test.ts
│ │ └── traces.test.ts
│ └── utils
│ ├── datadog.test.ts
│ └── tool.test.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
```
pnpm-lock.yaml
```
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
{
"singleQuote": true,
"semi": false,
"useTabs": false,
"trailingComma": "all",
"printWidth": 80
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Datadog MCP Server
> **DISCLAIMER**: This is a community-maintained project and is not officially affiliated with, endorsed by, or supported by Datadog, Inc. This MCP server utilizes the Datadog API but is developed independently as part of the [Model Context Protocol](https://github.com/modelcontextprotocol/servers) ecosystem.
[](https://codecov.io/gh/winor30/mcp-server-datadog)[](https://smithery.ai/server/@winor30/mcp-server-datadog)
MCP server for the Datadog API, enabling incident management and more.
<a href="https://glama.ai/mcp/servers/bu8gtzkwfr">
<img width="380" height="200" src="https://glama.ai/mcp/servers/bu8gtzkwfr/badge" alt="mcp-server-datadog MCP server" />
</a>
## Features
- **Observability Tools**: Provides a mechanism to leverage key Datadog monitoring features, such as incidents, monitors, logs, dashboards, and metrics, through the MCP server.
- **Extensible Design**: Designed to easily integrate with additional Datadog APIs, allowing for seamless future feature expansion.
## Tools
1. `list_incidents`
- Retrieve a list of incidents from Datadog.
- **Inputs**:
- `filter` (optional string): Filter parameters for incidents (e.g., status, priority).
- `pagination` (optional object): Pagination details like page size/offset.
- **Returns**: Array of Datadog incidents and associated metadata.
2. `get_incident`
- Retrieve detailed information about a specific Datadog incident.
- **Inputs**:
- `incident_id` (string): Incident ID to fetch details for.
- **Returns**: Detailed incident information (title, status, timestamps, etc.).
3. `get_monitors`
- Fetch the status of Datadog monitors.
- **Inputs**:
- `groupStates` (optional array): States to filter (e.g., alert, warn, no data, ok).
- `name` (optional string): Filter by name.
- `tags` (optional array): Filter by tags.
- **Returns**: Monitors data and a summary of their statuses.
4. `get_logs`
- Search and retrieve logs from Datadog.
- **Inputs**:
- `query` (string): Datadog logs query string.
- `from` (number): Start time in epoch seconds.
- `to` (number): End time in epoch seconds.
- `limit` (optional number): Maximum number of logs to return (defaults to 100).
- **Returns**: Array of matching logs.
5. `list_dashboards`
- Get a list of dashboards from Datadog.
- **Inputs**:
- `name` (optional string): Filter dashboards by name.
- `tags` (optional array): Filter dashboards by tags.
- **Returns**: Array of dashboards with URL references.
6. `get_dashboard`
- Retrieve a specific dashboard from Datadog.
- **Inputs**:
- `dashboard_id` (string): ID of the dashboard to fetch.
- **Returns**: Dashboard details including title, widgets, etc.
7. `query_metrics`
- Retrieve metrics data from Datadog.
- **Inputs**:
- `query` (string): Metrics query string.
- `from` (number): Start time in epoch seconds.
- `to` (number): End time in epoch seconds.
- **Returns**: Metrics data for the queried timeframe.
8. `list_traces`
- Retrieve a list of APM traces from Datadog.
- **Inputs**:
- `query` (string): Datadog APM trace query string.
- `from` (number): Start time in epoch seconds.
- `to` (number): End time in epoch seconds.
- `limit` (optional number): Maximum number of traces to return (defaults to 100).
- `sort` (optional string): Sort order for traces (defaults to '-timestamp').
- `service` (optional string): Filter by service name.
- `operation` (optional string): Filter by operation name.
- **Returns**: Array of matching traces from Datadog APM.
9. `list_hosts`
- Get list of hosts from Datadog.
- **Inputs**:
- `filter` (optional string): Filter string for search results.
- `sort_field` (optional string): Field to sort hosts by.
- `sort_dir` (optional string): Sort direction (asc/desc).
- `start` (optional number): Starting offset for pagination.
- `count` (optional number): Max number of hosts to return (max: 1000).
- `from` (optional number): Search hosts from this UNIX timestamp.
- `include_muted_hosts_data` (optional boolean): Include muted hosts status and expiry.
- `include_hosts_metadata` (optional boolean): Include host metadata (version, platform, etc).
- **Returns**: Array of hosts with details including name, ID, aliases, apps, mute status, and more.
10. `get_active_hosts_count`
- Get the total number of active hosts in Datadog.
- **Inputs**:
- `from` (optional number): Number of seconds from which you want to get total number of active hosts (defaults to 2h).
- **Returns**: Count of total active and up hosts.
11. `mute_host`
- Mute a host in Datadog.
- **Inputs**:
- `hostname` (string): The name of the host to mute.
- `message` (optional string): Message to associate with the muting of this host.
- `end` (optional number): POSIX timestamp for when the mute should end.
- `override` (optional boolean): If true and the host is already muted, replaces existing end time.
- **Returns**: Success status and confirmation message.
12. `unmute_host`
- Unmute a host in Datadog.
- **Inputs**:
- `hostname` (string): The name of the host to unmute.
- **Returns**: Success status and confirmation message.
13. `list_downtimes`
- List scheduled downtimes from Datadog.
- **Inputs**:
- `currentOnly` (optional boolean): Return only currently active downtimes when true.
- `monitorId` (optional number): Filter by monitor ID.
- **Returns**: Array of scheduled downtimes with details including scope, monitor information, and schedule.
14. `schedule_downtime`
- Schedule a downtime in Datadog.
- **Inputs**:
- `scope` (string): Scope to apply downtime to (e.g. 'host:my-host').
- `start` (optional number): UNIX timestamp for the start of the downtime.
- `end` (optional number): UNIX timestamp for the end of the downtime.
- `message` (optional string): A message to include with the downtime.
- `timezone` (optional string): The timezone for the downtime (e.g. 'UTC', 'America/New_York').
- `monitorId` (optional number): The ID of the monitor to mute.
- `monitorTags` (optional array): A list of monitor tags for filtering.
- `recurrence` (optional object): Recurrence settings for the downtime.
- `type` (string): Recurrence type ('days', 'weeks', 'months', 'years').
- `period` (number): How often to repeat (must be >= 1).
- `weekDays` (optional array): Days of the week for weekly recurrence.
- `until` (optional number): UNIX timestamp for when the recurrence ends.
- **Returns**: Scheduled downtime details including ID and active status.
15. `cancel_downtime`
- Cancel a scheduled downtime in Datadog.
- **Inputs**:
- `downtimeId` (number): The ID of the downtime to cancel.
- **Returns**: Confirmation of downtime cancellation.
16. `get_rum_applications`
- Get all RUM applications in the organization.
- **Inputs**: None.
- **Returns**: List of RUM applications.
17. `get_rum_events`
- Search and retrieve RUM events from Datadog.
- **Inputs**:
- `query` (string): Datadog RUM query string.
- `from` (number): Start time in epoch seconds.
- `to` (number): End time in epoch seconds.
- `limit` (optional number): Maximum number of events to return (default: 100).
- **Returns**: Array of RUM events.
18. `get_rum_grouped_event_count`
- Search, group and count RUM events by a specified dimension.
- **Inputs**:
- `query` (optional string): Additional query filter for RUM search (default: "\*").
- `from` (number): Start time in epoch seconds.
- `to` (number): End time in epoch seconds.
- `groupBy` (optional string): Dimension to group results by (default: "application.name").
- **Returns**: Grouped event counts.
19. `get_rum_page_performance`
- Get page (view) performance metrics from RUM data.
- **Inputs**:
- `query` (optional string): Additional query filter for RUM search (default: "\*").
- `from` (number): Start time in epoch seconds.
- `to` (number): End time in epoch seconds.
- `metricNames` (array of strings): Array of metric names to retrieve (e.g., 'view.load_time', 'view.first_contentful_paint').
- **Returns**: Performance metrics including average, min, max, and count for each metric.
20. `get_rum_page_waterfall`
- Retrieve RUM page (view) waterfall data filtered by application name and session ID.
- **Inputs**:
- `applicationName` (string): Application name to filter events.
- `sessionId` (string): Session ID to filter events.
- **Returns**: Waterfall data for the specified application and session.
## Setup
### Datadog Credentials
You need valid Datadog API credentials to use this MCP server:
- `DATADOG_API_KEY`: Your Datadog API key
- `DATADOG_APP_KEY`: Your Datadog Application key
- `DATADOG_SITE` (optional): The Datadog site (e.g. `datadoghq.eu`)
- `DATADOG_SUBDOMAIN` (optional): The Datadog subdomain (e.g. `<your-subdomain>.datadoghq.com`)
Export them in your environment before running the server:
```bash
export DATADOG_API_KEY="your_api_key"
export DATADOG_APP_KEY="your_app_key"
export DATADOG_SITE="your_datadog_site" # Optional
export DATADOG_SUBDOMAIN="your_datadog_subdomain" # Optional
```
## Installation
### Installing via Smithery
To install Datadog MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@winor30/mcp-server-datadog):
```bash
npx -y @smithery/cli install @winor30/mcp-server-datadog --client claude
```
### Manual Installation
```bash
pnpm install
pnpm build
pnpm watch # for development with auto-rebuild
```
## Usage with Claude Desktop
To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
}
}
}
}
```
```json
{
"mcpServers": {
"datadog": {
"command": "/path/to/mcp-server-datadog/build/index.js",
"env": {
"DATADOG_API_KEY": "<YOUR_API_KEY>",
"DATADOG_APP_KEY": "<YOUR_APP_KEY>",
"DATADOG_SITE": "<YOUR_SITE>", // Optional
"DATADOG_SUBDOMAIN": "<YOUR_SUBDOMAIN>" // Optional
}
}
}
}
```
Or specify via `npx`:
```json
{
"mcpServers": {
"mcp-server-datadog": {
"command": "npx",
"args": ["-y", "@winor30/mcp-server-datadog"],
"env": {
"DATADOG_API_KEY": "<YOUR_API_KEY>",
"DATADOG_APP_KEY": "<YOUR_APP_KEY>",
"DATADOG_SITE": "<YOUR_SITE>", // Optional
"DATADOG_SUBDOMAIN": "<YOUR_SUBDOMAIN>" // Optional
}
}
}
}
```
## Debugging
Because MCP servers communicate over standard input/output, debugging can sometimes be tricky. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). You can run the inspector with:
```bash
npm run inspector
```
The inspector will provide a URL you can open in your browser to see logs and send requests manually.
## Contributing
Contributions are welcome! Feel free to open an issue or a pull request if you have any suggestions, bug reports, or improvements to propose.
## License
This project is licensed under the [Apache License, Version 2.0](./LICENSE).
```
--------------------------------------------------------------------------------
/src/tools/rum/index.ts:
--------------------------------------------------------------------------------
```typescript
export { RUM_TOOLS, createRumToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
```yaml
packages:
- .
onlyBuiltDependencies:
- esbuild
- msw
```
--------------------------------------------------------------------------------
/src/tools/logs/index.ts:
--------------------------------------------------------------------------------
```typescript
export { LOGS_TOOLS, createLogsToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/src/tools/traces/index.ts:
--------------------------------------------------------------------------------
```typescript
export { TRACES_TOOLS, createTracesToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/src/tools/metrics/index.ts:
--------------------------------------------------------------------------------
```typescript
export { METRICS_TOOLS, createMetricsToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/src/tools/incident/index.ts:
--------------------------------------------------------------------------------
```typescript
export { INCIDENT_TOOLS, createIncidentToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/src/tools/monitors/index.ts:
--------------------------------------------------------------------------------
```typescript
export { MONITORS_TOOLS, createMonitorsToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/src/tools/downtimes/index.ts:
--------------------------------------------------------------------------------
```typescript
export { DOWNTIMES_TOOLS, createDowntimesToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/src/tools/dashboards/index.ts:
--------------------------------------------------------------------------------
```typescript
export { DASHBOARDS_TOOLS, createDashboardsToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
```typescript
export default {
entry: ['src/index.ts'],
dts: true,
format: ['esm'],
outDir: 'build',
}
```
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
```typescript
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
}
```
--------------------------------------------------------------------------------
/tests/helpers/datadog.ts:
--------------------------------------------------------------------------------
```typescript
// Base URL for Datadog API
export const baseUrl = 'https://api.datadoghq.com/api'
export interface DatadogToolResponse {
content: {
type: 'text'
text: string
}[]
}
```
--------------------------------------------------------------------------------
/tests/setup.ts:
--------------------------------------------------------------------------------
```typescript
import { afterEach, vi } from 'vitest'
process.env.DATADOG_API_KEY = 'test-api-key'
process.env.DATADOG_APP_KEY = 'test-app-key'
// Reset handlers after each test
afterEach(() => {
// server.resetHandlers()
vi.clearAllMocks()
})
```
--------------------------------------------------------------------------------
/src/tools/incident/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const ListIncidentsZodSchema = z.object({
pageSize: z.number().min(1).max(100).default(10),
pageOffset: z.number().min(0).default(0),
})
export const GetIncidentZodSchema = z.object({
incidentId: z.string().nonempty(),
})
```
--------------------------------------------------------------------------------
/src/tools/dashboards/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const ListDashboardsZodSchema = z.object({
name: z.string().optional().describe('Filter dashboards by name'),
tags: z.array(z.string()).optional().describe('Filter dashboards by tags'),
})
export const GetDashboardZodSchema = z.object({
dashboardId: z.string(),
})
```
--------------------------------------------------------------------------------
/src/tools/monitors/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const GetMonitorsZodSchema = z.object({
groupStates: z
.array(z.enum(['alert', 'warn', 'no data', 'ok']))
.optional()
.describe('Filter monitors by their states'),
name: z.string().optional().describe('Filter monitors by name'),
tags: z.array(z.string()).optional().describe('Filter monitors by tags'),
})
```
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
```typescript
import z from 'zod'
import {
Result,
CallToolRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js'
type ToolHandler = (
request: z.infer<typeof CallToolRequestSchema>,
) => Promise<Result>
export type ToolHandlers<T extends string = string> = Record<T, ToolHandler>
export type ExtendedTool<T extends string = string> = Tool & { name: T }
```
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
setupFiles: ['./tests/setup.ts'],
include: ['./tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
exclude: ['node_modules/', 'tests/'],
},
},
})
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ['**/*.{js,mjs,cjs,ts}'] },
{ ignores: ['node_modules/**', 'build/**'] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
]
```
--------------------------------------------------------------------------------
/src/tools/hosts/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Central export file for the Datadog Hosts management tools.
* Re-exports the tools and their handlers from the implementation file.
*
* HOSTS_TOOLS: Array of tool schemas defining the available host management operations
* createHostsToolHandlers: Function that creates host management operation handlers
*/
export { HOSTS_TOOLS, createHostsToolHandlers } from './tool'
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "esnext",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/src/tools/metrics/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const QueryMetricsZodSchema = z.object({
from: z
.number()
.describe(
'Start of the queried time period, seconds since the Unix epoch.',
),
to: z
.number()
.describe('End of the queried time period, seconds since the Unix epoch.'),
query: z
.string()
.describe('Datadog metrics query string. e.g. "avg:system.cpu.user{*}'),
})
export type QueryMetricsArgs = z.infer<typeof QueryMetricsZodSchema>
```
--------------------------------------------------------------------------------
/tests/helpers/mock.ts:
--------------------------------------------------------------------------------
```typescript
interface MockToolRequest {
method: 'tools/call'
params: {
name: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arguments: Record<string, any>
}
}
export function createMockToolRequest(
toolName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: Record<string, any>,
): MockToolRequest {
return {
method: 'tools/call',
params: {
name: toolName,
arguments: args,
},
}
}
```
--------------------------------------------------------------------------------
/src/tools/traces/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const ListTracesZodSchema = z.object({
query: z.string().describe('Datadog APM trace query string'),
from: z.number().describe('Start time in epoch seconds'),
to: z.number().describe('End time in epoch seconds'),
limit: z
.number()
.optional()
.default(100)
.describe('Maximum number of traces to return'),
sort: z
.enum(['timestamp', '-timestamp'])
.optional()
.default('-timestamp')
.describe('Sort order for traces'),
service: z.string().optional().describe('Filter by service name'),
operation: z.string().optional().describe('Filter by operation name'),
})
export type ListTracesArgs = z.infer<typeof ListTracesZodSchema>
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Publish to npm
on:
push:
tags:
- 'v*.*.*'
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org/'
- uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish
run: pnpm publish --provenance --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/src/utils/helper.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Logs a formatted message with a specified severity to stderr.
*
* The MCP server uses stdio transport, so using console.log might interfere with the transport.
* Therefore, logging messages are written to stderr.
*
* @param {'info' | 'error'} severity - The severity level of the log message.
* @param {...any[]} args - Additional arguments to be logged, which will be concatenated into a single string.
*/
export function log(
severity: 'info' | 'error',
...args: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
) {
const msg = `[${severity.toUpperCase()} ${new Date().toISOString()}] ${args.join(' ')}\n`
process.stderr.write(msg)
}
export { version as mcpDatadogVersion } from '../../package.json'
export function unreachable(value: never): never {
throw new Error(`Unreachable code: ${value}`)
}
```
--------------------------------------------------------------------------------
/tests/helpers/msw.ts:
--------------------------------------------------------------------------------
```typescript
import { RequestHandler } from 'msw'
import { SetupServerApi, setupServer as setupServerNode } from 'msw/node'
export function setupServer(...handlers: RequestHandler[]) {
const server = setupServerNode(...handlers)
debugServer(server)
return server
}
function debugServer(server: SetupServerApi) {
// Enable network request debugging
server.listen({
onUnhandledRequest: 'warn',
})
// Log all requests that pass through MSW
server.events.on('request:start', ({ request }) => {
console.log(`[MSW] Request started: ${request.method} ${request.url}`)
})
server.events.on('request:match', ({ request }) => {
console.log(`[MSW] Request matched: ${request.method} ${request.url}`)
})
server.events.on('request:unhandled', ({ request }) => {
console.log(`[MSW] Request not handled: ${request.method} ${request.url}`)
})
}
```
--------------------------------------------------------------------------------
/src/tools/downtimes/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const ListDowntimesZodSchema = z.object({
currentOnly: z.boolean().optional(),
})
export const ScheduleDowntimeZodSchema = z.object({
scope: z.string().nonempty(), // example: 'host:my-host'
start: z.number().optional(), // UNIX timestamp
end: z.number().optional(), // UNIX timestamp
message: z.string().optional(),
timezone: z.string().optional(), // example: 'UTC', 'America/New_York'
monitorId: z.number().optional(),
monitorTags: z.array(z.string()).optional(),
recurrence: z
.object({
type: z.enum(['days', 'weeks', 'months', 'years']),
period: z.number().min(1),
weekDays: z
.array(z.enum(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']))
.optional(),
until: z.number().optional(), // UNIX timestamp
})
.optional(),
})
export const CancelDowntimeZodSchema = z.object({
downtimeId: z.number(),
})
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:22.12-alpine AS builder
# Install pnpm globally
RUN npm install -g pnpm@10
WORKDIR /app
# Copy package files and install dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --ignore-scripts
# Copy the rest of the files
COPY . .
# Build the project
RUN pnpm build
FROM node:22.12-alpine AS installer
# Install pnpm globally
RUN npm install -g pnpm@10
WORKDIR /app
# Copy package files and install only production dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --ignore-scripts --prod
FROM node:22.12-alpine AS release
WORKDIR /app
COPY --from=builder /app/build /app/build
COPY --from=installer /app/node_modules /app/node_modules
# Expose port if needed (Not explicitly mentioned, MCP runs via stdio, so not needed)
CMD ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/src/tools/metrics/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v1 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { QueryMetricsZodSchema } from './schema'
type MetricsToolName = 'query_metrics'
type MetricsTool = ExtendedTool<MetricsToolName>
export const METRICS_TOOLS: MetricsTool[] = [
createToolSchema(
QueryMetricsZodSchema,
'query_metrics',
'Query timeseries points of metrics from Datadog',
),
] as const
type MetricsToolHandlers = ToolHandlers<MetricsToolName>
export const createMetricsToolHandlers = (
apiInstance: v1.MetricsApi,
): MetricsToolHandlers => {
return {
query_metrics: async (request) => {
const { from, to, query } = QueryMetricsZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.queryMetrics({
from,
to,
query,
})
return {
content: [
{
type: 'text',
text: `Queried metrics data: ${JSON.stringify({ response })}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/src/utils/datadog.ts:
--------------------------------------------------------------------------------
```typescript
import { client } from '@datadog/datadog-api-client'
interface CreateDatadogConfigParams {
apiKeyAuth: string
appKeyAuth: string
site?: string
subdomain?: string
}
export function createDatadogConfig(
config: CreateDatadogConfigParams,
): client.Configuration {
if (!config.apiKeyAuth || !config.appKeyAuth) {
throw new Error('Datadog API key and APP key are required')
}
const datadogConfig = client.createConfiguration({
authMethods: {
apiKeyAuth: config.apiKeyAuth,
appKeyAuth: config.appKeyAuth,
},
})
if (config.site != null) {
datadogConfig.setServerVariables({
site: config.site,
})
}
if (config.subdomain != null) {
datadogConfig.setServerVariables({
subdomain: config.subdomain,
})
}
datadogConfig.unstableOperations = {
'v2.listIncidents': true,
'v2.getIncident': true,
}
return datadogConfig
}
export function getDatadogSite(ddConfig: client.Configuration): string {
const config = ddConfig.servers[0]?.getConfiguration()
if (config == null) {
throw new Error('Datadog site is not set')
}
return config.site
}
```
--------------------------------------------------------------------------------
/src/tools/logs/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
export const GetLogsZodSchema = z.object({
query: z.string().default('').describe('Datadog logs query string'),
from: z.number().describe('Start time in epoch seconds'),
to: z.number().describe('End time in epoch seconds'),
limit: z
.number()
.optional()
.default(100)
.describe('Maximum number of logs to return. Default is 100.'),
})
/**
* Schema for retrieving all unique service names from logs.
* Defines parameters for querying logs within a time window.
*
* @param query - Optional. Additional query filter for log search. Defaults to "*" (all logs)
* @param from - Required. Start time in epoch seconds
* @param to - Required. End time in epoch seconds
* @param limit - Optional. Maximum number of logs to search through. Default is 1000.
*/
export const GetAllServicesZodSchema = z.object({
query: z
.string()
.default('*')
.describe('Optional query filter for log search'),
from: z.number().describe('Start time in epoch seconds'),
to: z.number().describe('End time in epoch seconds'),
limit: z
.number()
.optional()
.default(1000)
.describe('Maximum number of logs to search through. Default is 1000.'),
})
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- datadogApiKey
- datadogAppKey
properties:
datadogApiKey:
type: string
description: Your Datadog API key
datadogAppKey:
type: string
description: Your Datadog Application key
datadogSite:
type: string
default: ''
description: Optional Datadog site (e.g. datadoghq.eu)
datadogSubdomain:
type: string
default: ''
description: Optional Datadog subdomain (e.g. <your-subdomain>.datadoghq.com)
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['build/index.js'],
env: Object.assign({}, process.env, {
DATADOG_API_KEY: config.datadogApiKey,
DATADOG_APP_KEY: config.datadogAppKey,
...(config.datadogSite && { DATADOG_SITE: config.datadogSite }),
...(config.datadogSubdomain && { DATADOG_SUBDOMAIN: config.datadogSubdomain })
})
})
exampleConfig:
datadogApiKey: your_datadog_api_key_here
datadogAppKey: your_datadog_app_key_here
datadogSite: datadoghq.com
datadogSubdomain: your-subdomain
```
--------------------------------------------------------------------------------
/tests/utils/tool.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { createToolSchema } from '../../src/utils/tool'
import { z } from 'zod'
describe('createToolSchema', () => {
it('should generate tool schema with correct inputSchema when definitions exist', () => {
// Create a dummy schema with a matching definition for the tool name
const dummySchema = z.object({
foo: z.string().describe('foo description'),
bar: z.number().describe('bar description').optional(),
baz: z.boolean().describe('baz description').default(false),
qux: z.number().describe('qux description').min(10).max(20).default(15),
})
// Call createToolSchema with the dummy schema, tool name, and description
const gotTool = createToolSchema(
dummySchema,
'test',
'dummy test description',
)
// Expected inputSchema based on the dummy schema
const expectedInputSchema: Tool = {
name: 'test',
description: 'dummy test description',
inputSchema: {
type: 'object',
properties: {
foo: {
type: 'string',
description: 'foo description',
},
bar: {
type: 'number',
description: 'bar description',
},
baz: {
type: 'boolean',
description: 'baz description',
default: false,
},
qux: {
type: 'number',
description: 'qux description',
default: 15,
minimum: 10,
maximum: 20,
},
},
required: ['foo'],
},
}
// Verify the returned tool object matches expected structure
expect(gotTool).toEqual(expectedInputSchema)
})
})
```
--------------------------------------------------------------------------------
/src/utils/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { ZodSchema } from 'zod'
import zodToJsonSchema from 'zod-to-json-schema'
type JsonSchema = Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
function pickRootObjectProperty(
fullSchema: JsonSchema,
schemaName: string,
): {
type: 'object'
properties: any // eslint-disable-line @typescript-eslint/no-explicit-any
required?: string[]
} {
const definitions = fullSchema.definitions ?? {}
const root = definitions[schemaName]
return {
type: 'object',
properties: root?.properties ?? {},
required: root?.required ?? [],
}
}
/**
* Creates a tool definition object using the provided Zod schema.
*
* This function converts a Zod schema (acting as the single source of truth) into a JSON Schema,
* extracts the relevant root object properties, and embeds them into the tool definition.
* This approach avoids duplicate schema definitions and ensures type safety and consistency.
*
* Note: The provided name is also used as the tool's name in the Model Context Protocol.
*
* @param schema - The Zod schema representing the tool's parameters.
* @param name - The name of the tool and the key used to extract the corresponding schema definition, and the tool's name in the Model Context Protocol.
* @param description - A brief description of the tool's functionality.
* @returns A tool object containing the name, description, and input JSON Schema.
*/
export function createToolSchema<T extends string>(
schema: ZodSchema<any>, // eslint-disable-line @typescript-eslint/no-explicit-any
name: T,
description: string,
): Tool & { name: T } {
return {
name,
description,
inputSchema: pickRootObjectProperty(
zodToJsonSchema(schema, { name }),
name,
),
}
}
```
--------------------------------------------------------------------------------
/src/tools/traces/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v2 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { ListTracesZodSchema } from './schema'
type TracesToolName = 'list_traces'
type TracesTool = ExtendedTool<TracesToolName>
export const TRACES_TOOLS: TracesTool[] = [
createToolSchema(
ListTracesZodSchema,
'list_traces',
'Get APM traces from Datadog',
),
] as const
type TracesToolHandlers = ToolHandlers<TracesToolName>
export const createTracesToolHandlers = (
apiInstance: v2.SpansApi,
): TracesToolHandlers => {
return {
list_traces: async (request) => {
const {
query,
from,
to,
limit = 100,
sort = '-timestamp',
service,
operation,
} = ListTracesZodSchema.parse(request.params.arguments)
const response = await apiInstance.listSpans({
body: {
data: {
attributes: {
filter: {
query: [
query,
...(service ? [`service:${service}`] : []),
...(operation ? [`operation:${operation}`] : []),
].join(' '),
from: new Date(from * 1000).toISOString(),
to: new Date(to * 1000).toISOString(),
},
sort: sort as 'timestamp' | '-timestamp',
page: { limit },
},
type: 'search_request',
},
},
})
if (!response.data) {
throw new Error('No traces data returned')
}
return {
content: [
{
type: 'text',
text: `Traces: ${JSON.stringify({
traces: response.data,
count: response.data.length,
})}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/src/tools/incident/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v2 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { GetIncidentZodSchema, ListIncidentsZodSchema } from './schema'
type IncidentToolName = 'list_incidents' | 'get_incident'
type IncidentTool = ExtendedTool<IncidentToolName>
export const INCIDENT_TOOLS: IncidentTool[] = [
createToolSchema(
ListIncidentsZodSchema,
'list_incidents',
'Get incidents from Datadog',
),
createToolSchema(
GetIncidentZodSchema,
'get_incident',
'Get an incident from Datadog',
),
] as const
type IncidentToolHandlers = ToolHandlers<IncidentToolName>
export const createIncidentToolHandlers = (
apiInstance: v2.IncidentsApi,
): IncidentToolHandlers => {
return {
list_incidents: async (request) => {
const { pageSize, pageOffset } = ListIncidentsZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listIncidents({
pageSize,
pageOffset,
})
if (response.data == null) {
throw new Error('No incidents data returned')
}
return {
content: [
{
type: 'text',
text: `Listed incidents:\n${response.data
.map((d) => JSON.stringify(d))
.join('\n')}`,
},
],
}
},
get_incident: async (request) => {
const { incidentId } = GetIncidentZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.getIncident({
incidentId,
})
if (response.data == null) {
throw new Error('No incident data returned')
}
return {
content: [
{
type: 'text',
text: `Incident: ${JSON.stringify(response.data)}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm run lint
format:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check code format with Prettier
run: pnpm exec prettier --check .
build:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
test:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test:coverage
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: coverage
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@winor30/mcp-server-datadog",
"version": "1.7.0",
"description": "MCP server for interacting with Datadog API",
"repository": {
"type": "git",
"url": "https://github.com/winor30/mcp-server-datadog.git"
},
"type": "module",
"bin": {
"mcp-server-datadog": "./build/index.js"
},
"main": "build/index.js",
"module": "build/index.js",
"types": "build/index.d.ts",
"files": [
"build",
"README.md"
],
"access": "public",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"scripts": {
"build": "tsup && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "husky",
"watch": "tsup --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
"lint": "eslint . --ext .ts,.js --fix",
"format": "prettier --write .",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"lint-staged": "lint-staged"
},
"dependencies": {
"@datadog/datadog-api-client": "^1.34.1",
"@modelcontextprotocol/sdk": "0.6.0",
"zod": "^3.24.3",
"zod-to-json-schema": "^3.24.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.25.0",
"@types/jest": "^29.5.14",
"@types/node": "^20.17.30",
"@vitest/coverage-v8": "3.0.8",
"eslint": "^9.25.0",
"globals": "^16.0.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"msw": "^2.7.5",
"prettier": "^3.5.3",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
"tsup": "^8.4.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.1.4"
},
"engines": {
"node": ">=20.x",
"pnpm": ">=10"
},
"pnpm": {
"overrides": {
"vite": ">=6.3.4"
}
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
},
"packageManager": "[email protected]"
}
```
--------------------------------------------------------------------------------
/src/tools/dashboards/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v1 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { GetDashboardZodSchema, ListDashboardsZodSchema } from './schema'
type DashboardsToolName = 'list_dashboards' | 'get_dashboard'
type DashboardsTool = ExtendedTool<DashboardsToolName>
export const DASHBOARDS_TOOLS: DashboardsTool[] = [
createToolSchema(
ListDashboardsZodSchema,
'list_dashboards',
'Get list of dashboards from Datadog',
),
createToolSchema(
GetDashboardZodSchema,
'get_dashboard',
'Get a dashboard from Datadog',
),
] as const
type DashboardsToolHandlers = ToolHandlers<DashboardsToolName>
export const createDashboardsToolHandlers = (
apiInstance: v1.DashboardsApi,
): DashboardsToolHandlers => {
return {
list_dashboards: async (request) => {
const { name, tags } = ListDashboardsZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listDashboards({
filterShared: false,
})
if (!response.dashboards) {
throw new Error('No dashboards data returned')
}
// Filter dashboards based on name and tags if provided
let filteredDashboards = response.dashboards
if (name) {
const searchTerm = name.toLowerCase()
filteredDashboards = filteredDashboards.filter((dashboard) =>
dashboard.title?.toLowerCase().includes(searchTerm),
)
}
if (tags && tags.length > 0) {
filteredDashboards = filteredDashboards.filter((dashboard) => {
const dashboardTags = dashboard.description?.split(',') || []
return tags.every((tag) => dashboardTags.includes(tag))
})
}
const dashboards = filteredDashboards.map((dashboard) => ({
...dashboard,
url: `https://app.datadoghq.com/dashboard/${dashboard.id}`,
}))
return {
content: [
{
type: 'text',
text: `Dashboards: ${JSON.stringify(dashboards)}`,
},
],
}
},
get_dashboard: async (request) => {
const { dashboardId } = GetDashboardZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.getDashboard({
dashboardId,
})
return {
content: [
{
type: 'text',
text: `Dashboard: ${JSON.stringify(response)}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/src/tools/logs/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v2 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { GetLogsZodSchema, GetAllServicesZodSchema } from './schema'
type LogsToolName = 'get_logs' | 'get_all_services'
type LogsTool = ExtendedTool<LogsToolName>
export const LOGS_TOOLS: LogsTool[] = [
createToolSchema(
GetLogsZodSchema,
'get_logs',
'Search and retrieve logs from Datadog',
),
createToolSchema(
GetAllServicesZodSchema,
'get_all_services',
'Extract all unique service names from logs',
),
] as const
type LogsToolHandlers = ToolHandlers<LogsToolName>
export const createLogsToolHandlers = (
apiInstance: v2.LogsApi,
): LogsToolHandlers => ({
get_logs: async (request) => {
const { query, from, to, limit } = GetLogsZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listLogs({
body: {
filter: {
query,
// `from` and `to` are in epoch seconds, but the Datadog API expects milliseconds
from: `${from * 1000}`,
to: `${to * 1000}`,
},
page: {
limit,
},
sort: '-timestamp',
},
})
if (response.data == null) {
throw new Error('No logs data returned')
}
return {
content: [
{
type: 'text',
text: `Logs data: ${JSON.stringify(response.data)}`,
},
],
}
},
get_all_services: async (request) => {
const { query, from, to, limit } = GetAllServicesZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listLogs({
body: {
filter: {
query,
// `from` and `to` are in epoch seconds, but the Datadog API expects milliseconds
from: `${from * 1000}`,
to: `${to * 1000}`,
},
page: {
limit,
},
sort: '-timestamp',
},
})
if (response.data == null) {
throw new Error('No logs data returned')
}
// Extract unique services from logs
const services = new Set<string>()
for (const log of response.data) {
// Access service attribute from logs based on the Datadog API structure
if (log.attributes && log.attributes.service) {
services.add(log.attributes.service)
}
}
return {
content: [
{
type: 'text',
text: `Services: ${JSON.stringify(Array.from(services).sort())}`,
},
],
}
},
})
```
--------------------------------------------------------------------------------
/src/tools/downtimes/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v1 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import {
ListDowntimesZodSchema,
ScheduleDowntimeZodSchema,
CancelDowntimeZodSchema,
} from './schema'
type DowntimesToolName =
| 'list_downtimes'
| 'schedule_downtime'
| 'cancel_downtime'
type DowntimesTool = ExtendedTool<DowntimesToolName>
export const DOWNTIMES_TOOLS: DowntimesTool[] = [
createToolSchema(
ListDowntimesZodSchema,
'list_downtimes',
'List scheduled downtimes from Datadog',
),
createToolSchema(
ScheduleDowntimeZodSchema,
'schedule_downtime',
'Schedule a downtime in Datadog',
),
createToolSchema(
CancelDowntimeZodSchema,
'cancel_downtime',
'Cancel a scheduled downtime in Datadog',
),
] as const
type DowntimesToolHandlers = ToolHandlers<DowntimesToolName>
export const createDowntimesToolHandlers = (
apiInstance: v1.DowntimesApi,
): DowntimesToolHandlers => {
return {
list_downtimes: async (request) => {
const { currentOnly } = ListDowntimesZodSchema.parse(
request.params.arguments,
)
const res = await apiInstance.listDowntimes({
currentOnly,
})
return {
content: [
{
type: 'text',
text: `Listed downtimes:\n${JSON.stringify(res, null, 2)}`,
},
],
}
},
schedule_downtime: async (request) => {
const params = ScheduleDowntimeZodSchema.parse(request.params.arguments)
// Convert to the format expected by Datadog client
const downtimeData: v1.Downtime = {
scope: [params.scope],
start: params.start,
end: params.end,
message: params.message,
timezone: params.timezone,
monitorId: params.monitorId,
monitorTags: params.monitorTags,
}
// Add recurrence configuration if provided
if (params.recurrence) {
downtimeData.recurrence = {
type: params.recurrence.type,
period: params.recurrence.period,
weekDays: params.recurrence.weekDays,
}
}
const res = await apiInstance.createDowntime({
body: downtimeData,
})
return {
content: [
{
type: 'text',
text: `Scheduled downtime: ${JSON.stringify(res, null, 2)}`,
},
],
}
},
cancel_downtime: async (request) => {
const { downtimeId } = CancelDowntimeZodSchema.parse(
request.params.arguments,
)
await apiInstance.cancelDowntime({
downtimeId,
})
return {
content: [
{
type: 'text',
text: `Cancelled downtime with ID: ${downtimeId}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/src/tools/monitors/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v1 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { GetMonitorsZodSchema } from './schema'
import { unreachable } from '../../utils/helper'
import { UnparsedObject } from '@datadog/datadog-api-client/dist/packages/datadog-api-client-common/util.js'
type MonitorsToolName = 'get_monitors'
type MonitorsTool = ExtendedTool<MonitorsToolName>
export const MONITORS_TOOLS: MonitorsTool[] = [
createToolSchema(
GetMonitorsZodSchema,
'get_monitors',
'Get monitors status from Datadog',
),
] as const
type MonitorsToolHandlers = ToolHandlers<MonitorsToolName>
export const createMonitorsToolHandlers = (
apiInstance: v1.MonitorsApi,
): MonitorsToolHandlers => {
return {
get_monitors: async (request) => {
const { groupStates, name, tags } = GetMonitorsZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listMonitors({
groupStates: groupStates?.join(','),
name,
tags: tags?.join(','),
})
if (response == null) {
throw new Error('No monitors data returned')
}
const monitors = response.map((monitor) => ({
name: monitor.name || '',
id: monitor.id || 0,
status: (monitor.overallState as string) || 'unknown',
message: monitor.message,
tags: monitor.tags || [],
query: monitor.query || '',
lastUpdatedTs: monitor.modified
? Math.floor(new Date(monitor.modified).getTime() / 1000)
: undefined,
}))
// Calculate summary
const summary = response.reduce(
(acc, monitor) => {
const status = monitor.overallState
if (status == null || status instanceof UnparsedObject) {
return acc
}
switch (status) {
case 'Alert':
acc.alert++
break
case 'Warn':
acc.warn++
break
case 'No Data':
acc.noData++
break
case 'OK':
acc.ok++
break
case 'Ignored':
acc.ignored++
break
case 'Skipped':
acc.skipped++
break
case 'Unknown':
acc.unknown++
break
default:
unreachable(status)
}
return acc
},
{
alert: 0,
warn: 0,
noData: 0,
ok: 0,
ignored: 0,
skipped: 0,
unknown: 0,
},
)
return {
content: [
{
type: 'text',
text: `Monitors: ${JSON.stringify(monitors)}`,
},
{
type: 'text',
text: `Summary of monitors: ${JSON.stringify(summary)}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/src/tools/rum/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
/**
* Schema for retrieving RUM events.
* Defines parameters for querying RUM events within a time window.
*
* @param query - Datadog RUM query string
* @param from - Start time in epoch seconds
* @param to - End time in epoch seconds
* @param limit - Maximum number of events to return (default: 100)
*/
export const GetRumEventsZodSchema = z.object({
query: z.string().default('').describe('Datadog RUM query string'),
from: z.number().describe('Start time in epoch seconds'),
to: z.number().describe('End time in epoch seconds'),
limit: z
.number()
.optional()
.default(100)
.describe('Maximum number of events to return. Default is 100.'),
})
/**
* Schema for retrieving RUM applications.
* Returns a list of all RUM applications in the organization.
*/
export const GetRumApplicationsZodSchema = z.object({})
/**
* Schema for retrieving unique user session counts.
* Defines parameters for querying session counts within a time window.
*
* @param query - Optional. Additional query filter for RUM search. Defaults to "*" (all events)
* @param from - Start time in epoch seconds
* @param to - End time in epoch seconds
* @param groupBy - Optional. Dimension to group results by (e.g., 'application.name')
*/
export const GetRumGroupedEventCountZodSchema = z.object({
query: z
.string()
.default('*')
.describe('Optional query filter for RUM search'),
from: z.number().describe('Start time in epoch seconds'),
to: z.number().describe('End time in epoch seconds'),
groupBy: z
.string()
.optional()
.default('application.name')
.describe('Dimension to group results by. Default is application.name'),
})
/**
* Schema for retrieving page performance metrics.
* Defines parameters for querying performance metrics within a time window.
*
* @param query - Optional. Additional query filter for RUM search. Defaults to "*" (all events)
* @param from - Start time in epoch seconds
* @param to - End time in epoch seconds
* @param metricNames - Array of metric names to retrieve (e.g., 'view.load_time', 'view.first_contentful_paint')
*/
export const GetRumPagePerformanceZodSchema = z.object({
query: z
.string()
.default('*')
.describe('Optional query filter for RUM search'),
from: z.number().describe('Start time in epoch seconds'),
to: z.number().describe('End time in epoch seconds'),
metricNames: z
.array(z.string())
.default([
'view.load_time',
'view.first_contentful_paint',
'view.largest_contentful_paint',
])
.describe('Array of metric names to retrieve'),
})
/**
* Schema for retrieving RUM page waterfall data.
* Defines parameters for querying waterfall data within a time window.
*
* @param application - Application name or ID to filter events
* @param sessionId - Session ID to filter events
* @param from - Start time in epoch seconds
* @param to - End time in epoch seconds
*/
export const GetRumPageWaterfallZodSchema = z.object({
applicationName: z.string().describe('Application name to filter events'),
sessionId: z.string().describe('Session ID to filter events'),
})
```
--------------------------------------------------------------------------------
/src/tools/hosts/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod'
/**
* Zod schemas for validating input parameters for Datadog host management operations.
* These schemas define the expected shape and types of data for each host-related tool.
*/
/**
* Schema for muting a host in Datadog.
* Defines required and optional parameters for temporarily silencing a host's alerts.
*
* @param hostname - Required. Identifies the host to be muted
* @param message - Optional. Adds context about why the host is being muted
* @param end - Optional. Unix timestamp defining when the mute should automatically expire
* @param override - Optional. Controls whether to replace an existing mute's end time
*/
export const MuteHostZodSchema = z.object({
hostname: z.string().describe('The name of the host to mute'),
message: z
.string()
.optional()
.describe('Message to associate with the muting of this host'),
end: z
.number()
.int()
.optional()
.describe('POSIX timestamp for when the mute should end'),
override: z
.boolean()
.optional()
.default(false)
.describe(
'If true and the host is already muted, replaces existing end time',
),
})
/**
* Schema for unmuting a host in Datadog.
* Defines parameters for re-enabling alerts for a previously muted host.
*
* @param hostname - Required. Identifies the host to be unmuted
*/
export const UnmuteHostZodSchema = z.object({
hostname: z.string().describe('The name of the host to unmute'),
})
/**
* Schema for retrieving active host counts from Datadog.
* Defines parameters for querying the number of reporting hosts within a time window.
*
* @param from - Optional. Time window in seconds to check for host activity
* Defaults to 7200 seconds (2 hours)
*/
export const GetActiveHostsCountZodSchema = z.object({
from: z
.number()
.int()
.optional()
.default(7200)
.describe(
'Number of seconds from which you want to get total number of active hosts (defaults to 2h)',
),
})
/**
* Schema for listing and filtering hosts in Datadog.
* Defines comprehensive parameters for querying and filtering host information.
*
* @param filter - Optional. Search string to filter hosts
* @param sort_field - Optional. Field to sort results by
* @param sort_dir - Optional. Sort direction ('asc' or 'desc')
* @param start - Optional. Pagination offset
* @param count - Optional. Number of hosts to return (max 1000)
* @param from - Optional. Unix timestamp to start searching from
* @param include_muted_hosts_data - Optional. Include muting information
* @param include_hosts_metadata - Optional. Include detailed host metadata
*/
export const ListHostsZodSchema = z.object({
filter: z.string().optional().describe('Filter string for search results'),
sort_field: z.string().optional().describe('Field to sort hosts by'),
sort_dir: z.string().optional().describe('Sort direction (asc/desc)'),
start: z.number().int().optional().describe('Starting offset for pagination'),
count: z
.number()
.int()
.max(1000)
.optional()
.describe('Max number of hosts to return (max: 1000)'),
from: z
.number()
.int()
.optional()
.describe('Search hosts from this UNIX timestamp'),
include_muted_hosts_data: z
.boolean()
.optional()
.describe('Include muted hosts status and expiry'),
include_hosts_metadata: z
.boolean()
.optional()
.describe('Include host metadata (version, platform, etc)'),
})
```
--------------------------------------------------------------------------------
/tests/utils/datadog.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect } from 'vitest'
import {
ApiKeyAuthAuthentication,
AppKeyAuthAuthentication,
} from '@datadog/datadog-api-client/dist/packages/datadog-api-client-common'
import { createDatadogConfig, getDatadogSite } from '../../src/utils/datadog'
describe('createDatadogConfig', () => {
it('should create a datadog config with custom site when DATADOG_SITE is configured', () => {
const datadogConfig = createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: 'test-app-key',
site: 'us3.datadoghq.com',
})
expect(datadogConfig.authMethods).toEqual({
apiKeyAuth: new ApiKeyAuthAuthentication('test-api-key'),
appKeyAuth: new AppKeyAuthAuthentication('test-app-key'),
})
expect(datadogConfig.servers[0]?.getConfiguration()?.site).toBe(
'us3.datadoghq.com',
)
})
it('should create a datadog config with default site when DATADOG_SITE is not configured', () => {
const datadogConfig = createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: 'test-app-key',
})
expect(datadogConfig.authMethods).toEqual({
apiKeyAuth: new ApiKeyAuthAuthentication('test-api-key'),
appKeyAuth: new AppKeyAuthAuthentication('test-app-key'),
})
expect(datadogConfig.servers[0]?.getConfiguration()?.site).toBe(
'datadoghq.com',
)
})
})
describe('createDatadogConfig', () => {
it('should create a datadog config with custom subdomain when DATADOG_SUBDOMAIN is configured', () => {
const datadogConfig = createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: 'test-app-key',
subdomain: 'youryour-subdomain',
})
expect(datadogConfig.authMethods).toEqual({
apiKeyAuth: new ApiKeyAuthAuthentication('test-api-key'),
appKeyAuth: new AppKeyAuthAuthentication('test-app-key'),
})
expect(datadogConfig.servers[0]?.getConfiguration()?.subdomain).toBe(
'youryour-subdomain',
)
})
it('should create a datadog config with default subdomain when DATADOG_SUBDOMAIN is not configured', () => {
const datadogConfig = createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: 'test-app-key',
})
expect(datadogConfig.authMethods).toEqual({
apiKeyAuth: new ApiKeyAuthAuthentication('test-api-key'),
appKeyAuth: new AppKeyAuthAuthentication('test-app-key'),
})
expect(datadogConfig.servers[0]?.getConfiguration()?.subdomain).toBe('api')
})
it('should throw an error when DATADOG_API_KEY are not configured', () => {
expect(() =>
createDatadogConfig({
apiKeyAuth: '',
appKeyAuth: 'test-app-key',
}),
).toThrow('Datadog API key and APP key are required')
})
it('should throw an error when DATADOG_APP_KEY are not configured', () => {
expect(() =>
createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: '',
}),
).toThrow('Datadog API key and APP key are required')
})
})
describe('getDatadogSite', () => {
it('should return custom site when DATADOG_SITE is configured', () => {
const datadogConfig = createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: 'test-app-key',
site: 'us3.datadoghq.com',
})
const site = getDatadogSite(datadogConfig)
expect(site).toBe('us3.datadoghq.com')
})
it('should return default site when DATADOG_SITE is not configured', () => {
const datadogConfig = createDatadogConfig({
apiKeyAuth: 'test-api-key',
appKeyAuth: 'test-app-key',
})
const site = getDatadogSite(datadogConfig)
expect(site).toBe('datadoghq.com')
})
})
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
/**
* This script sets up the mcp-server-datadog.
* It initializes an MCP server that integrates with Datadog for incident management.
* By leveraging MCP, this server can list and retrieve incidents via the Datadog incident API.
* With a design built for scalability, future integrations with additional Datadog APIs are anticipated.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { log, mcpDatadogVersion } from './utils/helper'
import { INCIDENT_TOOLS, createIncidentToolHandlers } from './tools/incident'
import { METRICS_TOOLS, createMetricsToolHandlers } from './tools/metrics'
import { LOGS_TOOLS, createLogsToolHandlers } from './tools/logs'
import { MONITORS_TOOLS, createMonitorsToolHandlers } from './tools/monitors'
import {
DASHBOARDS_TOOLS,
createDashboardsToolHandlers,
} from './tools/dashboards'
import { TRACES_TOOLS, createTracesToolHandlers } from './tools/traces'
import { HOSTS_TOOLS, createHostsToolHandlers } from './tools/hosts'
import { ToolHandlers } from './utils/types'
import { createDatadogConfig } from './utils/datadog'
import { createDowntimesToolHandlers, DOWNTIMES_TOOLS } from './tools/downtimes'
import { createRumToolHandlers, RUM_TOOLS } from './tools/rum'
import { v2, v1 } from '@datadog/datadog-api-client'
const server = new Server(
{
name: 'Datadog MCP Server',
version: mcpDatadogVersion,
},
{
capabilities: {
tools: {},
},
},
)
server.onerror = (error) => {
log('error', `Server error: ${error.message}`, error.stack)
}
/**
* Handler that retrieves the list of available tools in the mcp-server-datadog.
* Currently, it provides incident management functionalities by integrating with Datadog's incident APIs.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
...INCIDENT_TOOLS,
...METRICS_TOOLS,
...LOGS_TOOLS,
...MONITORS_TOOLS,
...DASHBOARDS_TOOLS,
...TRACES_TOOLS,
...HOSTS_TOOLS,
...DOWNTIMES_TOOLS,
...RUM_TOOLS,
],
}
})
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
subdomain: process.env.DATADOG_SUBDOMAIN,
})
const TOOL_HANDLERS: ToolHandlers = {
...createIncidentToolHandlers(new v2.IncidentsApi(datadogConfig)),
...createMetricsToolHandlers(new v1.MetricsApi(datadogConfig)),
...createLogsToolHandlers(new v2.LogsApi(datadogConfig)),
...createMonitorsToolHandlers(new v1.MonitorsApi(datadogConfig)),
...createDashboardsToolHandlers(new v1.DashboardsApi(datadogConfig)),
...createTracesToolHandlers(new v2.SpansApi(datadogConfig)),
...createHostsToolHandlers(new v1.HostsApi(datadogConfig)),
...createDowntimesToolHandlers(new v1.DowntimesApi(datadogConfig)),
...createRumToolHandlers(new v2.RUMApi(datadogConfig)),
}
/**
* Handler for invoking Datadog-related tools in the mcp-server-datadog.
* The TOOL_HANDLERS object contains various tools that interact with different Datadog APIs.
* By specifying the tool name in the request, the LLM can select and utilize the required tool.
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (TOOL_HANDLERS[request.params.name]) {
return await TOOL_HANDLERS[request.params.name](request)
}
throw new Error('Unknown tool')
} catch (unknownError) {
const error =
unknownError instanceof Error
? unknownError
: new Error(String(unknownError))
log(
'error',
`Request: ${request.params.name}, ${JSON.stringify(request.params.arguments)} failed`,
error.message,
error.stack,
)
throw error
}
})
/**
* Initializes and starts the mcp-server-datadog using stdio transport,
* which sends and receives data through standard input and output.
*/
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
}
main().catch((error) => {
log('error', 'Server error:', error)
process.exit(1)
})
```
--------------------------------------------------------------------------------
/src/tools/hosts/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v1 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import {
ListHostsZodSchema,
GetActiveHostsCountZodSchema,
MuteHostZodSchema,
UnmuteHostZodSchema,
} from './schema'
/**
* This module implements Datadog host management tools for muting, unmuting,
* and retrieving host information using the Datadog API client.
*/
/** Available host management tool names */
type HostsToolName =
| 'list_hosts'
| 'get_active_hosts_count'
| 'mute_host'
| 'unmute_host'
/** Extended tool type with host-specific operations */
type HostsTool = ExtendedTool<HostsToolName>
/**
* Array of available host management tools.
* Each tool is created with a schema for input validation and includes a description.
*/
export const HOSTS_TOOLS: HostsTool[] = [
createToolSchema(MuteHostZodSchema, 'mute_host', 'Mute a host in Datadog'),
createToolSchema(
UnmuteHostZodSchema,
'unmute_host',
'Unmute a host in Datadog',
),
createToolSchema(
ListHostsZodSchema,
'list_hosts',
'Get list of hosts from Datadog',
),
createToolSchema(
GetActiveHostsCountZodSchema,
'get_active_hosts_count',
'Get the total number of active hosts in Datadog (defaults to last 5 minutes)',
),
] as const
/** Type definition for host management tool implementations */
type HostsToolHandlers = ToolHandlers<HostsToolName>
/**
* Implementation of host management tool handlers.
* Each handler validates inputs using Zod schemas and interacts with the Datadog API.
*/
export const createHostsToolHandlers = (
apiInstance: v1.HostsApi,
): HostsToolHandlers => {
return {
/**
* Mutes a specified host in Datadog.
* Silences alerts and notifications for the host until unmuted or until the specified end time.
*/
mute_host: async (request) => {
const { hostname, message, end, override } = MuteHostZodSchema.parse(
request.params.arguments,
)
await apiInstance.muteHost({
hostName: hostname,
body: {
message,
end,
override,
},
})
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'success',
message: `Host ${hostname} has been muted successfully${message ? ` with message: ${message}` : ''}${end ? ` until ${new Date(end * 1000).toISOString()}` : ''}`,
},
null,
2,
),
},
],
}
},
/**
* Unmutes a previously muted host in Datadog.
* Re-enables alerts and notifications for the specified host.
*/
unmute_host: async (request) => {
const { hostname } = UnmuteHostZodSchema.parse(request.params.arguments)
await apiInstance.unmuteHost({
hostName: hostname,
})
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
status: 'success',
message: `Host ${hostname} has been unmuted successfully`,
},
null,
2,
),
},
],
}
},
/**
* Retrieves counts of active and up hosts in Datadog.
* Provides total counts of hosts that are reporting and operational.
*/
get_active_hosts_count: async (request) => {
const { from } = GetActiveHostsCountZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.getHostTotals({
from,
})
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
total_active: response.totalActive || 0, // Total number of active hosts (UP and reporting) to Datadog
total_up: response.totalUp || 0, // Number of hosts that are UP and reporting to Datadog
},
null,
2,
),
},
],
}
},
/**
* Lists and filters hosts monitored by Datadog.
* Supports comprehensive querying with filtering, sorting, and pagination.
* Returns detailed host information including status, metadata, and monitoring data.
*/
list_hosts: async (request) => {
const {
filter,
sort_field,
sort_dir,
start,
count,
from,
include_muted_hosts_data,
include_hosts_metadata,
} = ListHostsZodSchema.parse(request.params.arguments)
const response = await apiInstance.listHosts({
filter,
sortField: sort_field,
sortDir: sort_dir,
start,
count,
from,
includeMutedHostsData: include_muted_hosts_data,
includeHostsMetadata: include_hosts_metadata,
})
if (!response.hostList) {
throw new Error('No hosts data returned')
}
// Transform API response into a more convenient format
const hosts = response.hostList.map((host) => ({
name: host.name,
id: host.id,
aliases: host.aliases,
apps: host.apps,
mute: host.isMuted,
last_reported: host.lastReportedTime,
meta: host.meta,
metrics: host.metrics,
sources: host.sources,
up: host.up,
url: `https://app.datadoghq.com/infrastructure?host=${host.name}`,
}))
return {
content: [
{
type: 'text',
text: `Hosts: ${JSON.stringify(hosts)}`,
},
],
}
},
}
}
```
--------------------------------------------------------------------------------
/tests/tools/dashboards.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v1 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createDashboardsToolHandlers } from '../../src/tools/dashboards/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const dashboardEndpoint = `${baseUrl}/v1/dashboard`
describe('Dashboards Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v1.DashboardsApi(datadogConfig)
const toolHandlers = createDashboardsToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/dashboards/#get-all-dashboards
describe.concurrent('list_dashboards', async () => {
it('should list dashboards', async () => {
const mockHandler = http.get(dashboardEndpoint, async () => {
return HttpResponse.json({
dashboards: [
{
id: 'q5j-nti-fv6',
type: 'host_timeboard',
},
],
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_dashboards', {
name: 'test name',
tags: ['test_tag'],
})
const response = (await toolHandlers.list_dashboards(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Dashboards')
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.get(dashboardEndpoint, async () => {
return HttpResponse.json(
{ errors: ['dummy authentication error'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_dashboards', {
name: 'test',
})
await expect(toolHandlers.list_dashboards(request)).rejects.toThrow(
'dummy authentication error',
)
})()
server.close()
})
it('should handle too many requests', async () => {
const mockHandler = http.get(dashboardEndpoint, async () => {
return HttpResponse.json(
{ errors: ['dummy too many requests'] },
{ status: 429 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_dashboards', {
name: 'test',
})
await expect(toolHandlers.list_dashboards(request)).rejects.toThrow(
'dummy too many requests',
)
})()
server.close()
})
it('should handle unknown errors', async () => {
const mockHandler = http.get(dashboardEndpoint, async () => {
return HttpResponse.json(
{ errors: ['dummy unknown error'] },
{ status: 500 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_dashboards', {
name: 'test',
})
await expect(toolHandlers.list_dashboards(request)).rejects.toThrow(
'dummy unknown error',
)
})()
server.close()
})
})
// https://docs.datadoghq.com/ja/api/latest/dashboards/#get-a-dashboard
describe.concurrent('get_dashboard', async () => {
it('should get a dashboard', async () => {
const dashboardId = '123456789'
const mockHandler = http.get(
`${dashboardEndpoint}/${dashboardId}`,
async () => {
return HttpResponse.json({
id: '123456789',
title: 'Dashboard',
layout_type: 'ordered',
widgets: [],
})
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_dashboard', {
dashboardId,
})
const response = (await toolHandlers.get_dashboard(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('123456789')
expect(response.content[0].text).toContain('Dashboard')
expect(response.content[0].text).toContain('ordered')
})()
server.close()
})
it('should handle not found errors', async () => {
const dashboardId = '999999999'
const mockHandler = http.get(
`${dashboardEndpoint}/${dashboardId}`,
async () => {
return HttpResponse.json({ errors: ['Not found'] }, { status: 404 })
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_dashboard', {
dashboardId,
})
await expect(toolHandlers.get_dashboard(request)).rejects.toThrow(
'Not found',
)
})()
server.close()
})
it('should handle server errors', async () => {
const dashboardId = '123456789'
const mockHandler = http.get(
`${dashboardEndpoint}/${dashboardId}`,
async () => {
return HttpResponse.json(
{ errors: ['Internal server error'] },
{ status: 500 },
)
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_dashboard', {
dashboardId,
})
await expect(toolHandlers.get_dashboard(request)).rejects.toThrow(
'Internal server error',
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/metrics.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v1 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createMetricsToolHandlers } from '../../src/tools/metrics/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const metricsEndpoint = `${baseUrl}/v1/query`
describe('Metrics Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v1.MetricsApi(datadogConfig)
const toolHandlers = createMetricsToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/metrics/#query-timeseries-data-across-multiple-products
describe.concurrent('query_metrics', async () => {
it('should query metrics data', async () => {
const mockHandler = http.get(metricsEndpoint, async () => {
return HttpResponse.json({
status: 'ok',
query: 'avg:system.cpu.user{*}',
series: [
{
metric: 'system.cpu.user',
display_name: 'system.cpu.user',
pointlist: [
[1640995000000, 23.45],
[1640995060000, 24.12],
[1640995120000, 22.89],
[1640995180000, 25.67],
],
scope: 'host:web-01',
expression: 'avg:system.cpu.user{*}',
unit: [
{
family: 'percentage',
scale_factor: 1,
name: 'percent',
short_name: '%',
},
],
},
{
metric: 'system.cpu.user',
display_name: 'system.cpu.user',
pointlist: [
[1640995000000, 18.32],
[1640995060000, 19.01],
[1640995120000, 17.76],
[1640995180000, 20.45],
],
scope: 'host:web-02',
expression: 'avg:system.cpu.user{*}',
unit: [
{
family: 'percentage',
scale_factor: 1,
name: 'percent',
short_name: '%',
},
],
},
],
from_date: 1640995000000,
to_date: 1641095000000,
group_by: ['host'],
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('query_metrics', {
from: 1640995000,
to: 1641095000,
query: 'avg:system.cpu.user{*}',
})
const response = (await toolHandlers.query_metrics(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Queried metrics data:')
expect(response.content[0].text).toContain('system.cpu.user')
expect(response.content[0].text).toContain('host:web-01')
expect(response.content[0].text).toContain('host:web-02')
expect(response.content[0].text).toContain('23.45')
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.get(metricsEndpoint, async () => {
return HttpResponse.json({
status: 'ok',
query: 'avg:non.existent.metric{*}',
series: [],
from_date: 1640995000000,
to_date: 1641095000000,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('query_metrics', {
from: 1640995000,
to: 1641095000,
query: 'avg:non.existent.metric{*}',
})
const response = (await toolHandlers.query_metrics(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Queried metrics data:')
expect(response.content[0].text).toContain('series":[]')
})()
server.close()
})
it('should handle failed query status', async () => {
const mockHandler = http.get(metricsEndpoint, async () => {
return HttpResponse.json({
status: 'error',
message: 'Invalid query format',
query: 'invalid:query:format',
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('query_metrics', {
from: 1640995000,
to: 1641095000,
query: 'invalid:query:format',
})
const response = (await toolHandlers.query_metrics(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('status":"error"')
expect(response.content[0].text).toContain('Invalid query format')
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.get(metricsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('query_metrics', {
from: 1640995000,
to: 1641095000,
query: 'avg:system.cpu.user{*}',
})
await expect(toolHandlers.query_metrics(request)).rejects.toThrow()
})()
server.close()
})
it('should handle rate limit errors', async () => {
const mockHandler = http.get(metricsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Rate limit exceeded'] },
{ status: 429 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('query_metrics', {
from: 1640995000,
to: 1641095000,
query: 'avg:system.cpu.user{*}',
})
await expect(toolHandlers.query_metrics(request)).rejects.toThrow(
'Rate limit exceeded',
)
})()
server.close()
})
it('should handle invalid time range errors', async () => {
const mockHandler = http.get(metricsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Time range exceeds allowed limit'] },
{ status: 400 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
// Using a very large time range that might exceed limits
const request = createMockToolRequest('query_metrics', {
from: 1600000000, // Very old date
to: 1700000000, // Very recent date
query: 'avg:system.cpu.user{*}',
})
await expect(toolHandlers.query_metrics(request)).rejects.toThrow(
'Time range exceeds allowed limit',
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/src/tools/rum/tool.ts:
--------------------------------------------------------------------------------
```typescript
import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v2 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import {
GetRumEventsZodSchema,
GetRumApplicationsZodSchema,
GetRumGroupedEventCountZodSchema,
GetRumPagePerformanceZodSchema,
GetRumPageWaterfallZodSchema,
} from './schema'
type RumToolName =
| 'get_rum_events'
| 'get_rum_applications'
| 'get_rum_grouped_event_count'
| 'get_rum_page_performance'
| 'get_rum_page_waterfall'
type RumTool = ExtendedTool<RumToolName>
export const RUM_TOOLS: RumTool[] = [
createToolSchema(
GetRumApplicationsZodSchema,
'get_rum_applications',
'Get all RUM applications in the organization',
),
createToolSchema(
GetRumEventsZodSchema,
'get_rum_events',
'Search and retrieve RUM events from Datadog',
),
createToolSchema(
GetRumGroupedEventCountZodSchema,
'get_rum_grouped_event_count',
'Search, group and count RUM events by a specified dimension',
),
createToolSchema(
GetRumPagePerformanceZodSchema,
'get_rum_page_performance',
'Get page (view) performance metrics from RUM data',
),
createToolSchema(
GetRumPageWaterfallZodSchema,
'get_rum_page_waterfall',
'Retrieve RUM page (view) waterfall data filtered by application name and session ID',
),
] as const
type RumToolHandlers = ToolHandlers<RumToolName>
export const createRumToolHandlers = (
apiInstance: v2.RUMApi,
): RumToolHandlers => ({
get_rum_applications: async (request) => {
GetRumApplicationsZodSchema.parse(request.params.arguments)
const response = await apiInstance.getRUMApplications()
if (response.data == null) {
throw new Error('No RUM applications data returned')
}
return {
content: [
{
type: 'text',
text: `RUM applications: ${JSON.stringify(response.data)}`,
},
],
}
},
get_rum_events: async (request) => {
const { query, from, to, limit } = GetRumEventsZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listRUMEvents({
filterQuery: query,
filterFrom: new Date(from * 1000),
filterTo: new Date(to * 1000),
sort: 'timestamp',
pageLimit: limit,
})
if (response.data == null) {
throw new Error('No RUM events data returned')
}
return {
content: [
{
type: 'text',
text: `RUM events data: ${JSON.stringify(response.data)}`,
},
],
}
},
get_rum_grouped_event_count: async (request) => {
const { query, from, to, groupBy } = GetRumGroupedEventCountZodSchema.parse(
request.params.arguments,
)
// For session counts, we need to use a query to count unique sessions
const response = await apiInstance.listRUMEvents({
filterQuery: query !== '*' ? query : undefined,
filterFrom: new Date(from * 1000),
filterTo: new Date(to * 1000),
sort: 'timestamp',
pageLimit: 2000,
})
if (response.data == null) {
throw new Error('No RUM events data returned')
}
// Extract session counts grouped by the specified dimension
const sessions = new Map<string, Set<string>>()
for (const event of response.data) {
if (!event.attributes?.attributes) {
continue
}
// Parse the groupBy path (e.g., 'application.id')
const groupPath = groupBy.split('.') as Array<
keyof typeof event.attributes.attributes
>
const result = getValueByPath(
event.attributes.attributes,
groupPath.map((path) => String(path)),
)
const groupValue = result.found ? String(result.value) : 'unknown'
// Get or create the session set for this group
if (!sessions.has(groupValue)) {
sessions.set(groupValue, new Set<string>())
}
// Add the session ID to the set if it exists
if (event.attributes.attributes.session?.id) {
sessions.get(groupValue)?.add(event.attributes.attributes.session.id)
}
}
// Convert the map to an object with counts
const sessionCounts = Object.fromEntries(
Array.from(sessions.entries()).map(([key, set]) => [key, set.size]),
)
return {
content: [
{
type: 'text',
text: `Session counts (grouped by ${groupBy}): ${JSON.stringify(sessionCounts)}`,
},
],
}
},
get_rum_page_performance: async (request) => {
const { query, from, to, metricNames } =
GetRumPagePerformanceZodSchema.parse(request.params.arguments)
// Build a query that focuses on view events with performance metrics
const viewQuery = query !== '*' ? `@type:view ${query}` : '@type:view'
const response = await apiInstance.listRUMEvents({
filterQuery: viewQuery,
filterFrom: new Date(from * 1000),
filterTo: new Date(to * 1000),
sort: 'timestamp',
pageLimit: 2000,
})
if (response.data == null) {
throw new Error('No RUM events data returned')
}
// Extract and calculate performance metrics
const metrics: Record<string, number[]> = metricNames.reduce(
(acc, name) => {
acc[name] = []
return acc
},
{} as Record<string, number[]>,
)
for (const event of response.data) {
if (!event.attributes?.attributes) {
continue
}
// Collect each requested metric if it exists
for (const metricName of metricNames) {
// Handle nested properties like 'view.load_time'
const metricNameParts = metricName.split('.') as Array<
keyof typeof event.attributes.attributes
>
if (event.attributes.attributes == null) {
continue
}
const value = metricNameParts.reduce(
(acc, part) => (acc ? acc[part] : undefined),
event.attributes.attributes,
)
// If we found a numeric value, add it to the metrics
if (typeof value === 'number') {
metrics[metricName].push(value)
}
}
}
// Calculate statistics for each metric
const results: Record<
string,
{ avg: number; min: number; max: number; count: number }
> = Object.entries(metrics).reduce(
(acc, [name, values]) => {
if (values.length > 0) {
const sum = values.reduce((a, b) => a + b, 0)
acc[name] = {
avg: sum / values.length,
min: Math.min(...values),
max: Math.max(...values),
count: values.length,
}
} else {
acc[name] = { avg: 0, min: 0, max: 0, count: 0 }
}
return acc
},
{} as Record<
string,
{ avg: number; min: number; max: number; count: number }
>,
)
return {
content: [
{
type: 'text',
text: `Page performance metrics: ${JSON.stringify(results)}`,
},
],
}
},
get_rum_page_waterfall: async (request) => {
const { applicationName, sessionId } = GetRumPageWaterfallZodSchema.parse(
request.params.arguments,
)
const response = await apiInstance.listRUMEvents({
filterQuery: `@application.name:${applicationName} @session.id:${sessionId}`,
sort: 'timestamp',
pageLimit: 2000,
})
if (response.data == null) {
throw new Error('No RUM events data returned')
}
return {
content: [
{
type: 'text',
text: `Waterfall data: ${JSON.stringify(response.data)}`,
},
],
}
},
})
// Get the group value using a recursive function approach
const getValueByPath = (
obj: Record<string, unknown>,
path: string[],
index = 0,
): { value: unknown; found: boolean } => {
if (index >= path.length) {
return { value: obj, found: true }
}
const key = path[index]
const typedObj = obj as Record<string, unknown>
if (typedObj[key] === undefined) {
return { value: null, found: false }
}
return getValueByPath(
typedObj[key] as Record<string, unknown>,
path,
index + 1,
)
}
```
--------------------------------------------------------------------------------
/tests/tools/monitors.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v1 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createMonitorsToolHandlers } from '../../src/tools/monitors/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const monitorsEndpoint = `${baseUrl}/v1/monitor`
describe('Monitors Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v1.MonitorsApi(datadogConfig)
const toolHandlers = createMonitorsToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/monitors/#get-all-monitor-details
describe.concurrent('get_monitors', async () => {
it('should list monitors', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json([
{
id: 12345,
name: 'Test API Monitor',
type: 'metric alert',
message: 'CPU usage is too high',
tags: ['env:test', 'service:api'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
overall_state: 'Alert',
created: '2023-01-01T00:00:00.000Z',
modified: '2023-01-02T00:00:00.000Z',
},
{
id: 67890,
name: 'Test Web Monitor',
type: 'service check',
message: 'Web service is down',
tags: ['env:test', 'service:web'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
overall_state: 'OK',
created: '2023-02-01T00:00:00.000Z',
modified: '2023-02-02T00:00:00.000Z',
},
])
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {
name: 'test-monitor',
groupStates: ['alert', 'warn'],
tags: ['env:test', 'service:api'],
})
const response = (await toolHandlers.get_monitors(
request,
)) as unknown as DatadogToolResponse
// Check that monitors data is included
expect(response.content[0].text).toContain('Monitors:')
expect(response.content[0].text).toContain('Test API Monitor')
expect(response.content[0].text).toContain('Test Web Monitor')
// Check that summary is included
expect(response.content[1].text).toContain('Summary of monitors:')
expect(response.content[1].text).toContain('"alert":1')
expect(response.content[1].text).toContain('"ok":1')
})()
server.close()
})
it('should handle monitors with various states', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json([
{
id: 1,
name: 'Alert Monitor',
overall_state: 'Alert',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
{
id: 2,
name: 'Warn Monitor',
overall_state: 'Warn',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
{
id: 3,
name: 'No Data Monitor',
overall_state: 'No Data',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
{
id: 4,
name: 'OK Monitor',
overall_state: 'OK',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
{
id: 5,
name: 'Ignored Monitor',
overall_state: 'Ignored',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
{
id: 6,
name: 'Skipped Monitor',
overall_state: 'Skipped',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
{
id: 7,
name: 'Unknown Monitor',
overall_state: 'Unknown',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
type: 'metric alert',
},
])
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {
tags: ['env:test'],
})
const response = (await toolHandlers.get_monitors(
request,
)) as unknown as DatadogToolResponse
// Check summary data has counts for all states
expect(response.content[1].text).toContain('"alert":1')
expect(response.content[1].text).toContain('"warn":1')
expect(response.content[1].text).toContain('"noData":1')
expect(response.content[1].text).toContain('"ok":1')
expect(response.content[1].text).toContain('"ignored":1')
expect(response.content[1].text).toContain('"skipped":1')
expect(response.content[1].text).toContain('"unknown":1')
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json([])
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {
name: 'non-existent-monitor',
})
const response = (await toolHandlers.get_monitors(
request,
)) as unknown as DatadogToolResponse
// Check that response contains empty array
expect(response.content[0].text).toContain('Monitors: []')
// Check that summary shows all zeros
expect(response.content[1].text).toContain('"alert":0')
expect(response.content[1].text).toContain('"warn":0')
expect(response.content[1].text).toContain('"noData":0')
expect(response.content[1].text).toContain('"ok":0')
})()
server.close()
})
it('should handle null response', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json(null)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {})
await expect(toolHandlers.get_monitors(request)).rejects.toThrow(
'No monitors data returned',
)
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {})
await expect(toolHandlers.get_monitors(request)).rejects.toThrow()
})()
server.close()
})
it('should handle rate limit errors', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Rate limit exceeded'] },
{ status: 429 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {})
await expect(toolHandlers.get_monitors(request)).rejects.toThrow(
'Rate limit exceeded',
)
})()
server.close()
})
it('should handle server errors', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Internal server error'] },
{ status: 500 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {})
await expect(toolHandlers.get_monitors(request)).rejects.toThrow(
'Internal server error',
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/traces.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v2 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createTracesToolHandlers } from '../../src/tools/traces/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const tracesEndpoint = `${baseUrl}/v2/spans/events/search`
describe('Traces Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v2.SpansApi(datadogConfig)
const toolHandlers = createTracesToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/spans/#search-spans
describe.concurrent('list_traces', async () => {
it('should list traces with basic query', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'span-id-1',
type: 'spans',
attributes: {
service: 'web-api',
name: 'http.request',
resource: 'GET /api/users',
trace_id: 'trace-id-1',
span_id: 'span-id-1',
parent_id: 'parent-id-1',
start: 1640995100000000000,
duration: 500000000,
error: 1,
meta: {
'http.method': 'GET',
'http.status_code': '500',
'error.type': 'Internal Server Error',
},
},
},
{
id: 'span-id-2',
type: 'spans',
attributes: {
service: 'web-api',
name: 'http.request',
resource: 'GET /api/products',
trace_id: 'trace-id-2',
span_id: 'span-id-2',
parent_id: 'parent-id-2',
start: 1640995000000000000,
duration: 300000000,
error: 1,
meta: {
'http.method': 'GET',
'http.status_code': '500',
'error.type': 'Internal Server Error',
},
},
},
],
meta: {
page: {
after: 'cursor-value',
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: 'http.status_code:500',
from: 1640995000,
to: 1640996000,
limit: 50,
})
const response = (await toolHandlers.list_traces(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Traces:')
expect(response.content[0].text).toContain('web-api')
expect(response.content[0].text).toContain('GET /api/users')
expect(response.content[0].text).toContain('GET /api/products')
expect(response.content[0].text).toContain('count":2')
})()
server.close()
})
it('should include service and operation filters', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'span-id-3',
type: 'spans',
attributes: {
service: 'payment-service',
name: 'process-payment',
resource: 'process-payment',
trace_id: 'trace-id-3',
span_id: 'span-id-3',
parent_id: 'parent-id-3',
start: 1640995100000000000,
duration: 800000000,
error: 1,
meta: {
'error.type': 'PaymentProcessingError',
},
},
},
],
meta: {
page: {
after: null,
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: 'error:true',
from: 1640995000,
to: 1640996000,
service: 'payment-service',
operation: 'process-payment',
})
const response = (await toolHandlers.list_traces(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('payment-service')
expect(response.content[0].text).toContain('process-payment')
expect(response.content[0].text).toContain('PaymentProcessingError')
})()
server.close()
})
it('should handle ascending sort', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'span-id-oldest',
type: 'spans',
attributes: {
service: 'api',
name: 'http.request',
start: 1640995000000000000,
},
},
{
id: 'span-id-newest',
type: 'spans',
attributes: {
service: 'api',
name: 'http.request',
start: 1640995100000000000,
},
},
],
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: '',
from: 1640995000,
to: 1640996000,
sort: 'timestamp', // ascending order
})
const response = (await toolHandlers.list_traces(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('span-id-oldest')
expect(response.content[0].text).toContain('span-id-newest')
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json({
data: [],
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: 'service:non-existent',
from: 1640995000,
to: 1640996000,
})
const response = (await toolHandlers.list_traces(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Traces:')
expect(response.content[0].text).toContain('count":0')
expect(response.content[0].text).toContain('traces":[]')
})()
server.close()
})
it('should handle null response data', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json({
data: null,
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: '',
from: 1640995000,
to: 1640996000,
})
await expect(toolHandlers.list_traces(request)).rejects.toThrow(
'No traces data returned',
)
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: '',
from: 1640995000,
to: 1640996000,
})
await expect(toolHandlers.list_traces(request)).rejects.toThrow()
})()
server.close()
})
it('should handle rate limit errors', async () => {
const mockHandler = http.post(tracesEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Rate limit exceeded'] },
{ status: 429 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_traces', {
query: '',
from: 1640995000,
to: 1640996000,
})
await expect(toolHandlers.list_traces(request)).rejects.toThrow(
/errors./,
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/downtimes.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v1 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createDowntimesToolHandlers } from '../../src/tools/downtimes/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const downtimesEndpoint = `${baseUrl}/v1/downtime`
describe('Downtimes Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v1.DowntimesApi(datadogConfig)
const toolHandlers = createDowntimesToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/downtimes/#get-all-downtimes
describe.concurrent('list_downtimes', async () => {
it('should list downtimes', async () => {
const mockHandler = http.get(downtimesEndpoint, async () => {
return HttpResponse.json([
{
id: 123456789,
active: true,
disabled: false,
start: 1640995100,
end: 1640995200,
scope: ['host:test-host'],
message: 'Test downtime',
monitor_id: 87654321,
created: 1640995000,
creator_id: 12345,
updated_at: 1640995010,
monitor_tags: ['env:test'],
},
{
id: 987654321,
active: false,
disabled: false,
start: 1641095100,
end: 1641095200,
scope: ['service:web'],
message: 'Another test downtime',
monitor_id: null,
created: 1641095000,
creator_id: 12345,
updated_at: 1641095010,
monitor_tags: ['service:web'],
},
])
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_downtimes', {
currentOnly: true,
})
const response = (await toolHandlers.list_downtimes(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Listed downtimes:')
expect(response.content[0].text).toContain('Test downtime')
expect(response.content[0].text).toContain('Another test downtime')
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.get(downtimesEndpoint, async () => {
return HttpResponse.json([])
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_downtimes', {
currentOnly: false,
})
const response = (await toolHandlers.list_downtimes(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Listed downtimes:')
expect(response.content[0].text).toContain('[]')
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.get(downtimesEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_downtimes', {})
await expect(toolHandlers.list_downtimes(request)).rejects.toThrow()
})()
server.close()
})
it('should handle rate limit errors', async () => {
const mockHandler = http.get(downtimesEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Rate limit exceeded'] },
{ status: 429 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_downtimes', {})
await expect(toolHandlers.list_downtimes(request)).rejects.toThrow(
'Rate limit exceeded',
)
})()
server.close()
})
})
// https://docs.datadoghq.com/api/latest/downtimes/#schedule-a-downtime
describe.concurrent('schedule_downtime', async () => {
it('should schedule a downtime', async () => {
const mockHandler = http.post(downtimesEndpoint, async () => {
return HttpResponse.json({
id: 123456789,
active: true,
disabled: false,
start: 1640995100,
end: 1640995200,
scope: ['host:test-host'],
message: 'Scheduled maintenance',
monitor_id: null,
timezone: 'UTC',
created: 1640995000,
creator_id: 12345,
updated_at: 1640995000,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('schedule_downtime', {
scope: 'host:test-host',
start: 1640995100,
end: 1640995200,
message: 'Scheduled maintenance',
timezone: 'UTC',
})
const response = (await toolHandlers.schedule_downtime(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Scheduled downtime:')
expect(response.content[0].text).toContain('123456789')
expect(response.content[0].text).toContain('Scheduled maintenance')
})()
server.close()
})
it('should schedule a recurring downtime', async () => {
const mockHandler = http.post(downtimesEndpoint, async () => {
return HttpResponse.json({
id: 123456789,
active: true,
disabled: false,
message: 'Weekly maintenance',
scope: ['service:api'],
recurrence: {
type: 'weeks',
period: 1,
week_days: ['Mon'],
},
created: 1640995000,
creator_id: 12345,
updated_at: 1640995000,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('schedule_downtime', {
scope: 'service:api',
message: 'Weekly maintenance',
recurrence: {
type: 'weeks',
period: 1,
weekDays: ['Mon'],
},
})
const response = (await toolHandlers.schedule_downtime(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Scheduled downtime:')
expect(response.content[0].text).toContain('Weekly maintenance')
expect(response.content[0].text).toContain('weeks')
expect(response.content[0].text).toContain('Mon')
})()
server.close()
})
it('should handle validation errors', async () => {
const mockHandler = http.post(downtimesEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Invalid scope format'] },
{ status: 400 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('schedule_downtime', {
scope: 'invalid:format',
start: 1640995100,
end: 1640995200,
})
await expect(toolHandlers.schedule_downtime(request)).rejects.toThrow(
'Invalid scope format',
)
})()
server.close()
})
})
// https://docs.datadoghq.com/api/latest/downtimes/#cancel-a-downtime
describe.concurrent('cancel_downtime', async () => {
it('should cancel a downtime', async () => {
const downtimeId = 123456789
const mockHandler = http.delete(
`${downtimesEndpoint}/${downtimeId}`,
async () => {
return new HttpResponse(null, { status: 204 })
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('cancel_downtime', {
downtimeId,
})
const response = (await toolHandlers.cancel_downtime(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
`Cancelled downtime with ID: ${downtimeId}`,
)
})()
server.close()
})
it('should handle not found errors', async () => {
const downtimeId = 999999999
const mockHandler = http.delete(
`${downtimesEndpoint}/${downtimeId}`,
async () => {
return HttpResponse.json(
{ errors: ['Downtime not found'] },
{ status: 404 },
)
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('cancel_downtime', {
downtimeId,
})
await expect(toolHandlers.cancel_downtime(request)).rejects.toThrow(
'Downtime not found',
)
})()
server.close()
})
it('should handle server errors', async () => {
const downtimeId = 123456789
const mockHandler = http.delete(
`${downtimesEndpoint}/${downtimeId}`,
async () => {
return HttpResponse.json(
{ errors: ['Internal server error'] },
{ status: 500 },
)
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('cancel_downtime', {
downtimeId,
})
await expect(toolHandlers.cancel_downtime(request)).rejects.toThrow(
'Internal server error',
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/hosts.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v1 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createHostsToolHandlers } from '../../src/tools/hosts/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const hostsBaseEndpoint = `${baseUrl}/v1/hosts`
const hostBaseEndpoint = `${baseUrl}/v1/host`
const hostTotalsEndpoint = `${hostsBaseEndpoint}/totals`
describe('Hosts Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v1.HostsApi(datadogConfig)
const toolHandlers = createHostsToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/hosts/#get-all-hosts
describe.concurrent('list_hosts', async () => {
it('should list hosts with filters', async () => {
const mockHandler = http.get(hostsBaseEndpoint, async () => {
return HttpResponse.json({
host_list: [
{
name: 'web-server-01',
id: 12345,
aliases: ['web-server-01.example.com'],
apps: ['nginx', 'redis'],
is_muted: false,
last_reported_time: 1640995100,
meta: {
platform: 'linux',
agent_version: '7.36.1',
socket_hostname: 'web-server-01',
},
metrics: {
load: 0.5,
cpu: 45.6,
memory: 78.2,
},
sources: ['agent'],
up: true,
},
{
name: 'db-server-01',
id: 67890,
aliases: ['db-server-01.example.com'],
apps: ['postgres'],
is_muted: true,
last_reported_time: 1640995000,
meta: {
platform: 'linux',
agent_version: '7.36.1',
socket_hostname: 'db-server-01',
},
metrics: {
load: 1.2,
cpu: 78.3,
memory: 92.1,
},
sources: ['agent'],
up: true,
},
],
total_matching: 2,
total_returned: 2,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_hosts', {
filter: 'env:production',
sort_field: 'status',
sort_dir: 'desc',
include_hosts_metadata: true,
})
const response = (await toolHandlers.list_hosts(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Hosts:')
expect(response.content[0].text).toContain('web-server-01')
expect(response.content[0].text).toContain('db-server-01')
expect(response.content[0].text).toContain('postgres')
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.get(hostsBaseEndpoint, async () => {
return HttpResponse.json({
host_list: [],
total_matching: 0,
total_returned: 0,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_hosts', {
filter: 'non-existent:value',
})
const response = (await toolHandlers.list_hosts(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Hosts: []')
})()
server.close()
})
it('should handle missing host_list', async () => {
const mockHandler = http.get(hostsBaseEndpoint, async () => {
return HttpResponse.json({
total_matching: 0,
total_returned: 0,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_hosts', {})
await expect(toolHandlers.list_hosts(request)).rejects.toThrow(
'No hosts data returned',
)
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.get(hostsBaseEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_hosts', {})
await expect(toolHandlers.list_hosts(request)).rejects.toThrow()
})()
server.close()
})
})
// https://docs.datadoghq.com/api/latest/hosts/#get-the-total-number-of-active-hosts
describe.concurrent('get_active_hosts_count', async () => {
it('should get active hosts count', async () => {
const mockHandler = http.get(hostTotalsEndpoint, async () => {
return HttpResponse.json({
total_up: 512,
total_active: 520,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_active_hosts_count', {
from: 3600,
})
const response = (await toolHandlers.get_active_hosts_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('total_active')
expect(response.content[0].text).toContain('520')
expect(response.content[0].text).toContain('total_up')
expect(response.content[0].text).toContain('512')
})()
server.close()
})
it('should use default from value if not provided', async () => {
const mockHandler = http.get(hostTotalsEndpoint, async () => {
return HttpResponse.json({
total_up: 510,
total_active: 518,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_active_hosts_count', {})
const response = (await toolHandlers.get_active_hosts_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('518')
expect(response.content[0].text).toContain('510')
})()
server.close()
})
it('should handle server errors', async () => {
const mockHandler = http.get(hostTotalsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Internal server error'] },
{ status: 500 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_active_hosts_count', {})
await expect(
toolHandlers.get_active_hosts_count(request),
).rejects.toThrow()
})()
server.close()
})
})
// https://docs.datadoghq.com/api/latest/hosts/#mute-a-host
describe.concurrent('mute_host', async () => {
it('should mute a host', async () => {
const mockHandler = http.post(
`${hostBaseEndpoint}/:hostname/mute`,
async ({ params }) => {
return HttpResponse.json({
action: 'muted',
hostname: params.hostname,
message: 'Maintenance in progress',
end: 1641095100,
})
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('mute_host', {
hostname: 'test-host',
message: 'Maintenance in progress',
end: 1641095100,
override: true,
})
const response = (await toolHandlers.mute_host(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('success')
expect(response.content[0].text).toContain('test-host')
expect(response.content[0].text).toContain('Maintenance in progress')
})()
server.close()
})
it('should handle host not found', async () => {
const mockHandler = http.post(
`${hostBaseEndpoint}/:hostname/mute`,
async () => {
return HttpResponse.json(
{ errors: ['Host not found'] },
{ status: 404 },
)
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('mute_host', {
hostname: 'non-existent-host',
})
await expect(toolHandlers.mute_host(request)).rejects.toThrow(
'Host not found',
)
})()
server.close()
})
})
// https://docs.datadoghq.com/api/latest/hosts/#unmute-a-host
describe.concurrent('unmute_host', async () => {
it('should unmute a host', async () => {
const mockHandler = http.post(
`${hostBaseEndpoint}/:hostname/unmute`,
async ({ params }) => {
return HttpResponse.json({
action: 'unmuted',
hostname: params.hostname,
})
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('unmute_host', {
hostname: 'test-host',
})
const response = (await toolHandlers.unmute_host(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('success')
expect(response.content[0].text).toContain('test-host')
expect(response.content[0].text).toContain('unmuted')
})()
server.close()
})
it('should handle host not found', async () => {
const mockHandler = http.post(
`${hostBaseEndpoint}/:hostname/unmute`,
async () => {
return HttpResponse.json(
{ errors: ['Host not found'] },
{ status: 404 },
)
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('unmute_host', {
hostname: 'non-existent-host',
})
await expect(toolHandlers.unmute_host(request)).rejects.toThrow(
'Host not found',
)
})()
server.close()
})
it('should handle host already unmuted', async () => {
const mockHandler = http.post(
`${hostBaseEndpoint}/:hostname/unmute`,
async () => {
return HttpResponse.json(
{ errors: ['Host is not muted'] },
{ status: 400 },
)
},
)
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('unmute_host', {
hostname: 'already-unmuted-host',
})
await expect(toolHandlers.unmute_host(request)).rejects.toThrow(
'Host is not muted',
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/logs.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v2 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createLogsToolHandlers } from '../../src/tools/logs/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const logsEndpoint = `${baseUrl}/v2/logs/events/search`
describe('Logs Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v2.LogsApi(datadogConfig)
const toolHandlers = createLogsToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/logs/#search-logs
describe.concurrent('get_logs', async () => {
it('should retrieve logs', async () => {
// Mock API response based on Datadog API documentation
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'AAAAAXGLdD0AAABPV-5whqgB',
attributes: {
timestamp: 1640995199999,
status: 'info',
message: 'Test log message',
service: 'test-service',
tags: ['env:test'],
},
type: 'log',
},
],
meta: {
page: {
after:
'eyJzdGFydEF0IjoiQVFBQUFYR0xkRDBBQUFCUFYtNXdocWdCIiwiaW5kZXgiOiJtYWluIn0=',
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_logs', {
query: 'service:test-service',
from: 1640995100, // epoch seconds
to: 1640995200, // epoch seconds
limit: 10,
})
const response = (await toolHandlers.get_logs(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Logs data')
expect(response.content[0].text).toContain('Test log message')
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json({
data: [],
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_logs', {
query: 'service:non-existent',
from: 1640995100,
to: 1640995200,
})
const response = (await toolHandlers.get_logs(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Logs data')
expect(response.content[0].text).toContain('[]')
})()
server.close()
})
it('should handle null response data', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json({
data: null,
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_logs', {
query: 'service:test',
from: 1640995100,
to: 1640995200,
})
await expect(toolHandlers.get_logs(request)).rejects.toThrow(
'No logs data returned',
)
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_logs', {
query: 'service:test',
from: 1640995100,
to: 1640995200,
})
await expect(toolHandlers.get_logs(request)).rejects.toThrow()
})()
server.close()
})
it('should handle rate limit errors', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Rate limit exceeded'] },
{ status: 429 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_logs', {
query: 'service:test',
from: 1640995100,
to: 1640995200,
})
await expect(toolHandlers.get_logs(request)).rejects.toThrow(
'Rate limit exceeded',
)
})()
server.close()
})
it('should handle server errors', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Internal server error'] },
{ status: 500 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_logs', {
query: 'service:test',
from: 1640995100,
to: 1640995200,
})
await expect(toolHandlers.get_logs(request)).rejects.toThrow(
'Internal server error',
)
})()
server.close()
})
})
describe.concurrent('get_all_services', async () => {
it('should extract unique service names from logs', async () => {
// Mock API response with multiple services
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'AAAAAXGLdD0AAABPV-5whqgB',
attributes: {
timestamp: 1640995199000,
status: 'info',
message: 'Test log message 1',
service: 'web-service',
tags: ['env:test'],
},
type: 'log',
},
{
id: 'AAAAAXGLdD0AAABPV-5whqgC',
attributes: {
timestamp: 1640995198000,
status: 'info',
message: 'Test log message 2',
service: 'api-service',
tags: ['env:test'],
},
type: 'log',
},
{
id: 'AAAAAXGLdD0AAABPV-5whqgD',
attributes: {
timestamp: 1640995197000,
status: 'info',
message: 'Test log message 3',
service: 'web-service', // Duplicate service to test uniqueness
tags: ['env:test'],
},
type: 'log',
},
{
id: 'AAAAAXGLdD0AAABPV-5whqgE',
attributes: {
timestamp: 1640995196000,
status: 'error',
message: 'Test error message',
service: 'database-service',
tags: ['env:test'],
},
type: 'log',
},
],
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_all_services', {
query: '*',
from: 1640995100, // epoch seconds
to: 1640995200, // epoch seconds
limit: 100,
})
const response = (await toolHandlers.get_all_services(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Services')
// Check if response contains the expected services (sorted alphabetically)
const expected = ['api-service', 'database-service', 'web-service']
expected.forEach((service) => {
expect(response.content[0].text).toContain(service)
})
// Check that we've extracted unique services (no duplicates)
const servicesText = response.content[0].text
const servicesJson = JSON.parse(
servicesText.substring(
servicesText.indexOf('['),
servicesText.lastIndexOf(']') + 1,
),
)
expect(servicesJson).toHaveLength(3) // Only 3 unique services, not 4
expect(servicesJson).toEqual(expected)
})()
server.close()
})
it('should handle logs with missing service attributes', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'AAAAAXGLdD0AAABPV-5whqgB',
attributes: {
timestamp: 1640995199000,
status: 'info',
message: 'Test log message 1',
service: 'web-service',
tags: ['env:test'],
},
type: 'log',
},
{
id: 'AAAAAXGLdD0AAABPV-5whqgC',
attributes: {
timestamp: 1640995198000,
status: 'info',
message: 'Test log message with no service',
// No service attribute
tags: ['env:test'],
},
type: 'log',
},
],
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_all_services', {
query: '*',
from: 1640995100,
to: 1640995200,
limit: 100,
})
const response = (await toolHandlers.get_all_services(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Services')
expect(response.content[0].text).toContain('web-service')
// Ensure we only have one service (the one with a defined service attribute)
const servicesText = response.content[0].text
const servicesJson = JSON.parse(
servicesText.substring(
servicesText.indexOf('['),
servicesText.lastIndexOf(']') + 1,
),
)
expect(servicesJson).toHaveLength(1)
})()
server.close()
})
it('should handle empty response data', async () => {
const mockHandler = http.post(logsEndpoint, async () => {
return HttpResponse.json({
data: [],
meta: {
page: {},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_all_services', {
query: 'service:non-existent',
from: 1640995100,
to: 1640995200,
limit: 100,
})
const response = (await toolHandlers.get_all_services(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Services')
expect(response.content[0].text).toContain('[]') // Empty array of services
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/incident.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v2 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createIncidentToolHandlers } from '../../src/tools/incident/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const incidentsEndpoint = `${baseUrl}/v2/incidents`
describe('Incident Tool', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v2.IncidentsApi(datadogConfig)
const toolHandlers = createIncidentToolHandlers(apiInstance)
// https://docs.datadoghq.com/api/latest/incidents/#get-a-list-of-incidents
describe.concurrent('list_incidents', async () => {
it('should list incidents with pagination', async () => {
const mockHandler = http.get(incidentsEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'incident-123',
type: 'incidents',
attributes: {
title: 'API Outage',
created: '2023-01-15T10:00:00.000Z',
modified: '2023-01-15T11:30:00.000Z',
status: 'active',
severity: 'SEV-1',
customer_impact_scope: 'All API services are down',
customer_impact_start: '2023-01-15T10:00:00.000Z',
customer_impacted: true,
},
relationships: {
created_by: {
data: {
id: 'user-123',
type: 'users',
},
},
},
},
{
id: 'incident-456',
type: 'incidents',
attributes: {
title: 'Database Slowdown',
created: '2023-01-10T09:00:00.000Z',
modified: '2023-01-10T12:00:00.000Z',
status: 'resolved',
severity: 'SEV-2',
customer_impact_scope: 'Database queries are slow',
customer_impact_start: '2023-01-10T09:00:00.000Z',
customer_impact_end: '2023-01-10T12:00:00.000Z',
customer_impacted: true,
},
relationships: {
created_by: {
data: {
id: 'user-456',
type: 'users',
},
},
},
},
],
meta: {
pagination: {
offset: 10,
size: 20,
total: 45,
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_incidents', {
pageSize: 20,
pageOffset: 10,
})
const response = (await toolHandlers.list_incidents(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Listed incidents:')
expect(response.content[0].text).toContain('API Outage')
expect(response.content[0].text).toContain('Database Slowdown')
expect(response.content[0].text).toContain('incident-123')
expect(response.content[0].text).toContain('incident-456')
})()
server.close()
})
it('should use default pagination parameters if not provided', async () => {
const mockHandler = http.get(incidentsEndpoint, async () => {
return HttpResponse.json({
data: [
{
id: 'incident-789',
type: 'incidents',
attributes: {
title: 'Network Connectivity Issues',
status: 'active',
},
},
],
meta: {
pagination: {
offset: 0,
size: 10,
total: 1,
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_incidents', {})
const response = (await toolHandlers.list_incidents(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Listed incidents:')
expect(response.content[0].text).toContain(
'Network Connectivity Issues',
)
})()
server.close()
})
it('should handle empty response', async () => {
const mockHandler = http.get(incidentsEndpoint, async () => {
return HttpResponse.json({
data: [],
meta: {
pagination: {
offset: 0,
size: 10,
total: 0,
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_incidents', {})
const response = (await toolHandlers.list_incidents(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Listed incidents:')
expect(response.content[0].text).not.toContain('incident-')
})()
server.close()
})
it('should handle null data response', async () => {
const mockHandler = http.get(incidentsEndpoint, async () => {
return HttpResponse.json({
data: null,
meta: {
pagination: {
offset: 0,
size: 10,
total: 0,
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_incidents', {})
await expect(toolHandlers.list_incidents(request)).rejects.toThrow(
'No incidents data returned',
)
})()
server.close()
})
it('should handle authentication errors', async () => {
const mockHandler = http.get(incidentsEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Authentication failed'] },
{ status: 403 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('list_incidents', {})
await expect(toolHandlers.list_incidents(request)).rejects.toThrow()
})()
server.close()
})
})
// https://docs.datadoghq.com/api/latest/incidents/#get-incident-details
describe.concurrent('get_incident', async () => {
it('should get a specific incident', async () => {
const incidentId = 'incident-123'
const specificIncidentEndpoint = `${incidentsEndpoint}/${incidentId}`
const mockHandler = http.get(specificIncidentEndpoint, async () => {
return HttpResponse.json({
data: {
id: 'incident-123',
type: 'incidents',
attributes: {
title: 'API Outage',
created: '2023-01-15T10:00:00.000Z',
modified: '2023-01-15T11:30:00.000Z',
status: 'active',
severity: 'SEV-1',
customer_impact_scope: 'All API services are down',
customer_impact_start: '2023-01-15T10:00:00.000Z',
customer_impacted: true,
fields: {
summary: 'Complete API outage affecting all customers',
root_cause: 'Database connection pool exhausted',
detection_method: 'Monitor alert',
services: ['api', 'database'],
teams: ['backend', 'sre'],
},
timeline: {
entries: [
{
timestamp: '2023-01-15T10:00:00.000Z',
content: 'Incident detected',
type: 'incident_created',
},
{
timestamp: '2023-01-15T10:05:00.000Z',
content: 'Investigation started',
type: 'comment',
},
],
},
},
relationships: {
created_by: {
data: {
id: 'user-123',
type: 'users',
},
},
commander: {
data: {
id: 'user-456',
type: 'users',
},
},
},
},
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_incident', {
incidentId: 'incident-123',
})
const response = (await toolHandlers.get_incident(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Incident:')
expect(response.content[0].text).toContain('API Outage')
expect(response.content[0].text).toContain('incident-123')
expect(response.content[0].text).toContain('SEV-1')
expect(response.content[0].text).toContain(
'Database connection pool exhausted',
)
})()
server.close()
})
it('should handle incident not found', async () => {
const incidentId = 'non-existent-incident'
const specificIncidentEndpoint = `${incidentsEndpoint}/${incidentId}`
const mockHandler = http.get(specificIncidentEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Incident not found'] },
{ status: 404 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_incident', {
incidentId: 'non-existent-incident',
})
await expect(toolHandlers.get_incident(request)).rejects.toThrow(
'Incident not found',
)
})()
server.close()
})
it('should handle null data response', async () => {
const incidentId = 'incident-123'
const specificIncidentEndpoint = `${incidentsEndpoint}/${incidentId}`
const mockHandler = http.get(specificIncidentEndpoint, async () => {
return HttpResponse.json({
data: null,
})
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_incident', {
incidentId: 'incident-123',
})
await expect(toolHandlers.get_incident(request)).rejects.toThrow(
'No incident data returned',
)
})()
server.close()
})
it('should handle server errors', async () => {
const incidentId = 'incident-123'
const specificIncidentEndpoint = `${incidentsEndpoint}/${incidentId}`
const mockHandler = http.get(specificIncidentEndpoint, async () => {
return HttpResponse.json(
{ errors: ['Internal server error'] },
{ status: 500 },
)
})
const server = setupServer(mockHandler)
await server.boundary(async () => {
const request = createMockToolRequest('get_incident', {
incidentId: 'incident-123',
})
await expect(toolHandlers.get_incident(request)).rejects.toThrow(
'Internal server error',
)
})()
server.close()
})
})
})
```
--------------------------------------------------------------------------------
/tests/tools/rum.test.ts:
--------------------------------------------------------------------------------
```typescript
import { v2 } from '@datadog/datadog-api-client'
import { describe, it, expect } from 'vitest'
import { createDatadogConfig } from '../../src/utils/datadog'
import { createRumToolHandlers } from '../../src/tools/rum/tool'
import { createMockToolRequest } from '../helpers/mock'
import { http, HttpResponse } from 'msw'
import { setupServer } from '../helpers/msw'
import { baseUrl, DatadogToolResponse } from '../helpers/datadog'
const getCommonServer = () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [
{
id: 'event1',
attributes: {
attributes: {
application: {
name: 'Application 1',
},
session: { id: 'sess1' },
view: {
load_time: 123,
first_contentful_paint: 456,
},
},
},
},
{
id: 'event2',
attributes: {
attributes: {
application: {
name: 'Application 1',
},
session: { id: 'sess2' },
view: {
load_time: 789,
first_contentful_paint: 101,
},
},
},
},
{
id: 'event3',
attributes: {
attributes: {
application: {
name: 'Application 2',
},
session: { id: 'sess3' },
view: {
load_time: 234,
first_contentful_paint: 567,
},
},
},
},
],
})
}),
)
return server
}
describe('RUM Tools', () => {
if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
}
const datadogConfig = createDatadogConfig({
apiKeyAuth: process.env.DATADOG_API_KEY,
appKeyAuth: process.env.DATADOG_APP_KEY,
site: process.env.DATADOG_SITE,
})
const apiInstance = new v2.RUMApi(datadogConfig)
const toolHandlers = createRumToolHandlers(apiInstance)
describe.concurrent('get_rum_applications', async () => {
it('should retrieve RUM applications', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/applications`, async () => {
return HttpResponse.json({
data: [
{
attributes: {
application_id: '7124cba6-8ffe-4122-a644-82c7f4c21ae0',
name: 'Application 1',
created_at: 1725949945579,
created_by_handle: '[email protected]',
org_id: 1,
type: 'browser',
updated_at: 1725949945579,
updated_by_handle: 'Datadog',
},
id: '7124cba6-8ffe-4122-a644-82c7f4c21ae0',
type: 'rum_application',
},
],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_applications', {})
const response = (await toolHandlers.get_rum_applications(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('RUM applications')
expect(response.content[0].text).toContain('Application 1')
expect(response.content[0].text).toContain('rum_application')
})()
server.close()
})
})
describe.concurrent('get_rum_events', async () => {
it('should retrieve RUM events', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_events', {
query: '*',
from: 1640995100,
to: 1640995200,
limit: 10,
})
const response = (await toolHandlers.get_rum_events(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('RUM events data')
expect(response.content[0].text).toContain('event1')
expect(response.content[0].text).toContain('event2')
expect(response.content[0].text).toContain('event3')
})()
server.close()
})
})
describe.concurrent('get_rum_grouped_event_count', async () => {
it('should retrieve grouped event counts by application name', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '*',
from: 1640995100,
to: 1640995200,
groupBy: 'application.name',
})
const response = (await toolHandlers.get_rum_grouped_event_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Session counts (grouped by application.name): {"Application 1":2,"Application 2":1}',
)
})()
server.close()
})
it('should handle custom query filter', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '@application.name:Application 1',
from: 1640995100,
to: 1640995200,
groupBy: 'application.name',
})
const response = (await toolHandlers.get_rum_grouped_event_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Session counts (grouped by application.name):',
)
expect(response.content[0].text).toContain('"Application 1":2')
})()
server.close()
})
it('should handle deeper nested path for groupBy', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '*',
from: 1640995100,
to: 1640995200,
groupBy: 'view.load_time',
})
const response = (await toolHandlers.get_rum_grouped_event_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Session counts (grouped by view.load_time):',
)
expect(response.content[0].text).toContain('"123":1')
expect(response.content[0].text).toContain('"789":1')
expect(response.content[0].text).toContain('"234":1')
})()
server.close()
})
it('should handle invalid groupBy path gracefully', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '*',
from: 1640995100,
to: 1640995200,
groupBy: 'nonexistent.path',
})
const response = (await toolHandlers.get_rum_grouped_event_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Session counts (grouped by nonexistent.path): {"unknown":3}',
)
})()
server.close()
})
it('should handle empty data response', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '*',
from: 1640995100,
to: 1640995200,
groupBy: 'application.name',
})
const response = (await toolHandlers.get_rum_grouped_event_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Session counts (grouped by application.name): {}',
)
})()
server.close()
})
it('should handle null data response', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: null,
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '*',
from: 1640995100,
to: 1640995200,
groupBy: 'application.name',
})
await expect(
toolHandlers.get_rum_grouped_event_count(request),
).rejects.toThrow('No RUM events data returned')
})()
server.close()
})
it('should handle events without attributes field', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [
{
id: 'event1',
// Missing attributes field
},
{
id: 'event2',
attributes: {
// Missing attributes.attributes field
},
},
{
id: 'event3',
attributes: {
attributes: {
application: {
name: 'Application 3',
},
// Missing session field
},
},
},
],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_grouped_event_count', {
query: '*',
from: 1640995100,
to: 1640995200,
groupBy: 'application.name',
})
const response = (await toolHandlers.get_rum_grouped_event_count(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Session counts (grouped by application.name): {"Application 3":0}',
)
})()
server.close()
})
})
describe.concurrent('get_rum_page_performance', async () => {
it('should retrieve page performance metrics', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['view.load_time', 'view.first_contentful_paint'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain(
'Page performance metrics: {"view.load_time":{"avg":382,"min":123,"max":789,"count":3},"view.first_contentful_paint":{"avg":374.6666666666667,"min":101,"max":567,"count":3}}',
)
})()
server.close()
})
it('should use default metric names if not provided', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
// metricNames not provided, should use defaults
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain('view.load_time')
expect(response.content[0].text).toContain(
'view.first_contentful_paint',
)
// Default also includes largest_contentful_paint, but our mock doesn't have this data
expect(response.content[0].text).toContain(
'view.largest_contentful_paint',
)
})()
server.close()
})
it('should handle custom query filter', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '@application.name:Application 1',
from: 1640995100,
to: 1640995200,
metricNames: ['view.load_time'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain('view.load_time')
})()
server.close()
})
it('should handle empty data response', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['view.load_time', 'view.first_contentful_paint'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain(
'"view.load_time":{"avg":0,"min":0,"max":0,"count":0}',
)
expect(response.content[0].text).toContain(
'"view.first_contentful_paint":{"avg":0,"min":0,"max":0,"count":0}',
)
})()
server.close()
})
it('should handle null data response', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: null,
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['view.load_time'],
})
await expect(
toolHandlers.get_rum_page_performance(request),
).rejects.toThrow('No RUM events data returned')
})()
server.close()
})
it('should handle events without attributes field', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [
{
id: 'event1',
// Missing attributes field
},
{
id: 'event2',
attributes: {
// Missing attributes.attributes field
},
},
{
id: 'event3',
attributes: {
attributes: {
application: {
name: 'Application 3',
},
// Missing view field with metrics
},
},
},
],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['view.load_time', 'view.first_contentful_paint'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain(
'"view.load_time":{"avg":0,"min":0,"max":0,"count":0}',
)
expect(response.content[0].text).toContain(
'"view.first_contentful_paint":{"avg":0,"min":0,"max":0,"count":0}',
)
})()
server.close()
})
it('should handle deeply nested metric paths', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [
{
id: 'event1',
attributes: {
attributes: {
application: {
name: 'Application 1',
},
deep: {
nested: {
metric: 42,
},
},
},
},
},
{
id: 'event2',
attributes: {
attributes: {
application: {
name: 'Application 2',
},
deep: {
nested: {
metric: 84,
},
},
},
},
},
],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['deep.nested.metric'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain(
'"deep.nested.metric":{"avg":63,"min":42,"max":84,"count":2}',
)
})()
server.close()
})
it('should handle mixed metric availability', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [
{
id: 'event1',
attributes: {
attributes: {
view: {
load_time: 100,
// first_contentful_paint is missing
},
},
},
},
{
id: 'event2',
attributes: {
attributes: {
view: {
// load_time is missing
first_contentful_paint: 200,
},
},
},
},
],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['view.load_time', 'view.first_contentful_paint'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain(
'"view.load_time":{"avg":100,"min":100,"max":100,"count":1}',
)
expect(response.content[0].text).toContain(
'"view.first_contentful_paint":{"avg":200,"min":200,"max":200,"count":1}',
)
})()
server.close()
})
it('should handle non-numeric values gracefully', async () => {
const server = setupServer(
http.get(`${baseUrl}/v2/rum/events`, async () => {
return HttpResponse.json({
data: [
{
id: 'event1',
attributes: {
attributes: {
invalid_metric: 'not-a-number',
view: {
load_time: 100,
},
},
},
},
],
})
}),
)
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_performance', {
query: '*',
from: 1640995100,
to: 1640995200,
metricNames: ['invalid_metric', 'view.load_time'],
})
const response = (await toolHandlers.get_rum_page_performance(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Page performance metrics')
expect(response.content[0].text).toContain(
'"invalid_metric":{"avg":0,"min":0,"max":0,"count":0}',
)
expect(response.content[0].text).toContain(
'"view.load_time":{"avg":100,"min":100,"max":100,"count":1}',
)
})()
server.close()
})
})
describe.concurrent('get_rum_page_waterfall', async () => {
it('should retrieve page waterfall data', async () => {
const server = getCommonServer()
await server.boundary(async () => {
const request = createMockToolRequest('get_rum_page_waterfall', {
applicationName: 'Application 1',
sessionId: 'sess1',
})
const response = (await toolHandlers.get_rum_page_waterfall(
request,
)) as unknown as DatadogToolResponse
expect(response.content[0].text).toContain('Waterfall data')
expect(response.content[0].text).toContain('event1')
expect(response.content[0].text).toContain('event2')
})()
server.close()
})
})
})
```