From 0807bc1c0b830467c70a0eb2bf812a865c9b86bb Mon Sep 17 00:00:00 2001 From: PChol22 Date: Mon, 21 Jul 2025 17:43:17 +0200 Subject: [PATCH 1/7] filter old logs for eval mcp --- package.json | 4 ++-- src/tools/logs/tool.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ddbee89..255e9f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@winor30/mcp-server-datadog", - "version": "1.6.0", + "name": "@anyshift/datadog-mcp-server", + "version": "0.0.1", "description": "MCP server for interacting with Datadog API", "repository": { "type": "git", diff --git a/src/tools/logs/tool.ts b/src/tools/logs/tool.ts index 322a7f7..83e2b6a 100644 --- a/src/tools/logs/tool.ts +++ b/src/tools/logs/tool.ts @@ -44,6 +44,15 @@ export const createLogsToolHandlers = ( }, }) + // only keep logs that are less than 30 minutes old + const thirtyMinutesAgo = Date.now() - 30 * 60 * 1000 + + const filteredData = response.data?.filter( + (log) => + log.attributes?.timestamp !== undefined && + log.attributes.timestamp.getTime() > thirtyMinutesAgo, + ) + if (response.data == null) { throw new Error('No logs data returned') } @@ -52,7 +61,7 @@ export const createLogsToolHandlers = ( content: [ { type: 'text', - text: `Logs data: ${JSON.stringify(response.data)}`, + text: `Logs data: ${JSON.stringify(filteredData)}`, }, ], } From 8216f353527c6e57e39e31bf1188295eb84be9a2 Mon Sep 17 00:00:00 2001 From: PChol22 Date: Mon, 21 Jul 2025 17:50:39 +0200 Subject: [PATCH 2/7] fix repo in packagejson --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 255e9f9..7a882e1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "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": { From b5cdb06c45270f3d41a0fa22bbb12473c20c5af2 Mon Sep 17 00:00:00 2001 From: PChol22 Date: Mon, 21 Jul 2025 18:20:19 +0200 Subject: [PATCH 3/7] fix: update 30 minutes filter --- inspector.config.json | 8 ++++++++ package.json | 3 ++- src/tools/logs/tool.ts | 20 ++++++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 inspector.config.json 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 7a882e1..590382f 100644 --- a/package.json +++ b/package.json @@ -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/tools/logs/tool.ts b/src/tools/logs/tool.ts index 83e2b6a..59f8652 100644 --- a/src/tools/logs/tool.ts +++ b/src/tools/logs/tool.ts @@ -29,12 +29,27 @@ export const createLogsToolHandlers = ( request.params.arguments, ) + // update from to be the max of from and 30 minutes ago + const thirtyMinutesAgo = Math.floor(Date.now() / 1000) - 30 * 60 + const adjustedFrom = Math.max(from, thirtyMinutesAgo) + + if (adjustedFrom > to) { + 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}`, + from: `${adjustedFrom * 1000}`, to: `${to * 1000}`, }, page: { @@ -44,9 +59,6 @@ export const createLogsToolHandlers = ( }, }) - // only keep logs that are less than 30 minutes old - const thirtyMinutesAgo = Date.now() - 30 * 60 * 1000 - const filteredData = response.data?.filter( (log) => log.attributes?.timestamp !== undefined && From 31705ccf6768fc9577a443cfd4ca94c32f39d85a Mon Sep 17 00:00:00 2001 From: PChol22 Date: Thu, 24 Jul 2025 12:08:36 +0200 Subject: [PATCH 4/7] feat: adjust MCP timestamps to only return data prior to incident --- src/index.ts | 10 +++++-- src/tools/logs/tool.ts | 36 ++++++++++++++----------- src/tools/metrics/tool.ts | 18 +++++++++++-- src/tools/rum/tool.ts | 49 ++++++++++++++++++++++++++++++----- src/utils/adjustTimestamps.ts | 25 ++++++++++++++++++ tests/setup.ts | 1 + 6 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 src/utils/adjustTimestamps.ts 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 59f8652..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,11 +30,9 @@ export const createLogsToolHandlers = ( request.params.arguments, ) - // update from to be the max of from and 30 minutes ago - const thirtyMinutesAgo = Math.floor(Date.now() / 1000) - 30 * 60 - const adjustedFrom = Math.max(from, thirtyMinutesAgo) + const adjusted = adjustTimestamps(from, to) - if (adjustedFrom > to) { + if (!adjusted.ok) { return { content: [ { @@ -49,8 +48,8 @@ export const createLogsToolHandlers = ( filter: { query, // `from` and `to` are in epoch seconds, but the Datadog API expects milliseconds - from: `${adjustedFrom * 1000}`, - to: `${to * 1000}`, + from: `${adjusted.from * 1000}`, + to: `${adjusted.to * 1000}`, }, page: { limit, @@ -59,12 +58,6 @@ export const createLogsToolHandlers = ( }, }) - const filteredData = response.data?.filter( - (log) => - log.attributes?.timestamp !== undefined && - log.attributes.timestamp.getTime() > thirtyMinutesAgo, - ) - if (response.data == null) { throw new Error('No logs data returned') } @@ -73,7 +66,7 @@ export const createLogsToolHandlers = ( content: [ { type: 'text', - text: `Logs data: ${JSON.stringify(filteredData)}`, + text: `Logs data: ${JSON.stringify(response.data)}`, }, ], } @@ -84,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..72df197 --- /dev/null +++ b/src/utils/adjustTimestamps.ts @@ -0,0 +1,25 @@ +// 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) + + if (adjustedTo < from) { + return { ok: false } + } + + return { + ok: true, + from, + 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() From 1d8d9ce3f70f3452cc250da30a30c0a223d54a5d Mon Sep 17 00:00:00 2001 From: PChol22 Date: Thu, 24 Jul 2025 12:10:55 +0200 Subject: [PATCH 5/7] v1.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 590382f..3660076 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@anyshift/datadog-mcp-server", - "version": "0.0.1", + "version": "1.7.4", "description": "MCP server for interacting with Datadog API", "repository": { "type": "git", From a508158c3d79e0fe681f2f32e5fc9865fe71ebed Mon Sep 17 00:00:00 2001 From: Julien Salomon Date: Wed, 17 Sep 2025 17:24:59 +0200 Subject: [PATCH 6/7] add anyshift ci --- .github/workflows/ci.yml | 103 ----------------------- .github/workflows/claude-code-review.yml | 53 ++++++++++++ .github/workflows/npm-publish.yml | 53 ++++++++++++ .github/workflows/pr-slack-notify.yml | 10 +++ .github/workflows/publish.yml | 38 --------- 5 files changed, 116 insertions(+), 141 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/npm-publish.yml create mode 100644 .github/workflows/pr-slack-notify.yml delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 623c8c5..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,103 +0,0 @@ -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 - with: - version: 10 - - - 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 - with: - version: 10 - - - 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 - with: - version: 10 - - - 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 - with: - version: 10 - - - 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 diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..1d9e920 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,53 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..5781f5c --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,53 @@ +name: Publish to NPM + +on: + push: + tags: + - 'v*.*.*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org/' + + - name: Extract version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Check if alpha release + id: check_prerelease + run: | + if [[ ${{ steps.get_version.outputs.VERSION }} == *"-alpha"* ]]; then + echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT + else + echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT + fi + + - name: Update version in package.json + run: npm version ${{ steps.get_version.outputs.VERSION }} --no-git-tag-version + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Publish to NPM (Alpha) + if: steps.check_prerelease.outputs.IS_PRERELEASE == 'true' + run: npm publish --tag alpha --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish to NPM (Release) + if: steps.check_prerelease.outputs.IS_PRERELEASE == 'false' + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/pr-slack-notify.yml b/.github/workflows/pr-slack-notify.yml new file mode 100644 index 0000000..5ccd8a4 --- /dev/null +++ b/.github/workflows/pr-slack-notify.yml @@ -0,0 +1,10 @@ +name: PR Review Notifications + +on: + pull_request: + types: [review_requested] + +jobs: + call-slack-notifier: + uses: anyshift-io/custom_github_actions/.github/workflows/slack-pr-reviewer-notify.yml@main + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index dede4d9..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,38 +0,0 @@ -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 - with: - version: 10 - - - 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 }} From 95de1fd44669c43542991fd2f5176e7dc596d55f Mon Sep 17 00:00:00 2001 From: Julien Salomon Date: Wed, 17 Sep 2025 17:26:22 +0200 Subject: [PATCH 7/7] readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4b7ff34..6b7a2c4 100644 --- a/README.md +++ b/README.md @@ -310,3 +310,5 @@ Contributions are welcome! Feel free to open an issue or a pull request if you h ## License This project is licensed under the [Apache License, Version 2.0](./LICENSE). + +blablabla \ No newline at end of file