Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions inspector.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"local": {
"command": "node",
"args": ["./build/index.js"]
}
}
}
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@winor30/mcp-server-datadog",
"version": "1.6.0",
"name": "@anyshift/datadog-mcp-server",
"version": "1.7.4",
"description": "MCP server for interacting with Datadog API",
"repository": {
"type": "git",
"url": "https://github.com/winor30/mcp-server-datadog.git"
"url": "https://github.com:anyshift-engineering/mcp-server-datadog.git"
},
"type": "module",
"bin": {
Expand Down Expand Up @@ -32,7 +32,8 @@
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"lint-staged": "lint-staged"
"lint-staged": "lint-staged",
"inspect-local": "pnpm build && npx @modelcontextprotocol/inspector --config ./inspector.config.json --server local"
},
"dependencies": {
"@datadog/datadog-api-client": "^1.34.1",
Expand Down
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
}
})

if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) {
throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set')
if (
!process.env.DATADOG_API_KEY ||
!process.env.DATADOG_APP_KEY ||
!process.env.DATADOG_EVAL_TIMESTAMP
) {
throw new Error(
'[MCP Eval Version] DATADOG_API_KEY and DATADOG_APP_KEY and DATADOG_EVAL_TIMESTAMP must be set',
)
}

const datadogConfig = createDatadogConfig({
Expand Down
35 changes: 31 additions & 4 deletions src/tools/logs/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v2 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { GetLogsZodSchema, GetAllServicesZodSchema } from './schema'
import { adjustTimestamps } from '../../utils/adjustTimestamps'

type LogsToolName = 'get_logs' | 'get_all_services'
type LogsTool = ExtendedTool<LogsToolName>
Expand Down Expand Up @@ -29,13 +30,26 @@ export const createLogsToolHandlers = (
request.params.arguments,
)

const adjusted = adjustTimestamps(from, to)

if (!adjusted.ok) {
return {
content: [
{
type: 'text',
text: `Logs data: ${[]}`,
},
],
}
}

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}`,
from: `${adjusted.from * 1000}`,
to: `${adjusted.to * 1000}`,
},
page: {
limit,
Expand Down Expand Up @@ -63,13 +77,26 @@ export const createLogsToolHandlers = (
request.params.arguments,
)

const adjusted = adjustTimestamps(from, to)

if (!adjusted.ok) {
return {
content: [
{
type: 'text',
text: `Services: ${[]}`,
},
],
}
}

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}`,
from: `${adjusted.from * 1000}`,
to: `${adjusted.to * 1000}`,
},
page: {
limit,
Expand Down
18 changes: 16 additions & 2 deletions src/tools/metrics/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types'
import { v1 } from '@datadog/datadog-api-client'
import { createToolSchema } from '../../utils/tool'
import { QueryMetricsZodSchema } from './schema'
import { adjustTimestamps } from '../../utils/adjustTimestamps'

type MetricsToolName = 'query_metrics'
type MetricsTool = ExtendedTool<MetricsToolName>
Expand All @@ -25,9 +26,22 @@ export const createMetricsToolHandlers = (
request.params.arguments,
)

const adjusted = adjustTimestamps(from, to)

if (!adjusted.ok) {
return {
content: [
{
type: 'text',
text: `Metrics data: ${[]}`,
},
],
}
}

const response = await apiInstance.queryMetrics({
from,
to,
from: adjusted.from,
to: adjusted.to,
query,
})

Expand Down
49 changes: 43 additions & 6 deletions src/tools/rum/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GetRumPagePerformanceZodSchema,
GetRumPageWaterfallZodSchema,
} from './schema'
import { adjustTimestamps } from '../../utils/adjustTimestamps'

type RumToolName =
| 'get_rum_events'
Expand Down Expand Up @@ -74,10 +75,22 @@ export const createRumToolHandlers = (
request.params.arguments,
)

const adjusted = adjustTimestamps(from, to)
if (!adjusted.ok) {
return {
content: [
{
type: 'text',
text: `RUM events data: ${[]}`,
},
],
}
}

const response = await apiInstance.listRUMEvents({
filterQuery: query,
filterFrom: new Date(from * 1000),
filterTo: new Date(to * 1000),
filterFrom: new Date(adjusted.from * 1000),
filterTo: new Date(adjusted.to * 1000),
sort: 'timestamp',
pageLimit: limit,
})
Expand All @@ -101,11 +114,23 @@ export const createRumToolHandlers = (
request.params.arguments,
)

const adjusted = adjustTimestamps(from, to)
if (!adjusted.ok) {
return {
content: [
{
type: 'text',
text: `RUM grouped event count: ${[]}`,
},
],
}
}

// 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),
filterFrom: new Date(adjusted.from * 1000),
filterTo: new Date(adjusted.to * 1000),
sort: 'timestamp',
pageLimit: 2000,
})
Expand Down Expand Up @@ -163,13 +188,25 @@ export const createRumToolHandlers = (
const { query, from, to, metricNames } =
GetRumPagePerformanceZodSchema.parse(request.params.arguments)

const adjusted = adjustTimestamps(from, to)
if (!adjusted.ok) {
return {
content: [
{
type: 'text',
text: `Page performance metrics: ${[]}`,
},
],
}
}

// 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),
filterFrom: new Date(adjusted.from * 1000),
filterTo: new Date(adjusted.to * 1000),
sort: 'timestamp',
pageLimit: 2000,
})
Expand Down
42 changes: 42 additions & 0 deletions src/utils/adjustTimestamps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// adjustTimestamps set the timeframe to end at the evaluation timestamp. Return not OK if the resulting timeframe is invalid.
export const adjustTimestamps = (
from: number,
to: number,
): { ok: false } | { ok: true; from: number; to: number } => {
const evalTimestamp = process.env.DATADOG_EVAL_TIMESTAMP

if (evalTimestamp === undefined) {
throw new Error('[MCP Eval Version] DATADOG_EVAL_TIMESTAMP must be set')
}

const evalSecs = Math.floor(new Date(evalTimestamp).getTime() / 1000)

const adjustedTo = Math.min(to, evalSecs)

// Calculate the original interval
const interval = to - from

// Adjust 'from' to maintain the same interval, but ensure it's not negative
const adjustedFrom = Math.max(0, adjustedTo - interval)

// If the requested timeframe is entirely after the eval timestamp,
// adjust to show the last 'interval' seconds before eval timestamp
if (from > evalSecs) {
const cappedInterval = Math.min(interval, evalSecs)
return {
ok: true,
from: evalSecs - cappedInterval,
to: evalSecs,
}
}

if (adjustedTo <= adjustedFrom) {
return { ok: false }
}

return {
ok: true,
from: adjustedFrom,
to: adjustedTo,
}
}
1 change: 1 addition & 0 deletions tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { afterEach, vi } from 'vitest'
process.env.DATADOG_API_KEY = 'test-api-key'
process.env.DATADOG_APP_KEY = 'test-app-key'

process.env.DATADOG_EVAL_TIMESTAMP = new Date().toISOString()
// Reset handlers after each test
afterEach(() => {
// server.resetHandlers()
Expand Down