diff --git a/inspector.config.json b/inspector.config.json new file mode 100644 index 0000000..9a1590e --- /dev/null +++ b/inspector.config.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "local": { + "command": "node", + "args": ["./build/index.js"] + } + } +} diff --git a/package.json b/package.json index ddbee89..3660076 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", diff --git a/src/index.ts b/src/index.ts index 8d1dda0..5771780 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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({ diff --git a/src/tools/logs/tool.ts b/src/tools/logs/tool.ts index 322a7f7..bb138fa 100644 --- a/src/tools/logs/tool.ts +++ b/src/tools/logs/tool.ts @@ -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 @@ -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, @@ -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, diff --git a/src/tools/metrics/tool.ts b/src/tools/metrics/tool.ts index 401c585..919dd1f 100644 --- a/src/tools/metrics/tool.ts +++ b/src/tools/metrics/tool.ts @@ -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 @@ -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, }) diff --git a/src/tools/rum/tool.ts b/src/tools/rum/tool.ts index 6949029..d3d8def 100644 --- a/src/tools/rum/tool.ts +++ b/src/tools/rum/tool.ts @@ -8,6 +8,7 @@ import { GetRumPagePerformanceZodSchema, GetRumPageWaterfallZodSchema, } from './schema' +import { adjustTimestamps } from '../../utils/adjustTimestamps' type RumToolName = | 'get_rum_events' @@ -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, }) @@ -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, }) @@ -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, }) diff --git a/src/utils/adjustTimestamps.ts b/src/utils/adjustTimestamps.ts new file mode 100644 index 0000000..949fd16 --- /dev/null +++ b/src/utils/adjustTimestamps.ts @@ -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, + } +} diff --git a/tests/setup.ts b/tests/setup.ts index c833a08..0069835 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -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()