@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.
Files changed (41) hide show
  1. package/dist/datadog.d.ts +212 -0
  2. package/dist/index.js +1461 -6
  3. package/dist/index.js.map +1 -1
  4. package/package.json +10 -7
  5. package/prompts/Development_Process.md +57 -35
  6. package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +143 -19
  7. package/prompts/Jaypie_Express_Package.md +408 -0
  8. package/prompts/Jaypie_Init_Express_on_Lambda.md +66 -38
  9. package/prompts/Jaypie_Init_Lambda_Package.md +202 -83
  10. package/prompts/Jaypie_Init_Project_Subpackage.md +21 -26
  11. package/prompts/Jaypie_Legacy_Patterns.md +4 -0
  12. package/prompts/Templates_CDK_Subpackage.md +113 -0
  13. package/prompts/Templates_Express_Subpackage.md +183 -0
  14. package/prompts/Templates_Project_Monorepo.md +326 -0
  15. package/prompts/Templates_Project_Subpackage.md +93 -0
  16. package/LICENSE.txt +0 -21
  17. package/prompts/Jaypie_Mongoose_Models_Package.md +0 -231
  18. package/prompts/Jaypie_Mongoose_with_Express_CRUD.md +0 -1000
  19. package/prompts/templates/cdk-subpackage/bin/cdk.ts +0 -11
  20. package/prompts/templates/cdk-subpackage/cdk.json +0 -19
  21. package/prompts/templates/cdk-subpackage/lib/cdk-app.ts +0 -41
  22. package/prompts/templates/cdk-subpackage/lib/cdk-infrastructure.ts +0 -15
  23. package/prompts/templates/express-subpackage/index.ts +0 -8
  24. package/prompts/templates/express-subpackage/src/app.ts +0 -18
  25. package/prompts/templates/express-subpackage/src/handler.config.ts +0 -44
  26. package/prompts/templates/express-subpackage/src/routes/resource/__tests__/resourceGet.route.spec.ts +0 -29
  27. package/prompts/templates/express-subpackage/src/routes/resource/resourceGet.route.ts +0 -22
  28. package/prompts/templates/express-subpackage/src/routes/resource.router.ts +0 -11
  29. package/prompts/templates/express-subpackage/src/types/express.ts +0 -9
  30. package/prompts/templates/project-monorepo/.vscode/settings.json +0 -72
  31. package/prompts/templates/project-monorepo/eslint.config.mjs +0 -1
  32. package/prompts/templates/project-monorepo/gitignore +0 -11
  33. package/prompts/templates/project-monorepo/package.json +0 -20
  34. package/prompts/templates/project-monorepo/tsconfig.base.json +0 -18
  35. package/prompts/templates/project-monorepo/tsconfig.json +0 -6
  36. package/prompts/templates/project-monorepo/vitest.workspace.js +0 -3
  37. package/prompts/templates/project-subpackage/package.json +0 -16
  38. package/prompts/templates/project-subpackage/tsconfig.json +0 -11
  39. package/prompts/templates/project-subpackage/vite.config.ts +0 -21
  40. package/prompts/templates/project-subpackage/vitest.config.ts +0 -7
  41. 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
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
- const PROMPTS_PATH = path.join(__dirname, "..", "prompts");
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", "Returns a bulleted list of all .md files in the prompts directory with their descriptions and requirements", {}, async () => {
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", "Returns the contents of a specified prompt file", {
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 name of the prompt file to read (e.g., example_prompt.md)"),
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
  }