@dynatrace-oss/dynatrace-mcp-server 0.5.0 → 0.6.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,31 @@
1
1
  # Dynatrace MCP Server
2
2
 
3
+ <h4 align="center">
4
+ <a href="https://github.com/dynatrace-oss/dynatrace-mcp/releases">
5
+ <img src="https://img.shields.io/github/release/dynatrace-oss/dynatrace-mcp" />
6
+ </a>
7
+ <a href="https://github.com/dynatrace-oss/dynatrace-mcp/blob/main/LICENSE">
8
+ <img src="https://img.shields.io/badge/license-mit-blue.svg" alt="Dynatrace MCP Server is released under the MIT License" />
9
+ </a>
10
+ <a href="https://www.npmjs.com/package/@dynatrace-oss/dynatrace-mcp-server">
11
+ <img src="https://img.shields.io/npm/dm/@dynatrace-oss/dynatrace-mcp-server?logo=npm&style=flat&color=red" alt="npm" />
12
+ </a>
13
+ <a href="https://github.com/dynatrace-oss/dynatrace-mcp">
14
+ <img src="https://img.shields.io/github/stars/dynatrace-oss/dynatrace-mcp" alt="Dynatrace MCP Server Stars on GitHub" />
15
+ </a>
16
+ <a href="https://github.com/dynatrace-oss/dynatrace-mcp">
17
+ <img src="https://img.shields.io/github/contributors/dynatrace-oss/dynatrace-mcp?color=green" alt="Dynatrace MCP Server Contributors on GitHub" />
18
+ </a>
19
+ </h4>
20
+
3
21
  This local MCP server allows interaction with the [Dynatrace](https://www.dynatrace.com/) observability platform.
4
22
  Bring real-time observability data directly into your development workflow.
5
23
 
6
- <img width="1046" alt="image" src="/assets/dynatrace-mcp-arch.png" />
24
+ > Note: This product is not officially supported by Dynatrace.
25
+
26
+ Please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
27
+
28
+ ![Architecture](https://github.com/dynatrace-oss/dynatrace-mcp/blob/main/assets/dynatrace-mcp-arch.png?raw=true)
7
29
 
8
30
  ## Use cases
9
31
 
@@ -27,19 +49,40 @@ Bring real-time observability data directly into your development workflow.
27
49
  - Get more information about a monitored entity
28
50
  - Get Ownership of an entity
29
51
 
30
- ## Costs
52
+ ### Costs
31
53
 
32
- **Important:** While this local MCP server is provided for free, using it to access data in Dynatrace Grail may incur additional costs based
54
+ **Important:** While this local MCP server is provided for free, using certain capabilities to access data in Dynatrace Grail may incur additional costs based
33
55
  on your Dynatrace consumption model. This affects `execute_dql` tool and other capabilities that **query** Dynatrace Grail storage, and costs
34
- depend on the volume (GB scanned/billed).
56
+ depend on the volume (GB scanned).
35
57
 
36
58
  **Before using this MCP server extensively, please:**
37
59
 
38
60
  1. Review your current Dynatrace consumption model and pricing
39
61
  2. Understand the cost implications of the specific data you plan to query (logs, events, metrics) - see [Dynatrace Pricing and Rate Card](https://www.dynatrace.com/pricing/)
40
62
  3. Start with smaller timeframes (e.g., 12h-24h) and make use of [buckets](https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model#built-in-grail-buckets) to reduce the cost impact
63
+ 4. Set an appropriate `DT_GRAIL_QUERY_BUDGET_GB` environment variable (default: 1000 GB) to control and monitor your Grail query consumption
64
+
65
+ **Grail Budget Tracking:**
66
+
67
+ The MCP server includes built-in budget tracking for Grail queries to help you monitor and control costs:
41
68
 
42
- **Note**: We will be providing a way to monitor Query Usage of the dynatrace-mcp-server in the future.
69
+ - Set `DT_GRAIL_QUERY_BUDGET_GB` (default: 1000 GB) to define your session budget limit
70
+ - The server tracks bytes scanned across all Grail queries in the current session
71
+ - You'll receive warnings when approaching 80% of your budget
72
+ - Budget exceeded alerts help prevent unexpected high consumption
73
+ - Budget resets when you restart the MCP server session
74
+
75
+ **To understand costs that occured:**
76
+
77
+ Execute the following DQL statement in a notebook to see how much bytes have been queried from Grail (Logs, Events, etc...):
78
+
79
+ ```
80
+ fetch dt.system.events
81
+ | filter event.kind == "QUERY_EXECUTION_EVENT" and contains(client.client_context, "dynatrace-mcp")
82
+ | sort timestamp desc
83
+ | fields timestamp, query_id, query_string, scanned_bytes, table, bucket, user.id, user.email, client.client_context
84
+ | maketimeSeries sum(scanned_bytes), by: { user.email, user.id, table }
85
+ ```
43
86
 
44
87
  ### AI-Powered Assistance (Preview)
45
88
 
@@ -56,7 +99,7 @@ Enhance your AI assistant with comprehensive Dynatrace observability analysis ca
56
99
 
57
100
  ### **🚀 Quick Setup for AI Assistants**
58
101
 
59
- Copy the comprehensive rule files from the [`rules/`](./rules/) directory to your AI assistant's rules directory:
102
+ Copy the comprehensive rule files from the [`dynatrace-agent-rules/rules/`](./dynatrace-agent-rules/rules/) directory to your AI assistant's rules directory:
60
103
 
61
104
  **IDE-Specific Locations:**
62
105
 
@@ -139,7 +182,7 @@ rules/
139
182
  - **Eliminated circular references** - No more confusing cross-referencing webs
140
183
  - **DQL-first approach** - Prefer flexible queries over rigid MCP calls
141
184
 
142
- For detailed information about the workshop rules, see the [Rules README](./rules/README.md).
185
+ For detailed information about the workshop rules, see the [Rules README](./dynatrace-agent-rules/rules/README.md).
143
186
 
144
187
  ## Quickstart
145
188
 
@@ -172,8 +215,7 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
172
215
  "command": "npx",
173
216
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
174
217
  "env": {
175
- "OAUTH_CLIENT_ID": "",
176
- "OAUTH_CLIENT_SECRET": "",
218
+ "DT_PLATFORM_TOKEN": "",
177
219
  "DT_ENVIRONMENT": ""
178
220
  }
179
221
  }
@@ -186,12 +228,11 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
186
228
  ```json
187
229
  {
188
230
  "mcpServers": {
189
- "mobile-mcp": {
231
+ "dynatrace-mcp-server": {
190
232
  "command": "npx",
191
233
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
192
234
  "env": {
193
- "OAUTH_CLIENT_ID": "",
194
- "OAUTH_CLIENT_SECRET": "",
235
+ "DT_PLATFORM_TOKEN": "",
195
236
  "DT_ENVIRONMENT": ""
196
237
  }
197
238
  }
@@ -206,12 +247,11 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
206
247
  ```json
207
248
  {
208
249
  "mcpServers": {
209
- "mobile-mcp": {
250
+ "dynatrace-mcp-server": {
210
251
  "command": "npx",
211
252
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
212
253
  "env": {
213
- "OAUTH_CLIENT_ID": "",
214
- "OAUTH_CLIENT_SECRET": "",
254
+ "DT_PLATFORM_TOKEN": "",
215
255
  "DT_ENVIRONMENT": ""
216
256
  }
217
257
  }
@@ -229,34 +269,21 @@ For scenarios where you need to run the MCP server as an HTTP service instead of
229
269
 
230
270
  ```bash
231
271
  # Get help and see all available options
232
- npx -y @dynatrace-oss/dynatrace-mcp-server --help
272
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --help
233
273
 
234
274
  # Run with HTTP server on default port 3000
235
- npx -y @dynatrace-oss/dynatrace-mcp-server --http
275
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http
236
276
 
237
277
  # Run with custom port (using short or long flag)
238
- npx -y @dynatrace-oss/dynatrace-mcp-server --server -p 8080
239
- npx -y @dynatrace-oss/dynatrace-mcp-server --http --port 3001
278
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --server -p 8080
279
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http --port 3001
240
280
 
241
281
  # Run with custom host/IP (using short or long flag)
242
- npx -y @dynatrace-oss/dynatrace-mcp-server --http --host 127.0.0.1
243
- npx -y @dynatrace-oss/dynatrace-mcp-server --http -H 192.168.0.1
282
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http --host 127.0.0.1
283
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http -H 192.168.0.1
244
284
 
245
285
  # Check version
246
- npx -y @dynatrace-oss/dynatrace-mcp-server --version
247
- ```
248
-
249
- **Configuration for MCP clients that support HTTP transport:**
250
-
251
- ```json
252
- {
253
- "mcpServers": {
254
- "dynatrace-http": {
255
- "url": "http://localhost:3000",
256
- "transport": "http"
257
- }
258
- }
259
- }
286
+ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --version
260
287
  ```
261
288
 
262
289
  **Configuration for MCP clients that support HTTP transport:**
@@ -299,16 +326,19 @@ For fetching just error-logs, add `| filter loglevel == "ERROR"`.
299
326
 
300
327
  ## Environment Variables
301
328
 
302
- You can set up authentication via **OAuth Client** or **Platform Tokens** (v0.5.0 and newer) via the following environment variables:
329
+ You can set up authentication via **Platform Tokens** (recommended) or **OAuth Client** via the following environment variables:
303
330
 
304
331
  - `DT_ENVIRONMENT` (string, e.g., https://abc12345.apps.dynatrace.com) - URL to your Dynatrace Platform (do not use Dynatrace classic URLs like `abc12345.live.dynatrace.com`)
305
- - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
306
- - `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
307
- - With v0.5.0 and newer: `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - Dynatrace Platform Token (limited support, as not all scopes are available; see below)
332
+ - `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
333
+ - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Alternative: Dynatrace OAuth Client ID (for advanced use cases)
334
+ - `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Alternative: Dynatrace OAuth Client Secret (for advanced use cases)
335
+ - `DT_GRAIL_QUERY_BUDGET_GB` (number, default: 1000) - Budget limit in GB (base 1000) for Grail query bytes scanned per session. The MCP server tracks your Grail usage and warns when approaching or exceeding this limit.
336
+
337
+ **Platform Tokens are recommended** for most use cases as they provide a simpler authentication flow. OAuth Clients should only be used when specific OAuth features are required.
308
338
 
309
339
  For more information, please have a look at the documentation about
310
- [creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients), as well as
311
- [creating a Platform Token in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/platform-tokens).
340
+ [creating a Platform Token in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/platform-tokens), as well as
341
+ [creating an OAuth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients) for advanced scenarios.
312
342
 
313
343
  In addition, depending on the features you use, the following variables can be configured:
314
344
 
@@ -318,6 +348,8 @@ In addition, depending on the features you use, the following variables can be c
318
348
 
319
349
  Depending on the features you are using, the following scopes are needed:
320
350
 
351
+ **Available for both Platform Tokens and OAuth Clients:**
352
+
321
353
  - `app-engine:apps:run` - needed for almost all tools
322
354
  - `app-engine:functions:run` - needed for for almost all tools
323
355
  - `environment-api:entities:read` - for retrieving ownership details from monitored entities (_currently not available for Platform Tokens_)
@@ -338,10 +370,13 @@ Depending on the features you are using, the following scopes are needed:
338
370
  - `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
339
371
  - `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
340
372
  - `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
373
+ - `email:emails:send` - needed for `send_email` tool to send emails
341
374
  - `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
342
375
 
343
376
  **Note**: Please ensure that `settings:objects:read` is used, and _not_ the similarly named scope `app-settings:objects:read`.
344
377
 
378
+ **Important**: Some features requiring `environment-api:entities:read` will only work with OAuth Clients. For most use cases, Platform Tokens provide all necessary functionality.
379
+
345
380
  ## ✨ Example prompts ✨
346
381
 
347
382
  Use these example prompts as a starting point. Just copy them into your IDE or agent setup, adapt them to your services/stack/architecture,
@@ -368,6 +403,12 @@ fetch logs | filter dt.source_entity == 'SERVICE-123' | summarize count(), by:{s
368
403
  How can I investigate slow database queries in Dynatrace?
369
404
  ```
370
405
 
406
+ **Send email notifications:**
407
+
408
+ ```
409
+ Send an email notification about the incident to the responsible team at team@example.com with CC to manager@example.com
410
+ ```
411
+
371
412
  ### **Advanced Incident Investigation**
372
413
 
373
414
  **Multi-phase incident response:**
@@ -493,12 +534,18 @@ to help identify what might be causing these deployment issues?
493
534
 
494
535
  ### Authentication Issues
495
536
 
496
- In most cases, something is wrong with the OAuth Client. Please ensure that you have added all scopes as requested above.
497
- In addition, please ensure that your user also has all necessary permissions on your Dynatrace Environment.
537
+ In most cases, authentication issues are related to missing scopes or invalid tokens. Please ensure that you have added all required scopes as listed above.
538
+
539
+ **For Platform Tokens:**
540
+
541
+ 1. Verify your Platform Token has all the necessary scopes listed in the "Scopes for Authentication" section
542
+ 2. Ensure your token is valid and not expired
543
+ 3. Check that your user has the required permissions in your Dynatrace Environment
498
544
 
499
- In case of any problems, you can troubleshoot SSO/OAuth issues based on our [Dynatrace Developer Documentation](https://developer.dynatrace.com/develop/access-platform-apis-from-outside/#get-bearer-token-and-call-app-function) and providing the list of scopes.
545
+ **For OAuth Clients:**
546
+ In case of OAuth-related problems, you can troubleshoot SSO/OAuth issues based on our [Dynatrace Developer Documentation](https://developer.dynatrace.com/develop/access-platform-apis-from-outside/#get-bearer-token-and-call-app-function).
500
547
 
501
- It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
548
+ It is recommended to test access with the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
502
549
 
503
550
  1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes):
504
551
 
@@ -534,6 +581,34 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
534
581
 
535
582
  Grail has a dedicated section about permissions in the Dynatrace Docs. Please refer to https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model/assign-permissions-in-grail for more details.
536
583
 
584
+ ## Telemetry
585
+
586
+ The Dynatrace MCP Server includes sending Telemetry Data via Dynatrace OpenKit to help improve the product. This includes:
587
+
588
+ - Server start events
589
+ - Tool usage (which tools are called, success/failure, execution duration)
590
+ - Error tracking for debugging and improvement
591
+
592
+ **Privacy and Opt-out:**
593
+
594
+ - Telemetry is **enabled by default** but can be disabled by setting `DT_MCP_DISABLE_TELEMETRY=true`
595
+ - No sensitive data from your Dynatrace environment is tracked
596
+ - Only anonymous usage statistics and error information are collected
597
+ - Usage statistics and error data are transmitted to Dynatrace’s analytics endpoint
598
+
599
+ **Configuration options:**
600
+
601
+ - `DT_MCP_DISABLE_TELEMETRY` (boolean, default: `false`) - Disable Telemetry
602
+ - `DT_MCP_TELEMETRY_APPLICATION_ID` (string, default: `dynatrace-mcp-server`) - Application ID for tracking
603
+ - `DT_MCP_TELEMETRY_ENDPOINT_URL` (string, default: Dynatrace endpoint) - OpenKit endpoint URL
604
+ - `DT_MCP_TELEMETRY_DEVICE_ID` (string, default: auto-generated) - Device identifier for tracking
605
+
606
+ To disable usage tracking, add this to your environment:
607
+
608
+ ```bash
609
+ DT_MCP_DISABLE_TELEMETRY=true
610
+ ```
611
+
537
612
  ## Development
538
613
 
539
614
  For local development purposes, you can use VSCode and GitHub Copilot.
@@ -581,8 +656,3 @@ This will
581
656
  - prepare the [changelog](CHANGELOG.md),
582
657
  - update the version number in [package.json](package.json),
583
658
  - commit the changes.
584
-
585
- ## Notes
586
-
587
- This product is not officially supported by Dynatrace.
588
- Please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
@@ -10,7 +10,7 @@ const user_agent_1 = require("../utils/user-agent");
10
10
  * @param clientSecret - Oauth Client Secret for Dynatrace
11
11
  * @param ssoAuthUrl - SSO Authentication URL
12
12
  * @param scopes - List of requested scopes
13
- * @returns
13
+ * @returns Response of the OAuth Endpoint (which, in the best case includes a token)
14
14
  */
15
15
  const requestToken = async (clientId, clientSecret, ssoAuthUrl, scopes) => {
16
16
  const res = await fetch(ssoAuthUrl, {
@@ -40,7 +40,7 @@ const requestToken = async (clientId, clientSecret, ssoAuthUrl, scopes) => {
40
40
  * @param clientId
41
41
  * @param clientSecret
42
42
  * @param dtPlatformToken
43
- * @returns
43
+ * @returns an authenticated HttpClient
44
44
  */
45
45
  const createDtHttpClient = async (environmentUrl, scopes, clientId, clientSecret, dtPlatformToken) => {
46
46
  if (clientId && clientSecret) {
@@ -1,7 +1,23 @@
1
1
  "use strict";
2
+ /**
3
+ * Davis CoPilot API Integration
4
+ *
5
+ * This module provides access to Davis CoPilot AI capabilities including:
6
+ * - Natural Language to DQL conversion
7
+ * - DQL explanation in plain English
8
+ * - AI-powered conversation assistance
9
+ * - Feedback submission for continuous improvement
10
+ *
11
+ * Note: While Davis CoPilot AI is generally available (GA),
12
+ * the Davis CoPilot APIs are currently in preview.
13
+ * For more information: https://dt-url.net/copilot-community
14
+ *
15
+ * DQL (Dynatrace Query Language) is the most powerful way to query any data
16
+ * in Dynatrace, including problem events, security issues, logs, metrics, and spans.
17
+ */
2
18
  Object.defineProperty(exports, "__esModule", { value: true });
3
19
  exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.generateDqlFromNaturalLanguage = void 0;
4
- // API Functions
20
+ const client_davis_copilot_1 = require("@dynatrace-sdk/client-davis-copilot");
5
21
  /**
6
22
  * Generate DQL from natural language
7
23
  * Converts plain English descriptions into powerful Dynatrace Query Language (DQL) statements.
@@ -9,17 +25,10 @@ exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.gen
9
25
  * security issues, logs, metrics, spans, and custom data.
10
26
  */
11
27
  const generateDqlFromNaturalLanguage = async (dtClient, text) => {
12
- const request = { text };
13
- const response = await dtClient.send({
14
- method: 'POST',
15
- url: '/platform/davis/copilot/v0.2/skills/nl2dql:generate',
16
- headers: {
17
- 'Content-Type': 'application/json',
18
- 'Accept': 'application/json',
19
- },
20
- body: JSON.stringify(request),
28
+ const client = new client_davis_copilot_1.PublicClient(dtClient);
29
+ return await client.nl2dql({
30
+ body: { text },
21
31
  });
22
- return await response.body('json');
23
32
  };
24
33
  exports.generateDqlFromNaturalLanguage = generateDqlFromNaturalLanguage;
25
34
  /**
@@ -29,35 +38,27 @@ exports.generateDqlFromNaturalLanguage = generateDqlFromNaturalLanguage;
29
38
  * queries for problem events, security issues, and performance metrics.
30
39
  */
31
40
  const explainDqlInNaturalLanguage = async (dtClient, dql) => {
32
- const request = { dql };
33
- const response = await dtClient.send({
34
- method: 'POST',
35
- url: '/platform/davis/copilot/v0.2/skills/dql2nl:explain',
36
- headers: {
37
- 'Content-Type': 'application/json',
38
- 'Accept': 'application/json',
39
- },
40
- body: request, // Not sure why this does not need JSON.stringify, but it only works like this; once we have the SDK, this will be consistent
41
+ const client = new client_davis_copilot_1.PublicClient(dtClient);
42
+ return await client.dql2nl({
43
+ body: { dql },
41
44
  });
42
- return await response.body('json');
43
45
  };
44
46
  exports.explainDqlInNaturalLanguage = explainDqlInNaturalLanguage;
45
47
  const chatWithDavisCopilot = async (dtClient, text, context, annotations, state) => {
46
- const request = {
47
- text,
48
- context,
49
- annotations,
50
- state,
51
- };
52
- const response = await dtClient.send({
53
- method: 'POST',
54
- url: '/platform/davis/copilot/v0.2/skills/conversations:message',
55
- headers: {
56
- 'Content-Type': 'application/json',
57
- 'Accept': 'application/json',
48
+ const client = new client_davis_copilot_1.PublicClient(dtClient);
49
+ const response = await client.recommenderConversation({
50
+ body: {
51
+ text,
52
+ context,
53
+ annotations,
54
+ state,
58
55
  },
59
- body: JSON.stringify(request),
60
56
  });
61
- return await response.body('json');
57
+ // Type guard: RecommenderResponse is ConversationResponse | EventArray
58
+ // In practice, the SDK defaults to non-streaming and returns ConversationResponse
59
+ if (Array.isArray(response)) {
60
+ throw new Error('Unexpected streaming response format. Please raise an issue at https://github.com/dynatrace-oss/dynatrace-mcp/issues.');
61
+ }
62
+ return response;
62
63
  };
63
64
  exports.chatWithDavisCopilot = chatWithDavisCopilot;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeDql = exports.verifyDqlStatement = void 0;
4
4
  const client_query_1 = require("@dynatrace-sdk/client-query");
5
5
  const user_agent_1 = require("../utils/user-agent");
6
+ const grail_budget_tracker_1 = require("../utils/grail-budget-tracker");
6
7
  const verifyDqlStatement = async (dtClient, dqlStatement) => {
7
8
  const queryAssistanceClient = new client_query_1.QueryAssistanceClient(dtClient);
8
9
  const response = await queryAssistanceClient.queryVerify({
@@ -13,25 +14,71 @@ const verifyDqlStatement = async (dtClient, dqlStatement) => {
13
14
  return response;
14
15
  };
15
16
  exports.verifyDqlStatement = verifyDqlStatement;
17
+ /**
18
+ * Helper function to create a DQL execution result and log metadata information.
19
+ * @param queryResult - The query result from Dynatrace API
20
+ * @param logPrefix - Prefix for the log message (e.g., "DQL Execution Metadata" or "DQL Execution Metadata (Polled)")
21
+ * @param budgetLimitGB - Budget limit in GB for tracking purposes
22
+ * @returns DqlExecutionResult with extracted metadata
23
+ */
24
+ const createResultAndLog = (queryResult, logPrefix, budgetLimitGB) => {
25
+ const scannedBytes = queryResult.metadata?.grail?.scannedBytes || 0;
26
+ // Track budget if limit is provided
27
+ let budgetState;
28
+ let budgetWarning;
29
+ if (budgetLimitGB !== undefined) {
30
+ const tracker = (0, grail_budget_tracker_1.getGrailBudgetTracker)(budgetLimitGB);
31
+ budgetState = tracker.addBytesScanned(scannedBytes);
32
+ budgetWarning = (0, grail_budget_tracker_1.generateBudgetWarning)(budgetState, scannedBytes) || undefined;
33
+ }
34
+ const result = {
35
+ records: queryResult.records,
36
+ metadata: queryResult.metadata,
37
+ scannedBytes,
38
+ scannedRecords: queryResult.metadata?.grail?.scannedRecords,
39
+ executionTimeMilliseconds: queryResult.metadata?.grail?.executionTimeMilliseconds,
40
+ queryId: queryResult.metadata?.grail?.queryId,
41
+ sampled: queryResult.metadata?.grail?.sampled,
42
+ budgetState,
43
+ budgetWarning,
44
+ };
45
+ console.error(`${logPrefix} scannedBytes=${result.scannedBytes} scannedRecords=${result.scannedRecords} executionTime=${result.executionTimeMilliseconds} queryId=${result.queryId}`);
46
+ return result;
47
+ };
16
48
  /**
17
49
  * Execute a DQL statement against the Dynatrace API.
18
50
  * If the result is immediately available, it will be returned.
19
51
  * If the result is not immediately available, it will poll for the result until it is available.
20
52
  * @param dtClient
21
53
  * @param body - Contains the DQL statement to execute, and optional parameters like maxResultRecords and maxResultBytes
22
- * @returns the result without metadata and without notifications, or undefined if the query failed or no result was returned.
54
+ * @param budgetLimitGB - Optional budget limit in GB for tracking bytes scanned
55
+ * @returns the result with records, metadata and cost information, or undefined if the query failed or no result was returned.
23
56
  */
24
- const executeDql = async (dtClient, body) => {
57
+ const executeDql = async (dtClient, body, budgetLimitGB) => {
58
+ // Check budget before executing the query if budget limit is provided
59
+ if (budgetLimitGB !== undefined) {
60
+ const tracker = (0, grail_budget_tracker_1.getGrailBudgetTracker)(budgetLimitGB);
61
+ const currentState = tracker.getState();
62
+ if (currentState.isBudgetExceeded) {
63
+ console.error('DQL execution aborted: Grail budget has been exceeded');
64
+ const budgetWarning = (0, grail_budget_tracker_1.generateBudgetWarning)(currentState, 0);
65
+ throw new Error(budgetWarning || 'DQL execution aborted: Grail budget has been exceeded');
66
+ }
67
+ }
68
+ // create a Dynatrace QueryExecutionClient
25
69
  const queryExecutionClient = new client_query_1.QueryExecutionClient(dtClient);
70
+ // and execute the query (part of body)
26
71
  const response = await queryExecutionClient.queryExecute({
27
72
  body,
73
+ // define a dedicated user agent to enable tracking of DQL queries executed by the dynatrace-mcp-server
28
74
  dtClientContext: (0, user_agent_1.getUserAgent)(),
29
75
  });
76
+ // check if we already got a result back
30
77
  if (response.result) {
31
- // return response result immediately
32
- return response.result.records;
78
+ // yes - return response result immediately
79
+ return createResultAndLog(response.result, 'execute_dql - Metadata:', budgetLimitGB);
33
80
  }
34
- // else: We might have to poll
81
+ // no result yet? we have wait and poll (this requires requestToken to be set)
35
82
  if (response.requestToken) {
36
83
  // poll for the result
37
84
  let pollResponse;
@@ -42,13 +89,18 @@ const executeDql = async (dtClient, body) => {
42
89
  requestToken: response.requestToken,
43
90
  dtClientContext: (0, user_agent_1.getUserAgent)(),
44
91
  });
45
- // done - let's return it
92
+ // check if we got a result from the polling endpoint
46
93
  if (pollResponse.result) {
47
- return pollResponse.result.records;
94
+ // yes - let's return the polled result
95
+ return createResultAndLog(pollResponse.result, 'execute_dql Metadata (polled):', budgetLimitGB);
48
96
  }
49
97
  } while (pollResponse.state === 'RUNNING' || pollResponse.state === 'NOT_STARTED');
98
+ // state != RUNNING and != NOT_STARTED - we should log that
99
+ console.error(`execute_dql with requestToken ${response.requestToken} ended with state ${pollResponse.state}, stopping...`);
100
+ return undefined;
50
101
  }
51
- // else: whatever happened - we have an error
102
+ // no requestToken set? This should not happen, but just in case, let's log it
103
+ console.error(`execute_dql did not respond with a requestToken, stopping...`);
52
104
  return undefined;
53
105
  };
54
106
  exports.executeDql = executeDql;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const execute_dql_1 = require("./execute-dql");
4
+ const client_query_1 = require("@dynatrace-sdk/client-query");
5
+ const grail_budget_tracker_1 = require("../utils/grail-budget-tracker");
6
+ // Mock the external dependencies
7
+ jest.mock('@dynatrace-sdk/client-query');
8
+ jest.mock('../utils/user-agent', () => ({
9
+ getUserAgent: () => 'test-user-agent',
10
+ }));
11
+ describe('executeDql Budget Check', () => {
12
+ let mockHttpClient;
13
+ let mockQueryExecutionClient;
14
+ beforeEach(() => {
15
+ // Reset budget tracker before each test
16
+ (0, grail_budget_tracker_1.resetGrailBudgetTracker)();
17
+ // Create mock HTTP client
18
+ mockHttpClient = {
19
+ // Add any necessary properties/methods for HttpClient mock
20
+ };
21
+ // Create mock QueryExecutionClient
22
+ mockQueryExecutionClient = {
23
+ queryExecute: jest.fn(),
24
+ queryPoll: jest.fn(),
25
+ queryCancel: jest.fn(),
26
+ };
27
+ // Mock the QueryExecutionClient constructor
28
+ client_query_1.QueryExecutionClient.mockImplementation(() => mockQueryExecutionClient);
29
+ });
30
+ afterEach(() => {
31
+ jest.clearAllMocks();
32
+ (0, grail_budget_tracker_1.resetGrailBudgetTracker)();
33
+ });
34
+ it('should prevent execution when budget is exceeded', async () => {
35
+ const budgetLimitGB = 0.001; // Very small budget limit (1 MB)
36
+ // First, exhaust the budget by adding bytes to tracker
37
+ const tracker = (0, grail_budget_tracker_1.getGrailBudgetTracker)(budgetLimitGB);
38
+ tracker.addBytesScanned(2 * 1000 * 1000); // Add 2 MB, exceeding the 1 MB limit
39
+ const dqlStatement = 'fetch logs | limit 10';
40
+ const body = { query: dqlStatement };
41
+ // Execute DQL with budget limit and expect it to throw
42
+ await expect((0, execute_dql_1.executeDql)(mockHttpClient, body, budgetLimitGB)).rejects.toThrow(/budget/);
43
+ // Verify that queryExecute was NOT called
44
+ expect(mockQueryExecutionClient.queryExecute).not.toHaveBeenCalled();
45
+ });
46
+ it('should allow execution when budget is not exceeded', async () => {
47
+ const budgetLimitGB = 1; // 1 GB budget limit
48
+ const dqlStatement = 'fetch logs | limit 10';
49
+ const body = { query: dqlStatement };
50
+ // Mock successful response
51
+ const mockResponse = {
52
+ state: 'RUNNING',
53
+ result: {
54
+ records: [{ field1: 'value1' }],
55
+ types: [],
56
+ metadata: {
57
+ grail: {
58
+ scannedBytes: 1000,
59
+ scannedRecords: 1,
60
+ executionTimeMilliseconds: 100,
61
+ queryId: 'test-query-id',
62
+ },
63
+ },
64
+ },
65
+ };
66
+ mockQueryExecutionClient.queryExecute.mockResolvedValue(mockResponse);
67
+ // Execute DQL with budget limit
68
+ const result = await (0, execute_dql_1.executeDql)(mockHttpClient, body, budgetLimitGB);
69
+ // Verify that queryExecute WAS called
70
+ expect(mockQueryExecutionClient.queryExecute).toHaveBeenCalledWith({
71
+ body,
72
+ dtClientContext: 'test-user-agent',
73
+ });
74
+ // Verify the result is returned correctly
75
+ expect(result).toBeDefined();
76
+ expect(result?.records).toEqual([{ field1: 'value1' }]);
77
+ expect(result?.scannedBytes).toBe(1000);
78
+ expect(result?.budgetState?.isBudgetExceeded).toBe(false);
79
+ });
80
+ it('should allow execution when no budget limit is provided', async () => {
81
+ const dqlStatement = 'fetch logs | limit 10';
82
+ const body = { query: dqlStatement };
83
+ // Mock successful response
84
+ const mockResponse = {
85
+ state: 'RUNNING',
86
+ result: {
87
+ records: [{ field1: 'value1' }],
88
+ types: [],
89
+ metadata: {
90
+ grail: {
91
+ scannedBytes: 1000000000, // 1 GB - would exceed small budgets
92
+ scannedRecords: 1000,
93
+ executionTimeMilliseconds: 100,
94
+ queryId: 'test-query-id',
95
+ },
96
+ },
97
+ },
98
+ };
99
+ mockQueryExecutionClient.queryExecute.mockResolvedValue(mockResponse);
100
+ // Execute DQL without budget limit
101
+ const result = await (0, execute_dql_1.executeDql)(mockHttpClient, body);
102
+ // Verify that queryExecute WAS called
103
+ expect(mockQueryExecutionClient.queryExecute).toHaveBeenCalledWith({
104
+ body,
105
+ dtClientContext: 'test-user-agent',
106
+ });
107
+ // Verify the result is returned correctly
108
+ expect(result).toBeDefined();
109
+ expect(result?.records).toEqual([{ field1: 'value1' }]);
110
+ expect(result?.scannedBytes).toBe(1000000000);
111
+ expect(result?.budgetState).toBeUndefined(); // No budget tracking
112
+ });
113
+ });
@@ -35,10 +35,10 @@ const findMonitoredEntityByName = async (dtClient, entityName) => {
35
35
  // Get response from API
36
36
  // Note: This may be slow, as we are appending multiple entity types above
37
37
  const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
38
- if (dqlResponse && dqlResponse.length > 0) {
38
+ if (dqlResponse && dqlResponse.records && dqlResponse.records.length > 0) {
39
39
  let resp = 'The following monitored entities were found:\n';
40
40
  // iterate over dqlResponse and create a string with the entity names
41
- dqlResponse.forEach((entity) => {
41
+ dqlResponse.records.forEach((entity) => {
42
42
  if (entity) {
43
43
  resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']} has entity id '${entity.id}'\n`;
44
44
  }