@jaypie/mcp 0.1.9 → 0.2.0
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/dist/datadog.d.ts +212 -0
- package/dist/index.js +1461 -6
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
- package/prompts/Development_Process.md +57 -35
- package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +143 -19
- package/prompts/Jaypie_Express_Package.md +408 -0
- package/prompts/Jaypie_Init_Express_on_Lambda.md +66 -38
- package/prompts/Jaypie_Init_Lambda_Package.md +202 -83
- package/prompts/Jaypie_Init_Project_Subpackage.md +21 -26
- package/prompts/Jaypie_Legacy_Patterns.md +4 -0
- package/prompts/Templates_CDK_Subpackage.md +113 -0
- package/prompts/Templates_Express_Subpackage.md +183 -0
- package/prompts/Templates_Project_Monorepo.md +326 -0
- package/prompts/Templates_Project_Subpackage.md +93 -0
- package/LICENSE.txt +0 -21
- package/prompts/Jaypie_Mongoose_Models_Package.md +0 -231
- package/prompts/Jaypie_Mongoose_with_Express_CRUD.md +0 -1000
- package/prompts/templates/cdk-subpackage/bin/cdk.ts +0 -11
- package/prompts/templates/cdk-subpackage/cdk.json +0 -19
- package/prompts/templates/cdk-subpackage/lib/cdk-app.ts +0 -41
- package/prompts/templates/cdk-subpackage/lib/cdk-infrastructure.ts +0 -15
- package/prompts/templates/express-subpackage/index.ts +0 -8
- package/prompts/templates/express-subpackage/src/app.ts +0 -18
- package/prompts/templates/express-subpackage/src/handler.config.ts +0 -44
- package/prompts/templates/express-subpackage/src/routes/resource/__tests__/resourceGet.route.spec.ts +0 -29
- package/prompts/templates/express-subpackage/src/routes/resource/resourceGet.route.ts +0 -22
- package/prompts/templates/express-subpackage/src/routes/resource.router.ts +0 -11
- package/prompts/templates/express-subpackage/src/types/express.ts +0 -9
- package/prompts/templates/project-monorepo/.vscode/settings.json +0 -72
- package/prompts/templates/project-monorepo/eslint.config.mjs +0 -1
- package/prompts/templates/project-monorepo/gitignore +0 -11
- package/prompts/templates/project-monorepo/package.json +0 -20
- package/prompts/templates/project-monorepo/tsconfig.base.json +0 -18
- package/prompts/templates/project-monorepo/tsconfig.json +0 -6
- package/prompts/templates/project-monorepo/vitest.workspace.js +0 -3
- package/prompts/templates/project-subpackage/package.json +0 -16
- package/prompts/templates/project-subpackage/tsconfig.json +0 -11
- package/prompts/templates/project-subpackage/vite.config.ts +0 -21
- package/prompts/templates/project-subpackage/vitest.config.ts +0 -7
- package/prompts/templates/project-subpackage/vitest.setup.ts +0 -6
package/dist/index.js
CHANGED
|
@@ -7,12 +7,801 @@ import { z } from 'zod';
|
|
|
7
7
|
import * as fs from 'node:fs/promises';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
|
+
import * as https from 'node:https';
|
|
10
11
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
12
|
import { randomUUID } from 'node:crypto';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Datadog API integration module
|
|
16
|
+
*/
|
|
17
|
+
const nullLogger = {
|
|
18
|
+
info: () => { },
|
|
19
|
+
error: () => { },
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Get Datadog credentials from environment variables
|
|
23
|
+
*/
|
|
24
|
+
function getDatadogCredentials() {
|
|
25
|
+
const apiKey = process.env.DATADOG_API_KEY || process.env.DD_API_KEY;
|
|
26
|
+
const appKey = process.env.DATADOG_APP_KEY ||
|
|
27
|
+
process.env.DATADOG_APPLICATION_KEY ||
|
|
28
|
+
process.env.DD_APP_KEY ||
|
|
29
|
+
process.env.DD_APPLICATION_KEY;
|
|
30
|
+
if (!apiKey || !appKey) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { apiKey, appKey };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build query string from environment variables and options
|
|
37
|
+
*/
|
|
38
|
+
function buildDatadogQuery(options) {
|
|
39
|
+
const ddEnv = process.env.DD_ENV;
|
|
40
|
+
const ddService = process.env.DD_SERVICE;
|
|
41
|
+
const ddSource = process.env.DD_SOURCE;
|
|
42
|
+
const ddQuery = process.env.DD_QUERY;
|
|
43
|
+
const queryParts = [];
|
|
44
|
+
// Add source (parameter > env var > default 'lambda')
|
|
45
|
+
const effectiveSource = options.source || ddSource || "lambda";
|
|
46
|
+
queryParts.push(`source:${effectiveSource}`);
|
|
47
|
+
// Add env (parameter > env var)
|
|
48
|
+
const effectiveEnv = options.env || ddEnv;
|
|
49
|
+
if (effectiveEnv) {
|
|
50
|
+
queryParts.push(`env:${effectiveEnv}`);
|
|
51
|
+
}
|
|
52
|
+
// Add service (parameter > env var)
|
|
53
|
+
const effectiveService = options.service || ddService;
|
|
54
|
+
if (effectiveService) {
|
|
55
|
+
queryParts.push(`service:${effectiveService}`);
|
|
56
|
+
}
|
|
57
|
+
// Add base query from DD_QUERY if available
|
|
58
|
+
if (ddQuery) {
|
|
59
|
+
queryParts.push(ddQuery);
|
|
60
|
+
}
|
|
61
|
+
// Add user-provided query terms
|
|
62
|
+
if (options.query) {
|
|
63
|
+
queryParts.push(options.query);
|
|
64
|
+
}
|
|
65
|
+
return queryParts.join(" ");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Search Datadog logs
|
|
69
|
+
*/
|
|
70
|
+
async function searchDatadogLogs(credentials, options = {}, logger = nullLogger) {
|
|
71
|
+
const effectiveQuery = buildDatadogQuery(options);
|
|
72
|
+
const effectiveFrom = options.from || "now-15m";
|
|
73
|
+
const effectiveTo = options.to || "now";
|
|
74
|
+
const effectiveLimit = Math.min(options.limit || 50, 1000);
|
|
75
|
+
const effectiveSort = options.sort || "-timestamp";
|
|
76
|
+
logger.info(`Effective query: ${effectiveQuery}`);
|
|
77
|
+
logger.info(`Search params: from=${effectiveFrom}, to=${effectiveTo}, limit=${effectiveLimit}, sort=${effectiveSort}`);
|
|
78
|
+
const requestBody = JSON.stringify({
|
|
79
|
+
filter: {
|
|
80
|
+
query: effectiveQuery,
|
|
81
|
+
from: effectiveFrom,
|
|
82
|
+
to: effectiveTo,
|
|
83
|
+
},
|
|
84
|
+
page: {
|
|
85
|
+
limit: effectiveLimit,
|
|
86
|
+
},
|
|
87
|
+
sort: effectiveSort,
|
|
88
|
+
});
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const requestOptions = {
|
|
91
|
+
hostname: "api.datadoghq.com",
|
|
92
|
+
port: 443,
|
|
93
|
+
path: "/api/v2/logs/events/search",
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
"DD-API-KEY": credentials.apiKey,
|
|
98
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
99
|
+
"Content-Length": Buffer.byteLength(requestBody),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const req = https.request(requestOptions, (res) => {
|
|
103
|
+
let data = "";
|
|
104
|
+
res.on("data", (chunk) => {
|
|
105
|
+
data += chunk.toString();
|
|
106
|
+
});
|
|
107
|
+
res.on("end", () => {
|
|
108
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
109
|
+
if (res.statusCode !== 200) {
|
|
110
|
+
logger.error(`Datadog API error: ${res.statusCode}`);
|
|
111
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
112
|
+
if (res.statusCode === 400) {
|
|
113
|
+
errorMessage = `Invalid query syntax. Check your query: "${effectiveQuery}". Datadog error: ${data}`;
|
|
114
|
+
}
|
|
115
|
+
else if (res.statusCode === 403) {
|
|
116
|
+
errorMessage =
|
|
117
|
+
"Access denied. Verify your API and Application keys have logs_read permission.";
|
|
118
|
+
}
|
|
119
|
+
else if (res.statusCode === 429) {
|
|
120
|
+
errorMessage =
|
|
121
|
+
"Rate limited by Datadog. Wait a moment and try again, or reduce your query scope.";
|
|
122
|
+
}
|
|
123
|
+
resolve({
|
|
124
|
+
success: false,
|
|
125
|
+
query: effectiveQuery,
|
|
126
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
127
|
+
logs: [],
|
|
128
|
+
error: errorMessage,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const response = JSON.parse(data);
|
|
134
|
+
const logs = (response.data || []).map((log) => {
|
|
135
|
+
const attrs = log.attributes || {};
|
|
136
|
+
return {
|
|
137
|
+
id: log.id,
|
|
138
|
+
timestamp: attrs.timestamp,
|
|
139
|
+
status: attrs.status,
|
|
140
|
+
service: attrs.service,
|
|
141
|
+
message: attrs.message,
|
|
142
|
+
attributes: attrs.attributes,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
logger.info(`Retrieved ${logs.length} log entries`);
|
|
146
|
+
resolve({
|
|
147
|
+
success: true,
|
|
148
|
+
query: effectiveQuery,
|
|
149
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
150
|
+
logs,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (parseError) {
|
|
154
|
+
logger.error("Failed to parse Datadog response:", parseError);
|
|
155
|
+
resolve({
|
|
156
|
+
success: false,
|
|
157
|
+
query: effectiveQuery,
|
|
158
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
159
|
+
logs: [],
|
|
160
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
req.on("error", (error) => {
|
|
166
|
+
logger.error("Request error:", error);
|
|
167
|
+
resolve({
|
|
168
|
+
success: false,
|
|
169
|
+
query: effectiveQuery,
|
|
170
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
171
|
+
logs: [],
|
|
172
|
+
error: `Connection error: ${error.message}`,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
req.write(requestBody);
|
|
176
|
+
req.end();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Aggregate Datadog logs using the Analytics API
|
|
181
|
+
* Groups logs by specified fields and computes aggregations
|
|
182
|
+
*/
|
|
183
|
+
async function aggregateDatadogLogs(credentials, options, logger = nullLogger) {
|
|
184
|
+
const effectiveQuery = buildDatadogQuery(options);
|
|
185
|
+
const effectiveFrom = options.from || "now-15m";
|
|
186
|
+
const effectiveTo = options.to || "now";
|
|
187
|
+
const groupBy = options.groupBy;
|
|
188
|
+
const compute = options.compute || [{ aggregation: "count" }];
|
|
189
|
+
logger.info(`Analytics query: ${effectiveQuery}`);
|
|
190
|
+
logger.info(`Group by: ${groupBy.join(", ")}`);
|
|
191
|
+
logger.info(`Time range: ${effectiveFrom} to ${effectiveTo}`);
|
|
192
|
+
// Build compute array - each item needs aggregation and type
|
|
193
|
+
const computeItems = compute.map((c) => {
|
|
194
|
+
const item = {
|
|
195
|
+
aggregation: c.aggregation,
|
|
196
|
+
type: "total",
|
|
197
|
+
};
|
|
198
|
+
if (c.metric) {
|
|
199
|
+
item.metric = c.metric;
|
|
200
|
+
}
|
|
201
|
+
return item;
|
|
202
|
+
});
|
|
203
|
+
// Build group_by with proper sort configuration
|
|
204
|
+
const groupByItems = groupBy.map((field) => {
|
|
205
|
+
const item = {
|
|
206
|
+
facet: field,
|
|
207
|
+
limit: 100,
|
|
208
|
+
sort: {
|
|
209
|
+
type: "measure",
|
|
210
|
+
order: "desc",
|
|
211
|
+
aggregation: compute[0]?.aggregation || "count",
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
return item;
|
|
215
|
+
});
|
|
216
|
+
const requestBody = JSON.stringify({
|
|
217
|
+
filter: {
|
|
218
|
+
query: effectiveQuery,
|
|
219
|
+
from: effectiveFrom,
|
|
220
|
+
to: effectiveTo,
|
|
221
|
+
},
|
|
222
|
+
group_by: groupByItems,
|
|
223
|
+
compute: computeItems,
|
|
224
|
+
page: {
|
|
225
|
+
limit: 100,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
return new Promise((resolve) => {
|
|
229
|
+
const requestOptions = {
|
|
230
|
+
hostname: "api.datadoghq.com",
|
|
231
|
+
port: 443,
|
|
232
|
+
path: "/api/v2/logs/analytics/aggregate",
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: {
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
"DD-API-KEY": credentials.apiKey,
|
|
237
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
238
|
+
"Content-Length": Buffer.byteLength(requestBody),
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const req = https.request(requestOptions, (res) => {
|
|
242
|
+
let data = "";
|
|
243
|
+
res.on("data", (chunk) => {
|
|
244
|
+
data += chunk.toString();
|
|
245
|
+
});
|
|
246
|
+
res.on("end", () => {
|
|
247
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
248
|
+
if (res.statusCode !== 200) {
|
|
249
|
+
logger.error(`Datadog Analytics API error: ${res.statusCode}`);
|
|
250
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
251
|
+
if (res.statusCode === 400) {
|
|
252
|
+
errorMessage = `Invalid query or groupBy fields. Verify facet names exist: ${groupBy.join(", ")}. Datadog error: ${data}`;
|
|
253
|
+
}
|
|
254
|
+
else if (res.statusCode === 403) {
|
|
255
|
+
errorMessage =
|
|
256
|
+
"Access denied. Verify your API and Application keys have logs_read permission.";
|
|
257
|
+
}
|
|
258
|
+
else if (res.statusCode === 429) {
|
|
259
|
+
errorMessage =
|
|
260
|
+
"Rate limited by Datadog. Wait a moment and try again, or reduce your query scope.";
|
|
261
|
+
}
|
|
262
|
+
resolve({
|
|
263
|
+
success: false,
|
|
264
|
+
query: effectiveQuery,
|
|
265
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
266
|
+
groupBy,
|
|
267
|
+
buckets: [],
|
|
268
|
+
error: errorMessage,
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const response = JSON.parse(data);
|
|
274
|
+
const buckets = (response.data?.buckets || []).map((bucket) => ({
|
|
275
|
+
by: bucket.by || {},
|
|
276
|
+
computes: bucket.computes || {},
|
|
277
|
+
}));
|
|
278
|
+
logger.info(`Retrieved ${buckets.length} aggregation buckets`);
|
|
279
|
+
resolve({
|
|
280
|
+
success: true,
|
|
281
|
+
query: effectiveQuery,
|
|
282
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
283
|
+
groupBy,
|
|
284
|
+
buckets,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (parseError) {
|
|
288
|
+
logger.error("Failed to parse Datadog analytics response:", parseError);
|
|
289
|
+
resolve({
|
|
290
|
+
success: false,
|
|
291
|
+
query: effectiveQuery,
|
|
292
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
293
|
+
groupBy,
|
|
294
|
+
buckets: [],
|
|
295
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
req.on("error", (error) => {
|
|
301
|
+
logger.error("Request error:", error);
|
|
302
|
+
resolve({
|
|
303
|
+
success: false,
|
|
304
|
+
query: effectiveQuery,
|
|
305
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
306
|
+
groupBy,
|
|
307
|
+
buckets: [],
|
|
308
|
+
error: `Connection error: ${error.message}`,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
req.write(requestBody);
|
|
312
|
+
req.end();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* List Datadog monitors with optional filtering
|
|
317
|
+
*/
|
|
318
|
+
async function listDatadogMonitors(credentials, options = {}, logger = nullLogger) {
|
|
319
|
+
logger.info("Fetching Datadog monitors");
|
|
320
|
+
const queryParams = new URLSearchParams();
|
|
321
|
+
if (options.tags && options.tags.length > 0) {
|
|
322
|
+
queryParams.set("tags", options.tags.join(","));
|
|
323
|
+
}
|
|
324
|
+
if (options.monitorTags && options.monitorTags.length > 0) {
|
|
325
|
+
queryParams.set("monitor_tags", options.monitorTags.join(","));
|
|
326
|
+
}
|
|
327
|
+
if (options.name) {
|
|
328
|
+
queryParams.set("name", options.name);
|
|
329
|
+
}
|
|
330
|
+
const queryString = queryParams.toString();
|
|
331
|
+
const path = `/api/v1/monitor${queryString ? `?${queryString}` : ""}`;
|
|
332
|
+
logger.info(`Request path: ${path}`);
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
const requestOptions = {
|
|
335
|
+
hostname: "api.datadoghq.com",
|
|
336
|
+
port: 443,
|
|
337
|
+
path,
|
|
338
|
+
method: "GET",
|
|
339
|
+
headers: {
|
|
340
|
+
"DD-API-KEY": credentials.apiKey,
|
|
341
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
const req = https.request(requestOptions, (res) => {
|
|
345
|
+
let data = "";
|
|
346
|
+
res.on("data", (chunk) => {
|
|
347
|
+
data += chunk.toString();
|
|
348
|
+
});
|
|
349
|
+
res.on("end", () => {
|
|
350
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
351
|
+
if (res.statusCode !== 200) {
|
|
352
|
+
logger.error(`Datadog Monitors API error: ${res.statusCode}`);
|
|
353
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
354
|
+
if (res.statusCode === 403) {
|
|
355
|
+
errorMessage =
|
|
356
|
+
"Access denied. Verify your API and Application keys have monitors_read permission.";
|
|
357
|
+
}
|
|
358
|
+
else if (res.statusCode === 429) {
|
|
359
|
+
errorMessage =
|
|
360
|
+
"Rate limited by Datadog. Wait a moment and try again.";
|
|
361
|
+
}
|
|
362
|
+
resolve({
|
|
363
|
+
success: false,
|
|
364
|
+
monitors: [],
|
|
365
|
+
error: errorMessage,
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const response = JSON.parse(data);
|
|
371
|
+
let monitors = response.map((monitor) => ({
|
|
372
|
+
id: monitor.id,
|
|
373
|
+
name: monitor.name,
|
|
374
|
+
type: monitor.type,
|
|
375
|
+
status: monitor.overall_state || "Unknown",
|
|
376
|
+
message: monitor.message,
|
|
377
|
+
tags: monitor.tags || [],
|
|
378
|
+
priority: monitor.priority,
|
|
379
|
+
query: monitor.query,
|
|
380
|
+
overallState: monitor.overall_state,
|
|
381
|
+
}));
|
|
382
|
+
// Filter by status if specified
|
|
383
|
+
if (options.status && options.status.length > 0) {
|
|
384
|
+
monitors = monitors.filter((m) => options.status.includes(m.status));
|
|
385
|
+
}
|
|
386
|
+
logger.info(`Retrieved ${monitors.length} monitors`);
|
|
387
|
+
resolve({
|
|
388
|
+
success: true,
|
|
389
|
+
monitors,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch (parseError) {
|
|
393
|
+
logger.error("Failed to parse Datadog monitors response:", parseError);
|
|
394
|
+
resolve({
|
|
395
|
+
success: false,
|
|
396
|
+
monitors: [],
|
|
397
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
req.on("error", (error) => {
|
|
403
|
+
logger.error("Request error:", error);
|
|
404
|
+
resolve({
|
|
405
|
+
success: false,
|
|
406
|
+
monitors: [],
|
|
407
|
+
error: `Connection error: ${error.message}`,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
req.end();
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* List Datadog Synthetic tests
|
|
415
|
+
*/
|
|
416
|
+
async function listDatadogSynthetics(credentials, options = {}, logger = nullLogger) {
|
|
417
|
+
logger.info("Fetching Datadog Synthetic tests");
|
|
418
|
+
return new Promise((resolve) => {
|
|
419
|
+
const requestOptions = {
|
|
420
|
+
hostname: "api.datadoghq.com",
|
|
421
|
+
port: 443,
|
|
422
|
+
path: "/api/v1/synthetics/tests",
|
|
423
|
+
method: "GET",
|
|
424
|
+
headers: {
|
|
425
|
+
"DD-API-KEY": credentials.apiKey,
|
|
426
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
const req = https.request(requestOptions, (res) => {
|
|
430
|
+
let data = "";
|
|
431
|
+
res.on("data", (chunk) => {
|
|
432
|
+
data += chunk.toString();
|
|
433
|
+
});
|
|
434
|
+
res.on("end", () => {
|
|
435
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
436
|
+
if (res.statusCode !== 200) {
|
|
437
|
+
logger.error(`Datadog Synthetics API error: ${res.statusCode}`);
|
|
438
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
439
|
+
if (res.statusCode === 403) {
|
|
440
|
+
errorMessage =
|
|
441
|
+
"Access denied. Verify your API and Application keys have synthetics_read permission.";
|
|
442
|
+
}
|
|
443
|
+
else if (res.statusCode === 429) {
|
|
444
|
+
errorMessage =
|
|
445
|
+
"Rate limited by Datadog. Wait a moment and try again.";
|
|
446
|
+
}
|
|
447
|
+
resolve({
|
|
448
|
+
success: false,
|
|
449
|
+
tests: [],
|
|
450
|
+
error: errorMessage,
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const response = JSON.parse(data);
|
|
456
|
+
let tests = (response.tests || []).map((test) => ({
|
|
457
|
+
publicId: test.public_id,
|
|
458
|
+
name: test.name,
|
|
459
|
+
type: test.type,
|
|
460
|
+
status: test.status,
|
|
461
|
+
tags: test.tags || [],
|
|
462
|
+
locations: test.locations || [],
|
|
463
|
+
message: test.message,
|
|
464
|
+
}));
|
|
465
|
+
// Filter by type if specified
|
|
466
|
+
if (options.type) {
|
|
467
|
+
tests = tests.filter((t) => t.type === options.type);
|
|
468
|
+
}
|
|
469
|
+
// Filter by tags if specified
|
|
470
|
+
if (options.tags && options.tags.length > 0) {
|
|
471
|
+
tests = tests.filter((t) => options.tags.some((tag) => t.tags.includes(tag)));
|
|
472
|
+
}
|
|
473
|
+
logger.info(`Retrieved ${tests.length} synthetic tests`);
|
|
474
|
+
resolve({
|
|
475
|
+
success: true,
|
|
476
|
+
tests,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
catch (parseError) {
|
|
480
|
+
logger.error("Failed to parse Datadog synthetics response:", parseError);
|
|
481
|
+
resolve({
|
|
482
|
+
success: false,
|
|
483
|
+
tests: [],
|
|
484
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
req.on("error", (error) => {
|
|
490
|
+
logger.error("Request error:", error);
|
|
491
|
+
resolve({
|
|
492
|
+
success: false,
|
|
493
|
+
tests: [],
|
|
494
|
+
error: `Connection error: ${error.message}`,
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
req.end();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get recent results for a specific Synthetic test
|
|
502
|
+
*/
|
|
503
|
+
async function getDatadogSyntheticResults(credentials, publicId, logger = nullLogger) {
|
|
504
|
+
logger.info(`Fetching results for Synthetic test: ${publicId}`);
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
const requestOptions = {
|
|
507
|
+
hostname: "api.datadoghq.com",
|
|
508
|
+
port: 443,
|
|
509
|
+
path: `/api/v1/synthetics/tests/${publicId}/results`,
|
|
510
|
+
method: "GET",
|
|
511
|
+
headers: {
|
|
512
|
+
"DD-API-KEY": credentials.apiKey,
|
|
513
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
const req = https.request(requestOptions, (res) => {
|
|
517
|
+
let data = "";
|
|
518
|
+
res.on("data", (chunk) => {
|
|
519
|
+
data += chunk.toString();
|
|
520
|
+
});
|
|
521
|
+
res.on("end", () => {
|
|
522
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
523
|
+
if (res.statusCode !== 200) {
|
|
524
|
+
logger.error(`Datadog Synthetics Results API error: ${res.statusCode}`);
|
|
525
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
526
|
+
if (res.statusCode === 403) {
|
|
527
|
+
errorMessage =
|
|
528
|
+
"Access denied. Verify your API and Application keys have synthetics_read permission.";
|
|
529
|
+
}
|
|
530
|
+
else if (res.statusCode === 404) {
|
|
531
|
+
errorMessage = `Synthetic test '${publicId}' not found. Use datadog_synthetics (without testId) to list available tests.`;
|
|
532
|
+
}
|
|
533
|
+
else if (res.statusCode === 429) {
|
|
534
|
+
errorMessage =
|
|
535
|
+
"Rate limited by Datadog. Wait a moment and try again.";
|
|
536
|
+
}
|
|
537
|
+
resolve({
|
|
538
|
+
success: false,
|
|
539
|
+
publicId,
|
|
540
|
+
results: [],
|
|
541
|
+
error: errorMessage,
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const response = JSON.parse(data);
|
|
547
|
+
const results = (response.results || []).map((result) => ({
|
|
548
|
+
publicId,
|
|
549
|
+
resultId: result.result_id,
|
|
550
|
+
status: result.status,
|
|
551
|
+
checkTime: result.check_time,
|
|
552
|
+
passed: result.result?.passed ?? result.status === 0,
|
|
553
|
+
location: result.dc_id?.toString(),
|
|
554
|
+
}));
|
|
555
|
+
logger.info(`Retrieved ${results.length} synthetic results`);
|
|
556
|
+
resolve({
|
|
557
|
+
success: true,
|
|
558
|
+
publicId,
|
|
559
|
+
results,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
catch (parseError) {
|
|
563
|
+
logger.error("Failed to parse Datadog synthetic results:", parseError);
|
|
564
|
+
resolve({
|
|
565
|
+
success: false,
|
|
566
|
+
publicId,
|
|
567
|
+
results: [],
|
|
568
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
req.on("error", (error) => {
|
|
574
|
+
logger.error("Request error:", error);
|
|
575
|
+
resolve({
|
|
576
|
+
success: false,
|
|
577
|
+
publicId,
|
|
578
|
+
results: [],
|
|
579
|
+
error: `Connection error: ${error.message}`,
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
req.end();
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Query Datadog metrics
|
|
587
|
+
*/
|
|
588
|
+
async function queryDatadogMetrics(credentials, options, logger = nullLogger) {
|
|
589
|
+
logger.info(`Querying metrics: ${options.query}`);
|
|
590
|
+
logger.info(`Time range: ${options.from} to ${options.to}`);
|
|
591
|
+
const queryParams = new URLSearchParams({
|
|
592
|
+
query: options.query,
|
|
593
|
+
from: options.from.toString(),
|
|
594
|
+
to: options.to.toString(),
|
|
595
|
+
});
|
|
596
|
+
return new Promise((resolve) => {
|
|
597
|
+
const requestOptions = {
|
|
598
|
+
hostname: "api.datadoghq.com",
|
|
599
|
+
port: 443,
|
|
600
|
+
path: `/api/v1/query?${queryParams.toString()}`,
|
|
601
|
+
method: "GET",
|
|
602
|
+
headers: {
|
|
603
|
+
"DD-API-KEY": credentials.apiKey,
|
|
604
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
const req = https.request(requestOptions, (res) => {
|
|
608
|
+
let data = "";
|
|
609
|
+
res.on("data", (chunk) => {
|
|
610
|
+
data += chunk.toString();
|
|
611
|
+
});
|
|
612
|
+
res.on("end", () => {
|
|
613
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
614
|
+
if (res.statusCode !== 200) {
|
|
615
|
+
logger.error(`Datadog Metrics API error: ${res.statusCode}`);
|
|
616
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
617
|
+
if (res.statusCode === 400) {
|
|
618
|
+
errorMessage = `Invalid metric query. Check format: 'aggregation:metric.name{tags}'. Query: "${options.query}". Datadog error: ${data}`;
|
|
619
|
+
}
|
|
620
|
+
else if (res.statusCode === 403) {
|
|
621
|
+
errorMessage =
|
|
622
|
+
"Access denied. Verify your API and Application keys have metrics_read permission.";
|
|
623
|
+
}
|
|
624
|
+
else if (res.statusCode === 429) {
|
|
625
|
+
errorMessage =
|
|
626
|
+
"Rate limited by Datadog. Wait a moment and try again, or reduce your time range.";
|
|
627
|
+
}
|
|
628
|
+
resolve({
|
|
629
|
+
success: false,
|
|
630
|
+
query: options.query,
|
|
631
|
+
timeRange: { from: options.from, to: options.to },
|
|
632
|
+
series: [],
|
|
633
|
+
error: errorMessage,
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const response = JSON.parse(data);
|
|
639
|
+
const series = (response.series || []).map((s) => ({
|
|
640
|
+
metric: s.metric,
|
|
641
|
+
scope: s.scope,
|
|
642
|
+
pointlist: s.pointlist,
|
|
643
|
+
unit: s.unit?.[0]?.name,
|
|
644
|
+
}));
|
|
645
|
+
logger.info(`Retrieved ${series.length} metric series`);
|
|
646
|
+
resolve({
|
|
647
|
+
success: true,
|
|
648
|
+
query: options.query,
|
|
649
|
+
timeRange: { from: options.from, to: options.to },
|
|
650
|
+
series,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (parseError) {
|
|
654
|
+
logger.error("Failed to parse Datadog metrics response:", parseError);
|
|
655
|
+
resolve({
|
|
656
|
+
success: false,
|
|
657
|
+
query: options.query,
|
|
658
|
+
timeRange: { from: options.from, to: options.to },
|
|
659
|
+
series: [],
|
|
660
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
req.on("error", (error) => {
|
|
666
|
+
logger.error("Request error:", error);
|
|
667
|
+
resolve({
|
|
668
|
+
success: false,
|
|
669
|
+
query: options.query,
|
|
670
|
+
timeRange: { from: options.from, to: options.to },
|
|
671
|
+
series: [],
|
|
672
|
+
error: `Connection error: ${error.message}`,
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
req.end();
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Search Datadog RUM events
|
|
680
|
+
*/
|
|
681
|
+
async function searchDatadogRum(credentials, options = {}, logger = nullLogger) {
|
|
682
|
+
const effectiveQuery = options.query || "*";
|
|
683
|
+
const effectiveFrom = options.from || "now-15m";
|
|
684
|
+
const effectiveTo = options.to || "now";
|
|
685
|
+
const effectiveLimit = Math.min(options.limit || 50, 1000);
|
|
686
|
+
const effectiveSort = options.sort || "-timestamp";
|
|
687
|
+
logger.info(`RUM query: ${effectiveQuery}`);
|
|
688
|
+
logger.info(`Time range: ${effectiveFrom} to ${effectiveTo}`);
|
|
689
|
+
const requestBody = JSON.stringify({
|
|
690
|
+
filter: {
|
|
691
|
+
query: effectiveQuery,
|
|
692
|
+
from: effectiveFrom,
|
|
693
|
+
to: effectiveTo,
|
|
694
|
+
},
|
|
695
|
+
page: {
|
|
696
|
+
limit: effectiveLimit,
|
|
697
|
+
},
|
|
698
|
+
sort: effectiveSort,
|
|
699
|
+
});
|
|
700
|
+
return new Promise((resolve) => {
|
|
701
|
+
const requestOptions = {
|
|
702
|
+
hostname: "api.datadoghq.com",
|
|
703
|
+
port: 443,
|
|
704
|
+
path: "/api/v2/rum/events/search",
|
|
705
|
+
method: "POST",
|
|
706
|
+
headers: {
|
|
707
|
+
"Content-Type": "application/json",
|
|
708
|
+
"DD-API-KEY": credentials.apiKey,
|
|
709
|
+
"DD-APPLICATION-KEY": credentials.appKey,
|
|
710
|
+
"Content-Length": Buffer.byteLength(requestBody),
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
const req = https.request(requestOptions, (res) => {
|
|
714
|
+
let data = "";
|
|
715
|
+
res.on("data", (chunk) => {
|
|
716
|
+
data += chunk.toString();
|
|
717
|
+
});
|
|
718
|
+
res.on("end", () => {
|
|
719
|
+
logger.info(`Response status: ${res.statusCode}`);
|
|
720
|
+
if (res.statusCode !== 200) {
|
|
721
|
+
logger.error(`Datadog RUM API error: ${res.statusCode}`);
|
|
722
|
+
let errorMessage = `Datadog API returned status ${res.statusCode}: ${data}`;
|
|
723
|
+
// Check for specific "No valid indexes" error which means no RUM app is configured
|
|
724
|
+
if (data.includes("No valid indexes")) {
|
|
725
|
+
errorMessage =
|
|
726
|
+
"No RUM application found. Ensure you have a RUM application configured in Datadog and it has collected data. " +
|
|
727
|
+
"You can create a RUM application at https://app.datadoghq.com/rum/list";
|
|
728
|
+
}
|
|
729
|
+
else if (res.statusCode === 400) {
|
|
730
|
+
errorMessage = `Invalid RUM query. Check syntax: "${effectiveQuery}". Datadog error: ${data}`;
|
|
731
|
+
}
|
|
732
|
+
else if (res.statusCode === 403) {
|
|
733
|
+
errorMessage =
|
|
734
|
+
"Access denied. Verify your API and Application keys have rum_read permission.";
|
|
735
|
+
}
|
|
736
|
+
else if (res.statusCode === 429) {
|
|
737
|
+
errorMessage =
|
|
738
|
+
"Rate limited by Datadog. Wait a moment and try again, or reduce your query scope.";
|
|
739
|
+
}
|
|
740
|
+
resolve({
|
|
741
|
+
success: false,
|
|
742
|
+
query: effectiveQuery,
|
|
743
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
744
|
+
events: [],
|
|
745
|
+
error: errorMessage,
|
|
746
|
+
});
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const response = JSON.parse(data);
|
|
751
|
+
const events = (response.data || []).map((event) => {
|
|
752
|
+
const attrs = event.attributes?.attributes || {};
|
|
753
|
+
return {
|
|
754
|
+
id: event.id,
|
|
755
|
+
type: event.type,
|
|
756
|
+
timestamp: event.attributes?.timestamp,
|
|
757
|
+
sessionId: attrs.session?.id,
|
|
758
|
+
viewUrl: attrs.view?.url,
|
|
759
|
+
viewName: attrs.view?.name,
|
|
760
|
+
errorMessage: attrs.error?.message,
|
|
761
|
+
errorType: attrs.error?.type,
|
|
762
|
+
attributes: attrs,
|
|
763
|
+
};
|
|
764
|
+
});
|
|
765
|
+
logger.info(`Retrieved ${events.length} RUM events`);
|
|
766
|
+
resolve({
|
|
767
|
+
success: true,
|
|
768
|
+
query: effectiveQuery,
|
|
769
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
770
|
+
events,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
catch (parseError) {
|
|
774
|
+
logger.error("Failed to parse Datadog RUM response:", parseError);
|
|
775
|
+
resolve({
|
|
776
|
+
success: false,
|
|
777
|
+
query: effectiveQuery,
|
|
778
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
779
|
+
events: [],
|
|
780
|
+
error: `Failed to parse response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
req.on("error", (error) => {
|
|
786
|
+
logger.error("Request error:", error);
|
|
787
|
+
resolve({
|
|
788
|
+
success: false,
|
|
789
|
+
query: effectiveQuery,
|
|
790
|
+
timeRange: { from: effectiveFrom, to: effectiveTo },
|
|
791
|
+
events: [],
|
|
792
|
+
error: `Connection error: ${error.message}`,
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
req.write(requestBody);
|
|
796
|
+
req.end();
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const BUILD_VERSION_STRING = "@jaypie/mcp@0.2.0"
|
|
801
|
+
;
|
|
802
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
803
|
+
const __dirname$1 = path.dirname(__filename$1);
|
|
804
|
+
const PROMPTS_PATH = path.join(__dirname$1, "..", "prompts");
|
|
16
805
|
// Logger utility
|
|
17
806
|
function createLogger(verbose) {
|
|
18
807
|
return {
|
|
@@ -79,7 +868,7 @@ function createMcpServer(options = {}) {
|
|
|
79
868
|
capabilities: {},
|
|
80
869
|
});
|
|
81
870
|
log.info("Registering tools...");
|
|
82
|
-
server.tool("list_prompts", "
|
|
871
|
+
server.tool("list_prompts", "List available Jaypie development prompts and guides. Use this FIRST when starting work on a Jaypie project to discover relevant documentation. Returns filenames, descriptions, and which file patterns each prompt applies to (e.g., 'Required for packages/express/**').", {}, async () => {
|
|
83
872
|
log.info("Tool called: list_prompts");
|
|
84
873
|
log.info(`Reading directory: ${PROMPTS_PATH}`);
|
|
85
874
|
try {
|
|
@@ -111,10 +900,10 @@ function createMcpServer(options = {}) {
|
|
|
111
900
|
}
|
|
112
901
|
});
|
|
113
902
|
log.info("Registered tool: list_prompts");
|
|
114
|
-
server.tool("read_prompt", "
|
|
903
|
+
server.tool("read_prompt", "Read a Jaypie prompt/guide by filename. Call list_prompts first to see available prompts. These contain best practices, templates, code patterns, and step-by-step guides for Jaypie development tasks.", {
|
|
115
904
|
filename: z
|
|
116
905
|
.string()
|
|
117
|
-
.describe("The
|
|
906
|
+
.describe("The prompt filename from list_prompts (e.g., 'Jaypie_Express_Package.md', 'Development_Process.md')"),
|
|
118
907
|
}, async ({ filename }) => {
|
|
119
908
|
log.info(`Tool called: read_prompt (filename: ${filename})`);
|
|
120
909
|
try {
|
|
@@ -155,6 +944,672 @@ function createMcpServer(options = {}) {
|
|
|
155
944
|
}
|
|
156
945
|
});
|
|
157
946
|
log.info("Registered tool: read_prompt");
|
|
947
|
+
server.tool("version", `Prints the current version and hash, \`${BUILD_VERSION_STRING}\``, {}, async () => {
|
|
948
|
+
log.info("Tool called: version");
|
|
949
|
+
return {
|
|
950
|
+
content: [
|
|
951
|
+
{
|
|
952
|
+
type: "text",
|
|
953
|
+
text: BUILD_VERSION_STRING,
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
};
|
|
957
|
+
});
|
|
958
|
+
log.info("Registered tool: version");
|
|
959
|
+
// Datadog Logs Tool
|
|
960
|
+
server.tool("datadog_logs", "Search and retrieve individual Datadog log entries. Use this to view actual log messages and details. For aggregated counts/statistics (e.g., 'how many errors by service?'), use datadog_log_analytics instead. Requires DATADOG_API_KEY and DATADOG_APP_KEY environment variables.", {
|
|
961
|
+
query: z
|
|
962
|
+
.string()
|
|
963
|
+
.optional()
|
|
964
|
+
.describe("Search query to filter logs. Examples: 'status:error', '@http.status_code:500', '*timeout*', '@requestId:abc123'. Combined with DD_ENV, DD_SERVICE, DD_SOURCE env vars if set."),
|
|
965
|
+
source: z
|
|
966
|
+
.string()
|
|
967
|
+
.optional()
|
|
968
|
+
.describe("Override the log source (e.g., 'lambda', 'auth0', 'nginx'). If not provided, uses DD_SOURCE env var or defaults to 'lambda'."),
|
|
969
|
+
env: z
|
|
970
|
+
.string()
|
|
971
|
+
.optional()
|
|
972
|
+
.describe("Override the environment (e.g., 'sandbox', 'kitchen', 'lab', 'studio', 'production'). If not provided, uses DD_ENV env var."),
|
|
973
|
+
service: z
|
|
974
|
+
.string()
|
|
975
|
+
.optional()
|
|
976
|
+
.describe("Override the service name. If not provided, uses DD_SERVICE env var."),
|
|
977
|
+
from: z
|
|
978
|
+
.string()
|
|
979
|
+
.optional()
|
|
980
|
+
.describe("Start time. Formats: relative ('now-15m', 'now-1h', 'now-1d'), ISO 8601 ('2024-01-15T10:00:00Z'). Defaults to 'now-15m'."),
|
|
981
|
+
to: z
|
|
982
|
+
.string()
|
|
983
|
+
.optional()
|
|
984
|
+
.describe("End time. Formats: 'now', relative ('now-5m'), or ISO 8601. Defaults to 'now'."),
|
|
985
|
+
limit: z
|
|
986
|
+
.number()
|
|
987
|
+
.optional()
|
|
988
|
+
.describe("Max logs to return (1-1000). Defaults to 50."),
|
|
989
|
+
sort: z
|
|
990
|
+
.enum(["timestamp", "-timestamp"])
|
|
991
|
+
.optional()
|
|
992
|
+
.describe("Sort order: 'timestamp' (oldest first) or '-timestamp' (newest first, default)."),
|
|
993
|
+
}, async ({ query, source, env, service, from, to, limit, sort }) => {
|
|
994
|
+
log.info("Tool called: datadog_logs");
|
|
995
|
+
const credentials = getDatadogCredentials();
|
|
996
|
+
if (!credentials) {
|
|
997
|
+
const missingApiKey = !process.env.DATADOG_API_KEY && !process.env.DD_API_KEY;
|
|
998
|
+
const missingAppKey = !process.env.DATADOG_APP_KEY &&
|
|
999
|
+
!process.env.DATADOG_APPLICATION_KEY &&
|
|
1000
|
+
!process.env.DD_APP_KEY &&
|
|
1001
|
+
!process.env.DD_APPLICATION_KEY;
|
|
1002
|
+
if (missingApiKey) {
|
|
1003
|
+
log.error("No Datadog API key found in environment");
|
|
1004
|
+
return {
|
|
1005
|
+
content: [
|
|
1006
|
+
{
|
|
1007
|
+
type: "text",
|
|
1008
|
+
text: "Error: No Datadog API key found. Please set DATADOG_API_KEY or DD_API_KEY environment variable.",
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (missingAppKey) {
|
|
1014
|
+
log.error("No Datadog Application key found in environment");
|
|
1015
|
+
return {
|
|
1016
|
+
content: [
|
|
1017
|
+
{
|
|
1018
|
+
type: "text",
|
|
1019
|
+
text: "Error: No Datadog Application key found. Please set DATADOG_APP_KEY or DD_APP_KEY environment variable. The Logs Search API requires both an API key and an Application key.",
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// credentials is guaranteed to be non-null here
|
|
1026
|
+
const result = await searchDatadogLogs(credentials, {
|
|
1027
|
+
query,
|
|
1028
|
+
source,
|
|
1029
|
+
env,
|
|
1030
|
+
service,
|
|
1031
|
+
from,
|
|
1032
|
+
to,
|
|
1033
|
+
limit,
|
|
1034
|
+
sort,
|
|
1035
|
+
}, log);
|
|
1036
|
+
if (!result.success) {
|
|
1037
|
+
return {
|
|
1038
|
+
content: [
|
|
1039
|
+
{
|
|
1040
|
+
type: "text",
|
|
1041
|
+
text: `Error from Datadog API: ${result.error}`,
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
if (result.logs.length === 0) {
|
|
1047
|
+
return {
|
|
1048
|
+
content: [
|
|
1049
|
+
{
|
|
1050
|
+
type: "text",
|
|
1051
|
+
text: `No logs found for query: ${result.query}\nTime range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1052
|
+
},
|
|
1053
|
+
],
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
const resultText = [
|
|
1057
|
+
`Query: ${result.query}`,
|
|
1058
|
+
`Time range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1059
|
+
`Found ${result.logs.length} log entries:`,
|
|
1060
|
+
"",
|
|
1061
|
+
JSON.stringify(result.logs, null, 2),
|
|
1062
|
+
].join("\n");
|
|
1063
|
+
return {
|
|
1064
|
+
content: [
|
|
1065
|
+
{
|
|
1066
|
+
type: "text",
|
|
1067
|
+
text: resultText,
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
};
|
|
1071
|
+
});
|
|
1072
|
+
log.info("Registered tool: datadog_logs");
|
|
1073
|
+
// Datadog Log Analytics Tool
|
|
1074
|
+
server.tool("datadog_log_analytics", "Aggregate and analyze Datadog logs by grouping them by fields. Use this for statistics and counts (e.g., 'errors by service', 'requests by status code'). For viewing individual log entries, use datadog_logs instead.", {
|
|
1075
|
+
groupBy: z
|
|
1076
|
+
.array(z.string())
|
|
1077
|
+
.describe("Fields to group by. Examples: ['source'], ['service', 'status'], ['@http.status_code']. Common facets: source, service, status, host, @http.status_code, @env."),
|
|
1078
|
+
query: z
|
|
1079
|
+
.string()
|
|
1080
|
+
.optional()
|
|
1081
|
+
.describe("Filter query. Examples: 'status:error', '*timeout*', '@http.method:POST'. Use '*' for all logs."),
|
|
1082
|
+
source: z
|
|
1083
|
+
.string()
|
|
1084
|
+
.optional()
|
|
1085
|
+
.describe("Override the log source filter. Use '*' to include all sources. If not provided, uses DD_SOURCE env var or defaults to 'lambda'."),
|
|
1086
|
+
env: z
|
|
1087
|
+
.string()
|
|
1088
|
+
.optional()
|
|
1089
|
+
.describe("Override the environment filter. If not provided, uses DD_ENV env var."),
|
|
1090
|
+
service: z
|
|
1091
|
+
.string()
|
|
1092
|
+
.optional()
|
|
1093
|
+
.describe("Override the service name filter. If not provided, uses DD_SERVICE env var."),
|
|
1094
|
+
from: z
|
|
1095
|
+
.string()
|
|
1096
|
+
.optional()
|
|
1097
|
+
.describe("Start time. Formats: relative ('now-15m', 'now-1h', 'now-1d'), ISO 8601 ('2024-01-15T10:00:00Z'). Defaults to 'now-15m'."),
|
|
1098
|
+
to: z
|
|
1099
|
+
.string()
|
|
1100
|
+
.optional()
|
|
1101
|
+
.describe("End time. Formats: 'now', relative ('now-5m'), or ISO 8601. Defaults to 'now'."),
|
|
1102
|
+
aggregation: z
|
|
1103
|
+
.enum(["count", "avg", "sum", "min", "max", "cardinality"])
|
|
1104
|
+
.optional()
|
|
1105
|
+
.describe("Aggregation type. 'count' counts logs, others require a metric field. Defaults to 'count'."),
|
|
1106
|
+
metric: z
|
|
1107
|
+
.string()
|
|
1108
|
+
.optional()
|
|
1109
|
+
.describe("Metric field to aggregate when using avg, sum, min, max, or cardinality. E.g., '@duration', '@http.response_time'."),
|
|
1110
|
+
}, async ({ groupBy, query, source, env, service, from, to, aggregation, metric, }) => {
|
|
1111
|
+
log.info("Tool called: datadog_log_analytics");
|
|
1112
|
+
const credentials = getDatadogCredentials();
|
|
1113
|
+
if (!credentials) {
|
|
1114
|
+
const missingApiKey = !process.env.DATADOG_API_KEY && !process.env.DD_API_KEY;
|
|
1115
|
+
const missingAppKey = !process.env.DATADOG_APP_KEY &&
|
|
1116
|
+
!process.env.DATADOG_APPLICATION_KEY &&
|
|
1117
|
+
!process.env.DD_APP_KEY &&
|
|
1118
|
+
!process.env.DD_APPLICATION_KEY;
|
|
1119
|
+
if (missingApiKey) {
|
|
1120
|
+
log.error("No Datadog API key found in environment");
|
|
1121
|
+
return {
|
|
1122
|
+
content: [
|
|
1123
|
+
{
|
|
1124
|
+
type: "text",
|
|
1125
|
+
text: "Error: No Datadog API key found. Please set DATADOG_API_KEY or DD_API_KEY environment variable.",
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
if (missingAppKey) {
|
|
1131
|
+
log.error("No Datadog Application key found in environment");
|
|
1132
|
+
return {
|
|
1133
|
+
content: [
|
|
1134
|
+
{
|
|
1135
|
+
type: "text",
|
|
1136
|
+
text: "Error: No Datadog Application key found. Please set DATADOG_APP_KEY or DD_APP_KEY environment variable.",
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const compute = aggregation
|
|
1143
|
+
? [{ aggregation, metric }]
|
|
1144
|
+
: [{ aggregation: "count" }];
|
|
1145
|
+
const result = await aggregateDatadogLogs(credentials, {
|
|
1146
|
+
query,
|
|
1147
|
+
source,
|
|
1148
|
+
env,
|
|
1149
|
+
service,
|
|
1150
|
+
from,
|
|
1151
|
+
to,
|
|
1152
|
+
groupBy,
|
|
1153
|
+
compute,
|
|
1154
|
+
}, log);
|
|
1155
|
+
if (!result.success) {
|
|
1156
|
+
return {
|
|
1157
|
+
content: [
|
|
1158
|
+
{
|
|
1159
|
+
type: "text",
|
|
1160
|
+
text: `Error from Datadog Analytics API: ${result.error}`,
|
|
1161
|
+
},
|
|
1162
|
+
],
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
if (result.buckets.length === 0) {
|
|
1166
|
+
return {
|
|
1167
|
+
content: [
|
|
1168
|
+
{
|
|
1169
|
+
type: "text",
|
|
1170
|
+
text: `No data found for query: ${result.query}\nTime range: ${result.timeRange.from} to ${result.timeRange.to}\nGrouped by: ${result.groupBy.join(", ")}`,
|
|
1171
|
+
},
|
|
1172
|
+
],
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
// Format buckets as a readable table
|
|
1176
|
+
const formattedBuckets = result.buckets.map((bucket) => {
|
|
1177
|
+
const byParts = Object.entries(bucket.by)
|
|
1178
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
1179
|
+
.join(", ");
|
|
1180
|
+
const computeParts = Object.entries(bucket.computes)
|
|
1181
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
1182
|
+
.join(", ");
|
|
1183
|
+
return ` ${byParts} => ${computeParts}`;
|
|
1184
|
+
});
|
|
1185
|
+
const resultText = [
|
|
1186
|
+
`Query: ${result.query}`,
|
|
1187
|
+
`Time range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1188
|
+
`Grouped by: ${result.groupBy.join(", ")}`,
|
|
1189
|
+
`Found ${result.buckets.length} groups:`,
|
|
1190
|
+
"",
|
|
1191
|
+
...formattedBuckets,
|
|
1192
|
+
].join("\n");
|
|
1193
|
+
return {
|
|
1194
|
+
content: [
|
|
1195
|
+
{
|
|
1196
|
+
type: "text",
|
|
1197
|
+
text: resultText,
|
|
1198
|
+
},
|
|
1199
|
+
],
|
|
1200
|
+
};
|
|
1201
|
+
});
|
|
1202
|
+
log.info("Registered tool: datadog_log_analytics");
|
|
1203
|
+
// Datadog Monitors Tool
|
|
1204
|
+
server.tool("datadog_monitors", "List and check Datadog monitors. Shows monitor status (Alert, Warn, No Data, OK), name, type, and tags. Useful for quickly checking if any monitors are alerting.", {
|
|
1205
|
+
status: z
|
|
1206
|
+
.array(z.enum(["Alert", "Warn", "No Data", "OK"]))
|
|
1207
|
+
.optional()
|
|
1208
|
+
.describe("Filter monitors by status. E.g., ['Alert', 'Warn'] to see only alerting monitors."),
|
|
1209
|
+
tags: z
|
|
1210
|
+
.array(z.string())
|
|
1211
|
+
.optional()
|
|
1212
|
+
.describe("Filter monitors by resource tags (tags on the monitored resources)."),
|
|
1213
|
+
monitorTags: z
|
|
1214
|
+
.array(z.string())
|
|
1215
|
+
.optional()
|
|
1216
|
+
.describe("Filter monitors by monitor tags (tags on the monitor itself)."),
|
|
1217
|
+
name: z
|
|
1218
|
+
.string()
|
|
1219
|
+
.optional()
|
|
1220
|
+
.describe("Filter monitors by name (partial match supported)."),
|
|
1221
|
+
}, async ({ status, tags, monitorTags, name }) => {
|
|
1222
|
+
log.info("Tool called: datadog_monitors");
|
|
1223
|
+
const credentials = getDatadogCredentials();
|
|
1224
|
+
if (!credentials) {
|
|
1225
|
+
return {
|
|
1226
|
+
content: [
|
|
1227
|
+
{
|
|
1228
|
+
type: "text",
|
|
1229
|
+
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const result = await listDatadogMonitors(credentials, { status, tags, monitorTags, name }, log);
|
|
1235
|
+
if (!result.success) {
|
|
1236
|
+
return {
|
|
1237
|
+
content: [
|
|
1238
|
+
{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: `Error from Datadog Monitors API: ${result.error}`,
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
if (result.monitors.length === 0) {
|
|
1246
|
+
return {
|
|
1247
|
+
content: [
|
|
1248
|
+
{
|
|
1249
|
+
type: "text",
|
|
1250
|
+
text: "No monitors found matching the specified criteria.",
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
// Group monitors by status for better readability
|
|
1256
|
+
const byStatus = {};
|
|
1257
|
+
for (const monitor of result.monitors) {
|
|
1258
|
+
const status = monitor.status;
|
|
1259
|
+
if (!byStatus[status]) {
|
|
1260
|
+
byStatus[status] = [];
|
|
1261
|
+
}
|
|
1262
|
+
byStatus[status].push(monitor);
|
|
1263
|
+
}
|
|
1264
|
+
const statusOrder = ["Alert", "Warn", "No Data", "OK", "Unknown"];
|
|
1265
|
+
const formattedMonitors = [];
|
|
1266
|
+
for (const status of statusOrder) {
|
|
1267
|
+
const monitors = byStatus[status];
|
|
1268
|
+
if (monitors && monitors.length > 0) {
|
|
1269
|
+
formattedMonitors.push(`\n## ${status} (${monitors.length})`);
|
|
1270
|
+
for (const m of monitors) {
|
|
1271
|
+
formattedMonitors.push(` - [${m.id}] ${m.name} (${m.type})`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const resultText = [
|
|
1276
|
+
`Found ${result.monitors.length} monitors:`,
|
|
1277
|
+
...formattedMonitors,
|
|
1278
|
+
].join("\n");
|
|
1279
|
+
return {
|
|
1280
|
+
content: [
|
|
1281
|
+
{
|
|
1282
|
+
type: "text",
|
|
1283
|
+
text: resultText,
|
|
1284
|
+
},
|
|
1285
|
+
],
|
|
1286
|
+
};
|
|
1287
|
+
});
|
|
1288
|
+
log.info("Registered tool: datadog_monitors");
|
|
1289
|
+
// Datadog Synthetics Tool
|
|
1290
|
+
server.tool("datadog_synthetics", "List Datadog Synthetic tests and optionally get recent results for a specific test. Shows test status, type (api/browser), and locations.", {
|
|
1291
|
+
type: z
|
|
1292
|
+
.enum(["api", "browser"])
|
|
1293
|
+
.optional()
|
|
1294
|
+
.describe("Filter tests by type: 'api' or 'browser'."),
|
|
1295
|
+
tags: z.array(z.string()).optional().describe("Filter tests by tags."),
|
|
1296
|
+
testId: z
|
|
1297
|
+
.string()
|
|
1298
|
+
.optional()
|
|
1299
|
+
.describe("If provided, fetches recent results for this specific test (public_id). Otherwise lists all tests."),
|
|
1300
|
+
}, async ({ type, tags, testId }) => {
|
|
1301
|
+
log.info("Tool called: datadog_synthetics");
|
|
1302
|
+
const credentials = getDatadogCredentials();
|
|
1303
|
+
if (!credentials) {
|
|
1304
|
+
return {
|
|
1305
|
+
content: [
|
|
1306
|
+
{
|
|
1307
|
+
type: "text",
|
|
1308
|
+
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
1309
|
+
},
|
|
1310
|
+
],
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
// If testId is provided, get results for that specific test
|
|
1314
|
+
if (testId) {
|
|
1315
|
+
const result = await getDatadogSyntheticResults(credentials, testId, log);
|
|
1316
|
+
if (!result.success) {
|
|
1317
|
+
return {
|
|
1318
|
+
content: [
|
|
1319
|
+
{
|
|
1320
|
+
type: "text",
|
|
1321
|
+
text: `Error from Datadog Synthetics API: ${result.error}`,
|
|
1322
|
+
},
|
|
1323
|
+
],
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
if (result.results.length === 0) {
|
|
1327
|
+
return {
|
|
1328
|
+
content: [
|
|
1329
|
+
{
|
|
1330
|
+
type: "text",
|
|
1331
|
+
text: `No recent results found for test: ${testId}`,
|
|
1332
|
+
},
|
|
1333
|
+
],
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const passedCount = result.results.filter((r) => r.passed).length;
|
|
1337
|
+
const failedCount = result.results.length - passedCount;
|
|
1338
|
+
const formattedResults = result.results.slice(0, 10).map((r) => {
|
|
1339
|
+
const date = new Date(r.checkTime * 1000).toISOString();
|
|
1340
|
+
const status = r.passed ? "✓ PASSED" : "✗ FAILED";
|
|
1341
|
+
return ` ${date} - ${status}`;
|
|
1342
|
+
});
|
|
1343
|
+
const resultText = [
|
|
1344
|
+
`Results for test: ${testId}`,
|
|
1345
|
+
`Recent: ${passedCount} passed, ${failedCount} failed (showing last ${Math.min(10, result.results.length)})`,
|
|
1346
|
+
"",
|
|
1347
|
+
...formattedResults,
|
|
1348
|
+
].join("\n");
|
|
1349
|
+
return {
|
|
1350
|
+
content: [
|
|
1351
|
+
{
|
|
1352
|
+
type: "text",
|
|
1353
|
+
text: resultText,
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
// Otherwise list all tests
|
|
1359
|
+
const result = await listDatadogSynthetics(credentials, { type, tags }, log);
|
|
1360
|
+
if (!result.success) {
|
|
1361
|
+
return {
|
|
1362
|
+
content: [
|
|
1363
|
+
{
|
|
1364
|
+
type: "text",
|
|
1365
|
+
text: `Error from Datadog Synthetics API: ${result.error}`,
|
|
1366
|
+
},
|
|
1367
|
+
],
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (result.tests.length === 0) {
|
|
1371
|
+
return {
|
|
1372
|
+
content: [
|
|
1373
|
+
{
|
|
1374
|
+
type: "text",
|
|
1375
|
+
text: "No synthetic tests found matching the specified criteria.",
|
|
1376
|
+
},
|
|
1377
|
+
],
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
// Group by status
|
|
1381
|
+
const byStatus = {};
|
|
1382
|
+
for (const test of result.tests) {
|
|
1383
|
+
const status = test.status;
|
|
1384
|
+
if (!byStatus[status]) {
|
|
1385
|
+
byStatus[status] = [];
|
|
1386
|
+
}
|
|
1387
|
+
byStatus[status].push(test);
|
|
1388
|
+
}
|
|
1389
|
+
const formattedTests = [];
|
|
1390
|
+
for (const [status, tests] of Object.entries(byStatus)) {
|
|
1391
|
+
formattedTests.push(`\n## ${status} (${tests.length})`);
|
|
1392
|
+
for (const t of tests) {
|
|
1393
|
+
formattedTests.push(` - [${t.publicId}] ${t.name} (${t.type})`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const resultText = [
|
|
1397
|
+
`Found ${result.tests.length} synthetic tests:`,
|
|
1398
|
+
...formattedTests,
|
|
1399
|
+
].join("\n");
|
|
1400
|
+
return {
|
|
1401
|
+
content: [
|
|
1402
|
+
{
|
|
1403
|
+
type: "text",
|
|
1404
|
+
text: resultText,
|
|
1405
|
+
},
|
|
1406
|
+
],
|
|
1407
|
+
};
|
|
1408
|
+
});
|
|
1409
|
+
log.info("Registered tool: datadog_synthetics");
|
|
1410
|
+
// Datadog Metrics Tool
|
|
1411
|
+
server.tool("datadog_metrics", "Query Datadog metrics. Returns timeseries data for the specified metric query. Useful for checking specific metric values.", {
|
|
1412
|
+
query: z
|
|
1413
|
+
.string()
|
|
1414
|
+
.describe("Metric query. Format: 'aggregation:metric.name{tags}'. Examples: 'avg:system.cpu.user{*}', 'sum:aws.lambda.invocations{function:my-func}.as_count()', 'max:aws.lambda.duration{env:production}'."),
|
|
1415
|
+
from: z
|
|
1416
|
+
.string()
|
|
1417
|
+
.optional()
|
|
1418
|
+
.describe("Start time. Formats: relative ('1h', '30m', '1d'), or Unix timestamp. Defaults to '1h'."),
|
|
1419
|
+
to: z
|
|
1420
|
+
.string()
|
|
1421
|
+
.optional()
|
|
1422
|
+
.describe("End time. Formats: 'now' or Unix timestamp. Defaults to 'now'."),
|
|
1423
|
+
}, async ({ query, from, to }) => {
|
|
1424
|
+
log.info("Tool called: datadog_metrics");
|
|
1425
|
+
const credentials = getDatadogCredentials();
|
|
1426
|
+
if (!credentials) {
|
|
1427
|
+
return {
|
|
1428
|
+
content: [
|
|
1429
|
+
{
|
|
1430
|
+
type: "text",
|
|
1431
|
+
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
1432
|
+
},
|
|
1433
|
+
],
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
// Parse time parameters
|
|
1437
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1438
|
+
let fromTs;
|
|
1439
|
+
let toTs;
|
|
1440
|
+
// Parse 'from' parameter
|
|
1441
|
+
const fromStr = from || "1h";
|
|
1442
|
+
if (fromStr.match(/^\d+$/)) {
|
|
1443
|
+
fromTs = parseInt(fromStr, 10);
|
|
1444
|
+
}
|
|
1445
|
+
else if (fromStr.match(/^(\d+)h$/)) {
|
|
1446
|
+
const hours = parseInt(fromStr.match(/^(\d+)h$/)[1], 10);
|
|
1447
|
+
fromTs = now - hours * 3600;
|
|
1448
|
+
}
|
|
1449
|
+
else if (fromStr.match(/^(\d+)m$/)) {
|
|
1450
|
+
const minutes = parseInt(fromStr.match(/^(\d+)m$/)[1], 10);
|
|
1451
|
+
fromTs = now - minutes * 60;
|
|
1452
|
+
}
|
|
1453
|
+
else if (fromStr.match(/^(\d+)d$/)) {
|
|
1454
|
+
const days = parseInt(fromStr.match(/^(\d+)d$/)[1], 10);
|
|
1455
|
+
fromTs = now - days * 86400;
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
fromTs = now - 3600; // Default 1 hour
|
|
1459
|
+
}
|
|
1460
|
+
// Parse 'to' parameter
|
|
1461
|
+
const toStr = to || "now";
|
|
1462
|
+
if (toStr === "now") {
|
|
1463
|
+
toTs = now;
|
|
1464
|
+
}
|
|
1465
|
+
else if (toStr.match(/^\d+$/)) {
|
|
1466
|
+
toTs = parseInt(toStr, 10);
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
toTs = now;
|
|
1470
|
+
}
|
|
1471
|
+
const result = await queryDatadogMetrics(credentials, { query, from: fromTs, to: toTs }, log);
|
|
1472
|
+
if (!result.success) {
|
|
1473
|
+
return {
|
|
1474
|
+
content: [
|
|
1475
|
+
{
|
|
1476
|
+
type: "text",
|
|
1477
|
+
text: `Error from Datadog Metrics API: ${result.error}`,
|
|
1478
|
+
},
|
|
1479
|
+
],
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
if (result.series.length === 0) {
|
|
1483
|
+
return {
|
|
1484
|
+
content: [
|
|
1485
|
+
{
|
|
1486
|
+
type: "text",
|
|
1487
|
+
text: `No data found for query: ${query}\nTime range: ${new Date(fromTs * 1000).toISOString()} to ${new Date(toTs * 1000).toISOString()}`,
|
|
1488
|
+
},
|
|
1489
|
+
],
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
const formattedSeries = result.series.map((s) => {
|
|
1493
|
+
const points = s.pointlist.slice(-5); // Last 5 points
|
|
1494
|
+
const formattedPoints = points
|
|
1495
|
+
.map(([ts, val]) => {
|
|
1496
|
+
const date = new Date(ts).toISOString();
|
|
1497
|
+
return ` ${date}: ${val !== null ? val.toFixed(4) : "null"}`;
|
|
1498
|
+
})
|
|
1499
|
+
.join("\n");
|
|
1500
|
+
return `\n ${s.metric} (${s.scope})${s.unit ? ` [${s.unit}]` : ""}:\n${formattedPoints}`;
|
|
1501
|
+
});
|
|
1502
|
+
const resultText = [
|
|
1503
|
+
`Query: ${query}`,
|
|
1504
|
+
`Time range: ${new Date(fromTs * 1000).toISOString()} to ${new Date(toTs * 1000).toISOString()}`,
|
|
1505
|
+
`Found ${result.series.length} series (showing last 5 points each):`,
|
|
1506
|
+
...formattedSeries,
|
|
1507
|
+
].join("\n");
|
|
1508
|
+
return {
|
|
1509
|
+
content: [
|
|
1510
|
+
{
|
|
1511
|
+
type: "text",
|
|
1512
|
+
text: resultText,
|
|
1513
|
+
},
|
|
1514
|
+
],
|
|
1515
|
+
};
|
|
1516
|
+
});
|
|
1517
|
+
log.info("Registered tool: datadog_metrics");
|
|
1518
|
+
// Datadog RUM Tool
|
|
1519
|
+
server.tool("datadog_rum", "Search Datadog RUM (Real User Monitoring) events. Find user sessions, page views, errors, and actions. Useful for debugging frontend issues and understanding user behavior.", {
|
|
1520
|
+
query: z
|
|
1521
|
+
.string()
|
|
1522
|
+
.optional()
|
|
1523
|
+
.describe("RUM search query. E.g., '@type:error', '@session.id:abc123', '@view.url:*checkout*'. Defaults to '*' (all events)."),
|
|
1524
|
+
from: z
|
|
1525
|
+
.string()
|
|
1526
|
+
.optional()
|
|
1527
|
+
.describe("Start time. Formats: relative ('now-15m', 'now-1h', 'now-1d'), ISO 8601 ('2024-01-15T10:00:00Z'). Defaults to 'now-15m'."),
|
|
1528
|
+
to: z
|
|
1529
|
+
.string()
|
|
1530
|
+
.optional()
|
|
1531
|
+
.describe("End time. Formats: 'now', relative ('now-5m'), or ISO 8601. Defaults to 'now'."),
|
|
1532
|
+
limit: z
|
|
1533
|
+
.number()
|
|
1534
|
+
.optional()
|
|
1535
|
+
.describe("Max events to return (1-1000). Defaults to 50."),
|
|
1536
|
+
}, async ({ query, from, to, limit }) => {
|
|
1537
|
+
log.info("Tool called: datadog_rum");
|
|
1538
|
+
const credentials = getDatadogCredentials();
|
|
1539
|
+
if (!credentials) {
|
|
1540
|
+
return {
|
|
1541
|
+
content: [
|
|
1542
|
+
{
|
|
1543
|
+
type: "text",
|
|
1544
|
+
text: "Error: Datadog credentials not found. Please set DATADOG_API_KEY and DATADOG_APP_KEY environment variables.",
|
|
1545
|
+
},
|
|
1546
|
+
],
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
const result = await searchDatadogRum(credentials, { query, from, to, limit }, log);
|
|
1550
|
+
if (!result.success) {
|
|
1551
|
+
return {
|
|
1552
|
+
content: [
|
|
1553
|
+
{
|
|
1554
|
+
type: "text",
|
|
1555
|
+
text: `Error from Datadog RUM API: ${result.error}`,
|
|
1556
|
+
},
|
|
1557
|
+
],
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
if (result.events.length === 0) {
|
|
1561
|
+
return {
|
|
1562
|
+
content: [
|
|
1563
|
+
{
|
|
1564
|
+
type: "text",
|
|
1565
|
+
text: `No RUM events found for query: ${result.query}\nTime range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1566
|
+
},
|
|
1567
|
+
],
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
// Group events by type for better readability
|
|
1571
|
+
const byType = {};
|
|
1572
|
+
for (const event of result.events) {
|
|
1573
|
+
const type = event.type;
|
|
1574
|
+
if (!byType[type]) {
|
|
1575
|
+
byType[type] = [];
|
|
1576
|
+
}
|
|
1577
|
+
byType[type].push(event);
|
|
1578
|
+
}
|
|
1579
|
+
const formattedEvents = [];
|
|
1580
|
+
for (const [type, events] of Object.entries(byType)) {
|
|
1581
|
+
formattedEvents.push(`\n## ${type} (${events.length})`);
|
|
1582
|
+
for (const e of events.slice(0, 10)) {
|
|
1583
|
+
// Limit per type
|
|
1584
|
+
const parts = [e.timestamp];
|
|
1585
|
+
if (e.viewName || e.viewUrl) {
|
|
1586
|
+
parts.push(e.viewName || e.viewUrl || "");
|
|
1587
|
+
}
|
|
1588
|
+
if (e.errorMessage) {
|
|
1589
|
+
parts.push(`Error: ${e.errorMessage}`);
|
|
1590
|
+
}
|
|
1591
|
+
if (e.sessionId) {
|
|
1592
|
+
parts.push(`Session: ${e.sessionId.substring(0, 8)}...`);
|
|
1593
|
+
}
|
|
1594
|
+
formattedEvents.push(` - ${parts.join(" | ")}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const resultText = [
|
|
1598
|
+
`Query: ${result.query}`,
|
|
1599
|
+
`Time range: ${result.timeRange.from} to ${result.timeRange.to}`,
|
|
1600
|
+
`Found ${result.events.length} RUM events:`,
|
|
1601
|
+
...formattedEvents,
|
|
1602
|
+
].join("\n");
|
|
1603
|
+
return {
|
|
1604
|
+
content: [
|
|
1605
|
+
{
|
|
1606
|
+
type: "text",
|
|
1607
|
+
text: resultText,
|
|
1608
|
+
},
|
|
1609
|
+
],
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1612
|
+
log.info("Registered tool: datadog_rum");
|
|
158
1613
|
log.info("MCP server configuration complete");
|
|
159
1614
|
return server;
|
|
160
1615
|
}
|