datadog-mcp 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Datadog MCP Server
2
2
 
3
+ [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=TANTIOPE_datadog-mcp-server)](https://sonarcloud.io/summary/new_code?id=TANTIOPE_datadog-mcp-server)
3
4
  [![CI](https://github.com/tantiope/datadog-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/tantiope/datadog-mcp-server/actions/workflows/ci.yml)
4
5
  [![npm](https://img.shields.io/npm/v/datadog-mcp)](https://www.npmjs.com/package/datadog-mcp)
5
6
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
- [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=TANTIOPE_datadog-mcp-server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=TANTIOPE_datadog-mcp-server)
7
7
  [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=TANTIOPE_datadog-mcp-server&metric=coverage)](https://sonarcloud.io/summary/new_code?id=TANTIOPE_datadog-mcp-server)
8
8
 
9
9
  > **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.
@@ -80,6 +80,26 @@ DD_SITE=datadoghq.com # Default. Use datadoghq.eu for EU, etc.
80
80
  }
81
81
  ```
82
82
 
83
+ ### Kubernetes
84
+
85
+ **Use environment variables instead of container args:**
86
+
87
+ ```yaml
88
+ env:
89
+ - name: DD_API_KEY
90
+ value: "your-api-key"
91
+ - name: DD_APP_KEY
92
+ value: "your-app-key"
93
+ - name: MCP_TRANSPORT
94
+ value: "http"
95
+ - name: MCP_PORT
96
+ value: "3000"
97
+ - name: MCP_HOST
98
+ value: "0.0.0.0"
99
+ ```
100
+
101
+ > **Note:** Kubernetes `args:` replaces the entire Dockerfile CMD, causing Node.js to receive the flags instead of your application. Environment variables avoid this issue.
102
+
83
103
  ### HTTP Transport
84
104
 
85
105
  When running with `--transport=http`:
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ var configSchema = z.object({
35
35
  transport: z.enum(["stdio", "http"]).default("stdio"),
36
36
  port: z.number().default(3e3),
37
37
  host: z.string().default("localhost")
38
- }),
38
+ }).default({}),
39
39
  limits: z.object({
40
40
  maxResults: z.number().default(100),
41
41
  maxLogLines: z.number().default(100),
@@ -44,7 +44,7 @@ var configSchema = z.object({
44
44
  // Default limit for initial queries
45
45
  maxMetricDataPoints: z.number().default(1e3),
46
46
  defaultTimeRangeHours: z.number().default(24)
47
- }),
47
+ }).default({}),
48
48
  features: z.object({
49
49
  readOnly: z.boolean().default(false),
50
50
  disabledTools: z.array(z.string()).default([])
@@ -52,6 +52,22 @@ var configSchema = z.object({
52
52
  });
53
53
 
54
54
  // src/config/index.ts
55
+ function parseEqualsFormat(arg) {
56
+ if (!arg.includes("=")) return null;
57
+ const parts = arg.slice(2).split("=");
58
+ const key = parts[0];
59
+ const value = parts.slice(1).join("=");
60
+ return key && value !== void 0 ? [key, value] : null;
61
+ }
62
+ function parseSpacedFormat(arg, nextArg) {
63
+ if (nextArg && !nextArg.startsWith("--")) {
64
+ return [arg.slice(2), nextArg];
65
+ }
66
+ return null;
67
+ }
68
+ function parseBooleanFlag(arg) {
69
+ return arg.slice(2);
70
+ }
55
71
  function parseArgs() {
56
72
  const strings = {};
57
73
  const booleans = /* @__PURE__ */ new Set();
@@ -60,23 +76,20 @@ function parseArgs() {
60
76
  const arg = argv[i];
61
77
  if (!arg) continue;
62
78
  if (arg.startsWith("--")) {
63
- if (arg.includes("=")) {
64
- const parts = arg.slice(2).split("=");
65
- const key = parts[0];
66
- const value = parts.slice(1).join("=");
67
- if (key && value !== void 0) {
68
- strings[key] = value;
69
- }
70
- } else {
71
- const argName = arg.slice(2);
72
- const nextArg = argv[i + 1];
73
- if (nextArg && !nextArg.startsWith("--")) {
74
- strings[argName] = nextArg;
75
- i += 1;
76
- } else {
77
- booleans.add(argName);
78
- }
79
+ const equalsResult = parseEqualsFormat(arg);
80
+ if (equalsResult) {
81
+ const [key, value] = equalsResult;
82
+ strings[key] = value;
83
+ continue;
79
84
  }
85
+ const spacedResult = parseSpacedFormat(arg, argv[i + 1]);
86
+ if (spacedResult) {
87
+ const [key, value] = spacedResult;
88
+ strings[key] = value;
89
+ i += 1;
90
+ continue;
91
+ }
92
+ booleans.add(parseBooleanFlag(arg));
80
93
  }
81
94
  }
82
95
  return { strings, booleans };
@@ -261,7 +274,7 @@ function formatResponse(data) {
261
274
  return [
262
275
  {
263
276
  type: "text",
264
- text: JSON.stringify(data, null, 2)
277
+ text: JSON.stringify(data, null, 2) ?? "null"
265
278
  }
266
279
  ];
267
280
  }
@@ -1136,7 +1149,7 @@ TOKEN TIP: Use compact:true to reduce payload size (strips heavy fields) when qu
1136
1149
  );
1137
1150
  }
1138
1151
  case "aggregate": {
1139
- const aggregateQuery = requireParam(query, "query", "aggregate");
1152
+ const aggregateQuery = query ?? "*";
1140
1153
  return toolResult(
1141
1154
  await aggregateLogs(
1142
1155
  api,
@@ -1399,6 +1412,18 @@ function formatSpan(span) {
1399
1412
  tags
1400
1413
  };
1401
1414
  }
1415
+ function buildHttpStatusFilter(httpStatus) {
1416
+ const status = httpStatus.toLowerCase();
1417
+ if (status.endsWith("xx")) {
1418
+ const base = Number.parseInt(status[0] ?? "0", 10) * 100;
1419
+ return `@http.status_code:[${base} TO ${base + 99}]`;
1420
+ }
1421
+ if (status.startsWith(">=")) return `@http.status_code:>=${status.slice(2)}`;
1422
+ if (status.startsWith(">")) return `@http.status_code:>${status.slice(1)}`;
1423
+ if (status.startsWith("<=")) return `@http.status_code:<=${status.slice(2)}`;
1424
+ if (status.startsWith("<")) return `@http.status_code:<${status.slice(1)}`;
1425
+ return `@http.status_code:${httpStatus}`;
1426
+ }
1402
1427
  function buildTraceQuery(params) {
1403
1428
  const parts = [];
1404
1429
  if (params.query) {
@@ -1432,21 +1457,7 @@ function buildTraceQuery(params) {
1432
1457
  }
1433
1458
  }
1434
1459
  if (params.httpStatus) {
1435
- const status = params.httpStatus.toLowerCase();
1436
- if (status.endsWith("xx")) {
1437
- const base = Number.parseInt(status[0] ?? "0", 10) * 100;
1438
- parts.push(`@http.status_code:[${base} TO ${base + 99}]`);
1439
- } else if (status.startsWith(">=")) {
1440
- parts.push(`@http.status_code:>=${status.slice(2)}`);
1441
- } else if (status.startsWith(">")) {
1442
- parts.push(`@http.status_code:>${status.slice(1)}`);
1443
- } else if (status.startsWith("<=")) {
1444
- parts.push(`@http.status_code:<=${status.slice(2)}`);
1445
- } else if (status.startsWith("<")) {
1446
- parts.push(`@http.status_code:<${status.slice(1)}`);
1447
- } else {
1448
- parts.push(`@http.status_code:${params.httpStatus}`);
1449
- }
1460
+ parts.push(buildHttpStatusFilter(params.httpStatus));
1450
1461
  }
1451
1462
  if (params.errorType) {
1452
1463
  const escaped = params.errorType.replace(/"/g, '\\"');
@@ -1751,7 +1762,7 @@ function extractTitleFromMessage(message) {
1751
1762
  if (!message) return "";
1752
1763
  const content = message.replace(/^%%%\s*\n?/, "").trim();
1753
1764
  const firstLine = content.split("\n")[0]?.trim() ?? "";
1754
- return firstLine.replace(/\s+!?\s*$/, "").trim();
1765
+ return firstLine.replace(/\s*!?\s*$/, "").trim();
1755
1766
  }
1756
1767
  function extractMonitorIdFromMessage(message) {
1757
1768
  if (!message) return void 0;
@@ -2086,7 +2097,7 @@ async function timeseriesEventsV2(api, params, limits, site) {
2086
2097
  const fullQuery = buildEventQuery({
2087
2098
  query: params.query ?? "source:alert",
2088
2099
  sources: params.sources,
2089
- tags: params.tags ?? ["source:alert"]
2100
+ tags: params.tags
2090
2101
  });
2091
2102
  const intervalMs = parseIntervalToMs(params.interval);
2092
2103
  const groupByFields = params.groupBy ?? ["monitor_name"];
@@ -2171,7 +2182,7 @@ async function incidentsEventsV2(api, params, limits, site) {
2171
2182
  const fullQuery = buildEventQuery({
2172
2183
  query: params.query ?? "source:alert",
2173
2184
  sources: params.sources,
2174
- tags: params.tags ?? ["source:alert"]
2185
+ tags: params.tags
2175
2186
  });
2176
2187
  const dedupeWindowNs = parseDurationToNs(params.dedupeWindow ?? "5m");
2177
2188
  const dedupeWindowMs = dedupeWindowNs ? Math.floor(dedupeWindowNs / 1e6) : 3e5;
@@ -4804,8 +4815,9 @@ import { randomUUID } from "crypto";
4804
4815
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4805
4816
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4806
4817
  var transports = {};
4807
- async function connectHttp(server, config) {
4818
+ function createExpressApp(server, config) {
4808
4819
  const app = express();
4820
+ app.disable("x-powered-by");
4809
4821
  app.use(express.json());
4810
4822
  app.get("/health", (_req, res) => {
4811
4823
  res.json({ status: "ok", name: config.name, version: config.version });
@@ -4858,6 +4870,10 @@ async function connectHttp(server, config) {
4858
4870
  res.status(400).json({ error: "Invalid session" });
4859
4871
  }
4860
4872
  });
4873
+ return app;
4874
+ }
4875
+ async function connectHttp(server, config) {
4876
+ const app = createExpressApp(server, config);
4861
4877
  app.listen(config.port, config.host, () => {
4862
4878
  console.error(`[MCP] Datadog MCP server running on http://${config.host}:${config.port}/mcp`);
4863
4879
  console.error(`[MCP] Health check available at http://${config.host}:${config.port}/health`);