@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 +120 -50
- package/dist/authentication/dynatrace-clients.js +2 -2
- package/dist/capabilities/davis-copilot.js +36 -35
- package/dist/capabilities/execute-dql.js +60 -8
- package/dist/capabilities/execute-dql.test.js +113 -0
- package/dist/capabilities/find-monitored-entity-by-name.js +2 -2
- package/dist/capabilities/get-monitored-entity-details.js +5 -4
- package/dist/capabilities/list-vulnerabilities.js +2 -2
- package/dist/capabilities/send-email.js +62 -0
- package/dist/getDynatraceEnv.js +6 -1
- package/dist/getDynatraceEnv.test.js +1 -0
- package/dist/index.js +230 -47
- package/dist/utils/grail-budget-tracker.js +139 -0
- package/dist/utils/grail-budget-tracker.test.js +182 -0
- package/dist/utils/telemetry-openkit.js +223 -0
- package/dist/utils/user-agent.js +2 -2
- package/dist/utils/version.js +12 -0
- package/package.json +4 -1
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
52
|
+
### Costs
|
|
31
53
|
|
|
32
|
-
**Important:** While this local MCP server is provided for free, using
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
231
|
+
"dynatrace-mcp-server": {
|
|
190
232
|
"command": "npx",
|
|
191
233
|
"args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
|
|
192
234
|
"env": {
|
|
193
|
-
"
|
|
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
|
-
"
|
|
250
|
+
"dynatrace-mcp-server": {
|
|
210
251
|
"command": "npx",
|
|
211
252
|
"args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
|
|
212
253
|
"env": {
|
|
213
|
-
"
|
|
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 **
|
|
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
|
-
- `
|
|
306
|
-
- `
|
|
307
|
-
-
|
|
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
|
|
311
|
-
[creating
|
|
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,
|
|
497
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
|
78
|
+
// yes - return response result immediately
|
|
79
|
+
return createResultAndLog(response.result, 'execute_dql - Metadata:', budgetLimitGB);
|
|
33
80
|
}
|
|
34
|
-
//
|
|
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
|
-
//
|
|
92
|
+
// check if we got a result from the polling endpoint
|
|
46
93
|
if (pollResponse.result) {
|
|
47
|
-
return
|
|
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
|
-
//
|
|
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
|
}
|