# 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()
    })
  })
})
```