@dynatrace-oss/dynatrace-mcp-server 0.5.0 → 0.6.0-rc.1
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 +100 -36
- package/dist/authentication/dynatrace-clients.js +2 -2
- package/dist/capabilities/execute-dql.js +35 -7
- 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/index.js +115 -24
- 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 +3 -1
package/README.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
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
|
|
|
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
|
+
|
|
6
28
|
<img width="1046" alt="image" src="/assets/dynatrace-mcp-arch.png" />
|
|
7
29
|
|
|
8
30
|
## Use cases
|
|
@@ -27,11 +49,11 @@ 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
|
|
|
@@ -39,7 +61,17 @@ depend on the volume (GB scanned/billed).
|
|
|
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
|
|
41
63
|
|
|
42
|
-
**
|
|
64
|
+
**To understand costs that occured:**
|
|
65
|
+
|
|
66
|
+
Execute the following DQL statement in a notebook to see how much bytes have been queried from Grail (Logs, Events, etc...):
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
fetch dt.system.events
|
|
70
|
+
| filter event.kind == "QUERY_EXECUTION_EVENT" and contains(client.client_context, "dynatrace-mcp")
|
|
71
|
+
| sort timestamp desc
|
|
72
|
+
| fields timestamp, query_id, query_string, scanned_bytes, table, bucket, user.id, user.email, client.client_context
|
|
73
|
+
| maketimeSeries sum(scanned_bytes), by: { user.email, user.id, table }
|
|
74
|
+
```
|
|
43
75
|
|
|
44
76
|
### AI-Powered Assistance (Preview)
|
|
45
77
|
|
|
@@ -56,7 +88,7 @@ Enhance your AI assistant with comprehensive Dynatrace observability analysis ca
|
|
|
56
88
|
|
|
57
89
|
### **🚀 Quick Setup for AI Assistants**
|
|
58
90
|
|
|
59
|
-
Copy the comprehensive rule files from the [`rules/`](./rules/) directory to your AI assistant's rules directory:
|
|
91
|
+
Copy the comprehensive rule files from the [`dynatrace-agent-rules/rules/`](./dynatrace-agent-rules/rules/) directory to your AI assistant's rules directory:
|
|
60
92
|
|
|
61
93
|
**IDE-Specific Locations:**
|
|
62
94
|
|
|
@@ -139,7 +171,7 @@ rules/
|
|
|
139
171
|
- **Eliminated circular references** - No more confusing cross-referencing webs
|
|
140
172
|
- **DQL-first approach** - Prefer flexible queries over rigid MCP calls
|
|
141
173
|
|
|
142
|
-
For detailed information about the workshop rules, see the [Rules README](./rules/README.md).
|
|
174
|
+
For detailed information about the workshop rules, see the [Rules README](./dynatrace-agent-rules/rules/README.md).
|
|
143
175
|
|
|
144
176
|
## Quickstart
|
|
145
177
|
|
|
@@ -172,8 +204,7 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
|
|
|
172
204
|
"command": "npx",
|
|
173
205
|
"args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
|
|
174
206
|
"env": {
|
|
175
|
-
"
|
|
176
|
-
"OAUTH_CLIENT_SECRET": "",
|
|
207
|
+
"DT_PLATFORM_TOKEN": "",
|
|
177
208
|
"DT_ENVIRONMENT": ""
|
|
178
209
|
}
|
|
179
210
|
}
|
|
@@ -186,12 +217,11 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
|
|
|
186
217
|
```json
|
|
187
218
|
{
|
|
188
219
|
"mcpServers": {
|
|
189
|
-
"
|
|
220
|
+
"dynatrace-mcp-server": {
|
|
190
221
|
"command": "npx",
|
|
191
222
|
"args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
|
|
192
223
|
"env": {
|
|
193
|
-
"
|
|
194
|
-
"OAUTH_CLIENT_SECRET": "",
|
|
224
|
+
"DT_PLATFORM_TOKEN": "",
|
|
195
225
|
"DT_ENVIRONMENT": ""
|
|
196
226
|
}
|
|
197
227
|
}
|
|
@@ -206,12 +236,11 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
|
|
|
206
236
|
```json
|
|
207
237
|
{
|
|
208
238
|
"mcpServers": {
|
|
209
|
-
"
|
|
239
|
+
"dynatrace-mcp-server": {
|
|
210
240
|
"command": "npx",
|
|
211
241
|
"args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
|
|
212
242
|
"env": {
|
|
213
|
-
"
|
|
214
|
-
"OAUTH_CLIENT_SECRET": "",
|
|
243
|
+
"DT_PLATFORM_TOKEN": "",
|
|
215
244
|
"DT_ENVIRONMENT": ""
|
|
216
245
|
}
|
|
217
246
|
}
|
|
@@ -229,21 +258,21 @@ For scenarios where you need to run the MCP server as an HTTP service instead of
|
|
|
229
258
|
|
|
230
259
|
```bash
|
|
231
260
|
# Get help and see all available options
|
|
232
|
-
npx -y @dynatrace-oss/dynatrace-mcp-server --help
|
|
261
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --help
|
|
233
262
|
|
|
234
263
|
# Run with HTTP server on default port 3000
|
|
235
|
-
npx -y @dynatrace-oss/dynatrace-mcp-server --http
|
|
264
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http
|
|
236
265
|
|
|
237
266
|
# 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
|
|
267
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --server -p 8080
|
|
268
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http --port 3001
|
|
240
269
|
|
|
241
270
|
# 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
|
|
271
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http --host 127.0.0.1
|
|
272
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --http -H 192.168.0.1
|
|
244
273
|
|
|
245
274
|
# Check version
|
|
246
|
-
npx -y @dynatrace-oss/dynatrace-mcp-server --version
|
|
275
|
+
npx -y @dynatrace-oss/dynatrace-mcp-server@latest --version
|
|
247
276
|
```
|
|
248
277
|
|
|
249
278
|
**Configuration for MCP clients that support HTTP transport:**
|
|
@@ -299,16 +328,18 @@ For fetching just error-logs, add `| filter loglevel == "ERROR"`.
|
|
|
299
328
|
|
|
300
329
|
## Environment Variables
|
|
301
330
|
|
|
302
|
-
You can set up authentication via **
|
|
331
|
+
You can set up authentication via **Platform Tokens** (recommended) or **OAuth Client** via the following environment variables:
|
|
303
332
|
|
|
304
333
|
- `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
|
-
-
|
|
334
|
+
- `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
|
|
335
|
+
- `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Alternative: Dynatrace OAuth Client ID (for advanced use cases)
|
|
336
|
+
- `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Alternative: Dynatrace OAuth Client Secret (for advanced use cases)
|
|
337
|
+
|
|
338
|
+
**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
339
|
|
|
309
340
|
For more information, please have a look at the documentation about
|
|
310
|
-
[creating
|
|
311
|
-
[creating
|
|
341
|
+
[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
|
|
342
|
+
[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
343
|
|
|
313
344
|
In addition, depending on the features you use, the following variables can be configured:
|
|
314
345
|
|
|
@@ -318,6 +349,8 @@ In addition, depending on the features you use, the following variables can be c
|
|
|
318
349
|
|
|
319
350
|
Depending on the features you are using, the following scopes are needed:
|
|
320
351
|
|
|
352
|
+
**Available for both Platform Tokens and OAuth Clients:**
|
|
353
|
+
|
|
321
354
|
- `app-engine:apps:run` - needed for almost all tools
|
|
322
355
|
- `app-engine:functions:run` - needed for for almost all tools
|
|
323
356
|
- `environment-api:entities:read` - for retrieving ownership details from monitored entities (_currently not available for Platform Tokens_)
|
|
@@ -342,6 +375,8 @@ Depending on the features you are using, the following scopes are needed:
|
|
|
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,
|
|
@@ -493,12 +528,18 @@ to help identify what might be causing these deployment issues?
|
|
|
493
528
|
|
|
494
529
|
### Authentication Issues
|
|
495
530
|
|
|
496
|
-
In most cases,
|
|
497
|
-
In addition, please ensure that your user also has all necessary permissions on your Dynatrace Environment.
|
|
531
|
+
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.
|
|
498
532
|
|
|
499
|
-
|
|
533
|
+
**For Platform Tokens:**
|
|
500
534
|
|
|
501
|
-
|
|
535
|
+
1. Verify your Platform Token has all the necessary scopes listed in the "Scopes for Authentication" section
|
|
536
|
+
2. Ensure your token is valid and not expired
|
|
537
|
+
3. Check that your user has the required permissions in your Dynatrace Environment
|
|
538
|
+
|
|
539
|
+
**For OAuth Clients:**
|
|
540
|
+
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).
|
|
541
|
+
|
|
542
|
+
It is recommended to test access with the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
|
|
502
543
|
|
|
503
544
|
1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes):
|
|
504
545
|
|
|
@@ -534,6 +575,34 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
|
|
|
534
575
|
|
|
535
576
|
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
577
|
|
|
578
|
+
## Telemetry
|
|
579
|
+
|
|
580
|
+
The Dynatrace MCP Server includes sending Telemetry Data via Dynatrace OpenKit to help improve the product. This includes:
|
|
581
|
+
|
|
582
|
+
- Server start events
|
|
583
|
+
- Tool usage (which tools are called, success/failure, execution duration)
|
|
584
|
+
- Error tracking for debugging and improvement
|
|
585
|
+
|
|
586
|
+
**Privacy and Opt-out:**
|
|
587
|
+
|
|
588
|
+
- Telemetry is **enabled by default** but can be disabled by setting `DT_MCP_DISABLE_TELEMETRY=true`
|
|
589
|
+
- No sensitive data from your Dynatrace environment is tracked
|
|
590
|
+
- Only anonymous usage statistics and error information are collected
|
|
591
|
+
- Usage statistics and error data are transmitted to Dynatrace’s analytics endpoint
|
|
592
|
+
|
|
593
|
+
**Configuration options:**
|
|
594
|
+
|
|
595
|
+
- `DT_MCP_DISABLE_TELEMETRY` (boolean, default: `false`) - Disable Telemetry
|
|
596
|
+
- `DT_MCP_TELEMETRY_APPLICATION_ID` (string, default: `dynatrace-mcp-server`) - Application ID for tracking
|
|
597
|
+
- `DT_MCP_TELEMETRY_ENDPOINT_URL` (string, default: Dynatrace endpoint) - OpenKit endpoint URL
|
|
598
|
+
- `DT_MCP_TELEMETRY_DEVICE_ID` (string, default: auto-generated) - Device identifier for tracking
|
|
599
|
+
|
|
600
|
+
To disable usage tracking, add this to your environment:
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
DT_MCP_DISABLE_TELEMETRY=true
|
|
604
|
+
```
|
|
605
|
+
|
|
537
606
|
## Development
|
|
538
607
|
|
|
539
608
|
For local development purposes, you can use VSCode and GitHub Copilot.
|
|
@@ -581,8 +650,3 @@ This will
|
|
|
581
650
|
- prepare the [changelog](CHANGELOG.md),
|
|
582
651
|
- update the version number in [package.json](package.json),
|
|
583
652
|
- 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) {
|
|
@@ -13,25 +13,48 @@ const verifyDqlStatement = async (dtClient, dqlStatement) => {
|
|
|
13
13
|
return response;
|
|
14
14
|
};
|
|
15
15
|
exports.verifyDqlStatement = verifyDqlStatement;
|
|
16
|
+
/**
|
|
17
|
+
* Helper function to create a DQL execution result and log metadata information.
|
|
18
|
+
* @param queryResult - The query result from Dynatrace API
|
|
19
|
+
* @param logPrefix - Prefix for the log message (e.g., "DQL Execution Metadata" or "DQL Execution Metadata (Polled)")
|
|
20
|
+
* @returns DqlExecutionResult with extracted metadata
|
|
21
|
+
*/
|
|
22
|
+
const createResultAndLog = (queryResult, logPrefix) => {
|
|
23
|
+
const result = {
|
|
24
|
+
records: queryResult.records,
|
|
25
|
+
metadata: queryResult.metadata,
|
|
26
|
+
scannedBytes: queryResult.metadata?.grail?.scannedBytes,
|
|
27
|
+
scannedRecords: queryResult.metadata?.grail?.scannedRecords,
|
|
28
|
+
executionTimeMilliseconds: queryResult.metadata?.grail?.executionTimeMilliseconds,
|
|
29
|
+
queryId: queryResult.metadata?.grail?.queryId,
|
|
30
|
+
sampled: queryResult.metadata?.grail?.sampled,
|
|
31
|
+
};
|
|
32
|
+
console.error(`${logPrefix} scannedBytes=${result.scannedBytes} scannedRecords=${result.scannedRecords} executionTime=${result.executionTimeMilliseconds} queryId=${result.queryId}`);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
16
35
|
/**
|
|
17
36
|
* Execute a DQL statement against the Dynatrace API.
|
|
18
37
|
* If the result is immediately available, it will be returned.
|
|
19
38
|
* If the result is not immediately available, it will poll for the result until it is available.
|
|
20
39
|
* @param dtClient
|
|
21
40
|
* @param body - Contains the DQL statement to execute, and optional parameters like maxResultRecords and maxResultBytes
|
|
22
|
-
* @returns the result
|
|
41
|
+
* @returns the result with records, metadata and cost information, or undefined if the query failed or no result was returned.
|
|
23
42
|
*/
|
|
24
43
|
const executeDql = async (dtClient, body) => {
|
|
44
|
+
// create a Dynatrace QueryExecutionClient
|
|
25
45
|
const queryExecutionClient = new client_query_1.QueryExecutionClient(dtClient);
|
|
46
|
+
// and execute the query (part of body)
|
|
26
47
|
const response = await queryExecutionClient.queryExecute({
|
|
27
48
|
body,
|
|
49
|
+
// define a dedicated user agent to enable tracking of DQL queries executed by the dynatrace-mcp-server
|
|
28
50
|
dtClientContext: (0, user_agent_1.getUserAgent)(),
|
|
29
51
|
});
|
|
52
|
+
// check if we already got a result back
|
|
30
53
|
if (response.result) {
|
|
31
|
-
// return response result immediately
|
|
32
|
-
return response.result
|
|
54
|
+
// yes - return response result immediately
|
|
55
|
+
return createResultAndLog(response.result, 'execute_dql - Metadata:');
|
|
33
56
|
}
|
|
34
|
-
//
|
|
57
|
+
// no result yet? we have wait and poll (this requires requestToken to be set)
|
|
35
58
|
if (response.requestToken) {
|
|
36
59
|
// poll for the result
|
|
37
60
|
let pollResponse;
|
|
@@ -42,13 +65,18 @@ const executeDql = async (dtClient, body) => {
|
|
|
42
65
|
requestToken: response.requestToken,
|
|
43
66
|
dtClientContext: (0, user_agent_1.getUserAgent)(),
|
|
44
67
|
});
|
|
45
|
-
//
|
|
68
|
+
// check if we got a result from the polling endpoint
|
|
46
69
|
if (pollResponse.result) {
|
|
47
|
-
return
|
|
70
|
+
// yes - let's return the polled result
|
|
71
|
+
return createResultAndLog(pollResponse.result, 'execute_dql Metadata (polled):');
|
|
48
72
|
}
|
|
49
73
|
} while (pollResponse.state === 'RUNNING' || pollResponse.state === 'NOT_STARTED');
|
|
74
|
+
// state != RUNNING and != NOT_STARTED - we should log that
|
|
75
|
+
console.error(`execute_dql with requestToken ${response.requestToken} ended with state ${pollResponse.state}, stopping...`);
|
|
76
|
+
return undefined;
|
|
50
77
|
}
|
|
51
|
-
//
|
|
78
|
+
// no requestToken set? This should not happen, but just in case, let's log it
|
|
79
|
+
console.error(`execute_dql did not respond with a requestToken, stopping...`);
|
|
52
80
|
return undefined;
|
|
53
81
|
};
|
|
54
82
|
exports.executeDql = executeDql;
|
|
@@ -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
|
}
|
|
@@ -21,15 +21,15 @@ const getMonitoredEntityDetails = async (dtClient, entityId) => {
|
|
|
21
21
|
// Get response from API
|
|
22
22
|
const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
|
|
23
23
|
// verify response and length
|
|
24
|
-
if (!dqlResponse || dqlResponse.length === 0) {
|
|
24
|
+
if (!dqlResponse || !dqlResponse.records || dqlResponse.records.length === 0) {
|
|
25
25
|
console.error(`No entity found for ID: ${entityId}`);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
// in case we have more than one entity -> log it
|
|
29
|
-
if (dqlResponse.length > 1) {
|
|
30
|
-
console.error(`Multiple entities (${dqlResponse.length}) found for entity ID: ${entityId}. Returning the first one.`);
|
|
29
|
+
if (dqlResponse.records.length > 1) {
|
|
30
|
+
console.error(`Multiple entities (${dqlResponse.records.length}) found for entity ID: ${entityId}. Returning the first one.`);
|
|
31
31
|
}
|
|
32
|
-
const entity = dqlResponse[0];
|
|
32
|
+
const entity = dqlResponse.records[0];
|
|
33
33
|
// make typescript happy; entity should never be null though
|
|
34
34
|
if (!entity) {
|
|
35
35
|
console.error(`No entity found for ID: ${entityId}`);
|
|
@@ -38,6 +38,7 @@ const getMonitoredEntityDetails = async (dtClient, entityId) => {
|
|
|
38
38
|
// return entity details
|
|
39
39
|
return {
|
|
40
40
|
entityId: String(entity.id),
|
|
41
|
+
entityTypeTable: entityType,
|
|
41
42
|
displayName: String(entity['entity.name']),
|
|
42
43
|
type: String(entity['entity.type']),
|
|
43
44
|
allProperties: entity || undefined,
|
|
@@ -22,10 +22,10 @@ const listVulnerabilities = async (dtClient, additionalFilter, riskScore) => {
|
|
|
22
22
|
maxResultRecords: 5000,
|
|
23
23
|
maxResultBytes: 5_000_000, // 5 MB
|
|
24
24
|
});
|
|
25
|
-
if (!response || response.length === 0) {
|
|
25
|
+
if (!response || !response.records || response.records.length === 0) {
|
|
26
26
|
return [];
|
|
27
27
|
}
|
|
28
|
-
const vulnerabilities = response.map((vuln) => {
|
|
28
|
+
const vulnerabilities = response.records.map((vuln) => {
|
|
29
29
|
const vulnerabilityId = vuln['vulnerability.id'] || 'N/A';
|
|
30
30
|
const vulnerabilityDisplayId = vuln['vulnerability.display_id'] || 'N/A';
|
|
31
31
|
const riskScore = vuln['vulnerability.risk.score'] || 'N/A';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const node_http_1 = require("node:http");
|
|
|
11
11
|
const node_crypto_1 = require("node:crypto");
|
|
12
12
|
const commander_1 = require("commander");
|
|
13
13
|
const zod_1 = require("zod");
|
|
14
|
-
const
|
|
14
|
+
const version_1 = require("./utils/version");
|
|
15
15
|
const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
|
|
16
16
|
const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
|
|
17
17
|
const list_problems_1 = require("./capabilities/list-problems");
|
|
@@ -25,6 +25,7 @@ const send_slack_message_1 = require("./capabilities/send-slack-message");
|
|
|
25
25
|
const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-entity-by-name");
|
|
26
26
|
const davis_copilot_1 = require("./capabilities/davis-copilot");
|
|
27
27
|
const getDynatraceEnv_1 = require("./getDynatraceEnv");
|
|
28
|
+
const telemetry_openkit_1 = require("./utils/telemetry-openkit");
|
|
28
29
|
(0, dotenv_1.config)();
|
|
29
30
|
let scopesBase = [
|
|
30
31
|
'app-engine:apps:run', // needed for environmentInformationClient
|
|
@@ -58,7 +59,7 @@ const main = async () => {
|
|
|
58
59
|
console.error(err.message);
|
|
59
60
|
process.exit(1);
|
|
60
61
|
}
|
|
61
|
-
console.error(`Initializing Dynatrace MCP Server v${
|
|
62
|
+
console.error(`Initializing Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
62
63
|
const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv;
|
|
63
64
|
// Test connection on startup
|
|
64
65
|
let retryCount = 0;
|
|
@@ -88,10 +89,24 @@ const main = async () => {
|
|
|
88
89
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
console.error(`Starting Dynatrace MCP Server v${
|
|
92
|
+
console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
93
|
+
// Initialize usage tracking
|
|
94
|
+
const telemetry = (0, telemetry_openkit_1.createTelemetry)();
|
|
95
|
+
await telemetry.trackMcpServerStart();
|
|
96
|
+
// Create a shutdown handler that takes shutdown operations as parameters
|
|
97
|
+
const shutdownHandler = (...shutdownOps) => {
|
|
98
|
+
return async () => {
|
|
99
|
+
console.error('Shutting down MCP server...');
|
|
100
|
+
for (const op of shutdownOps) {
|
|
101
|
+
await op();
|
|
102
|
+
}
|
|
103
|
+
process.exit(0);
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
// Initialize Metadata for MCP Server
|
|
92
107
|
const server = new mcp_js_1.McpServer({
|
|
93
108
|
name: 'Dynatrace MCP Server',
|
|
94
|
-
version:
|
|
109
|
+
version: (0, version_1.getPackageJsonVersion)(),
|
|
95
110
|
}, {
|
|
96
111
|
capabilities: {
|
|
97
112
|
tools: {},
|
|
@@ -100,13 +115,21 @@ const main = async () => {
|
|
|
100
115
|
// quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
|
|
101
116
|
const tool = (name, description, paramsSchema, cb) => {
|
|
102
117
|
const wrappedCb = async (args) => {
|
|
118
|
+
// track starttime for telemetry
|
|
119
|
+
const startTime = Date.now();
|
|
120
|
+
// track toolcall for telemetry
|
|
121
|
+
let toolCallSuccessful = false;
|
|
103
122
|
try {
|
|
123
|
+
// call the tool
|
|
104
124
|
const response = await cb(args);
|
|
125
|
+
toolCallSuccessful = true;
|
|
105
126
|
return {
|
|
106
127
|
content: [{ type: 'text', text: response }],
|
|
107
128
|
};
|
|
108
129
|
}
|
|
109
130
|
catch (error) {
|
|
131
|
+
// Track error
|
|
132
|
+
telemetry.trackError(error, `tool_${name}`).catch((e) => console.warn('Failed to track error:', e));
|
|
110
133
|
// check if it's an error originating from the Dynatrace SDK / API Gateway and provide an appropriate message to the user
|
|
111
134
|
if ((0, shared_errors_1.isClientRequestError)(error)) {
|
|
112
135
|
return {
|
|
@@ -121,9 +144,17 @@ const main = async () => {
|
|
|
121
144
|
isError: true,
|
|
122
145
|
};
|
|
123
146
|
}
|
|
147
|
+
finally {
|
|
148
|
+
// Track tool usage
|
|
149
|
+
const duration = Date.now() - startTime;
|
|
150
|
+
telemetry
|
|
151
|
+
.trackMcpToolUsage(name, toolCallSuccessful, duration)
|
|
152
|
+
.catch((e) => console.warn('Failed to track tool usage:', e));
|
|
153
|
+
}
|
|
124
154
|
};
|
|
125
155
|
server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
|
|
126
156
|
};
|
|
157
|
+
/** Tool Definitions below */
|
|
127
158
|
tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, async ({}) => {
|
|
128
159
|
// create an oauth-client
|
|
129
160
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
@@ -193,10 +224,10 @@ const main = async () => {
|
|
|
193
224
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read', 'storage:buckets:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
194
225
|
// get problems (uses fetch)
|
|
195
226
|
const result = await (0, list_problems_1.listProblems)(dtClient, additionalFilter);
|
|
196
|
-
if (result && result.length > 0) {
|
|
197
|
-
let resp = `Found ${result.length} problems! Displaying the top ${maxProblemsToDisplay} problems:\n`;
|
|
227
|
+
if (result && result.records && result.records.length > 0) {
|
|
228
|
+
let resp = `Found ${result.records.length} problems! Displaying the top ${maxProblemsToDisplay} problems:\n`;
|
|
198
229
|
// iterate over dqlResponse and create a string with the problem details, but only show the top maxProblemsToDisplay problems
|
|
199
|
-
result.slice(0, maxProblemsToDisplay).forEach((problem) => {
|
|
230
|
+
result.records.slice(0, maxProblemsToDisplay).forEach((problem) => {
|
|
200
231
|
if (problem) {
|
|
201
232
|
resp += `Problem ${problem['display_id']} (please refer to this problem with \`problemId\` or \`event.id\` ${problem['problem_id']}))
|
|
202
233
|
with event.status ${problem['event.status']}, event.category ${problem['event.category']}: ${problem['event.name']} -
|
|
@@ -250,6 +281,18 @@ const main = async () => {
|
|
|
250
281
|
else if (entityDetails.type == 'CLOUD_APPLICATION') {
|
|
251
282
|
resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}`;
|
|
252
283
|
}
|
|
284
|
+
resp += `\n\n**Filter**:`;
|
|
285
|
+
// Use entityTypeTable as the filter (e.g., fetch logs | filter dt.entity.service == "SERVICE-1234")
|
|
286
|
+
if (entityDetails.entityTypeTable) {
|
|
287
|
+
resp += ` You can use the following filter to get relevant information from other tools: \`| filter ${entityDetails.entityTypeTable} == "${entityDetails.entityId}"\`. `;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
resp += ` Try to use search command as follows: \`| search "${entityDetails.entityId}"\`. `;
|
|
291
|
+
}
|
|
292
|
+
resp += `\n\n**Next Steps**\n\n`;
|
|
293
|
+
resp += `1. Find available metrics for this entity, by using execute_dql tool with the following DQL statement: "fetch metric.series" and the filter defined above\n`;
|
|
294
|
+
resp += `2. Find out whether any problems exist for this entity using the list_problems tool\n`;
|
|
295
|
+
resp += `3. Explore logs for this entity by using execute_dql with "fetch logs" and applying the filter mentioned above'\n`;
|
|
253
296
|
return resp;
|
|
254
297
|
});
|
|
255
298
|
tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
|
|
@@ -281,11 +324,11 @@ const main = async () => {
|
|
|
281
324
|
return resp;
|
|
282
325
|
});
|
|
283
326
|
tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
|
|
284
|
-
'You can also use "generate_dql_from_natural_language" to generate a DQL statement based on your request. ' +
|
|
327
|
+
'You can also use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
|
|
285
328
|
'Note: For more information about available fields for filters and aggregation, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
|
|
286
329
|
dqlStatement: zod_1.z
|
|
287
330
|
.string()
|
|
288
|
-
.describe('DQL Statement (Ex: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}.", "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }")'),
|
|
331
|
+
.describe('DQL Statement (Ex: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}.", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }")'),
|
|
289
332
|
}, async ({ dqlStatement }) => {
|
|
290
333
|
// Create a HTTP Client that has all storage:*:read scopes
|
|
291
334
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
|
|
@@ -300,7 +343,37 @@ const main = async () => {
|
|
|
300
343
|
'storage:user.sessions:read', // Read User sessions from Grail
|
|
301
344
|
'storage:security.events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
302
345
|
const response = await (0, execute_dql_1.executeDql)(dtClient, { query: dqlStatement });
|
|
303
|
-
|
|
346
|
+
if (!response) {
|
|
347
|
+
return 'DQL execution failed or returned no result.';
|
|
348
|
+
}
|
|
349
|
+
let result = `📊 **DQL Query Results**\n\n`;
|
|
350
|
+
// Cost and Performance Information
|
|
351
|
+
if (response.scannedRecords !== undefined) {
|
|
352
|
+
result += `- **Scanned Records:** ${response.scannedRecords.toLocaleString()}\n`;
|
|
353
|
+
}
|
|
354
|
+
if (response.scannedBytes !== undefined) {
|
|
355
|
+
const scannedGB = response.scannedBytes / (1000 * 1000 * 1000);
|
|
356
|
+
result += `- **Scanned Bytes:** ${scannedGB.toFixed(2)} GB`;
|
|
357
|
+
// Cost warning based on scanned bytes
|
|
358
|
+
if (scannedGB > 500) {
|
|
359
|
+
result += `\n ⚠️ **Very High Data Usage Warning:** This query scanned ${scannedGB.toFixed(1)} GB of data, which may impact your Dynatrace consumption. Please take measures to optimize your query, like limiting the timeframe or selecting a bucket.\n`;
|
|
360
|
+
}
|
|
361
|
+
else if (scannedGB > 50) {
|
|
362
|
+
result += `\n ⚠️ **High Data Usage Warning:** This query scanned ${scannedGB.toFixed(2)} GB of data, which may impact your Dynatrace consumption.\n`;
|
|
363
|
+
}
|
|
364
|
+
else if (scannedGB > 5) {
|
|
365
|
+
result += `\n 💡 **Moderate Data Usage:** This query scanned ${scannedGB.toFixed(2)} GB of data.\n`;
|
|
366
|
+
}
|
|
367
|
+
else if (response.scannedBytes === 0) {
|
|
368
|
+
result += `\n 💡 **No Data consumed:** This query did not consume any data.\n`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (response.sampled !== undefined && response.sampled) {
|
|
372
|
+
result += `- **⚠️ Sampling Used:** Yes (results may be approximate)\n`;
|
|
373
|
+
}
|
|
374
|
+
result += `\n📋 **Query Results**: (${response.records?.length || 0} records):\n\n`;
|
|
375
|
+
result += `\`\`\`json\n${JSON.stringify(response.records, null, 2)}\n\`\`\``;
|
|
376
|
+
return result;
|
|
304
377
|
});
|
|
305
378
|
tool('generate_dql_from_natural_language', 'Convert natural language queries to Dynatrace Query Language (DQL) using Davis CoPilot AI. You can ask for problem events, security issues, logs, metrics, spans, and custom data.', {
|
|
306
379
|
text: zod_1.z
|
|
@@ -311,7 +384,10 @@ const main = async () => {
|
|
|
311
384
|
const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
|
|
312
385
|
let resp = `🔤 Natural Language to DQL:\n\n`;
|
|
313
386
|
resp += `**Query:** "${text}"\n\n`;
|
|
314
|
-
|
|
387
|
+
if (response.dql) {
|
|
388
|
+
// Typically, the DQL response is empty if status == FAILED
|
|
389
|
+
resp += `**Generated DQL:**\n\`\`\`\n${response.dql}\n\`\`\`\n\n`;
|
|
390
|
+
}
|
|
315
391
|
resp += `**Status:** ${response.status}\n`;
|
|
316
392
|
resp += `**Message Token:** ${response.messageToken}\n`;
|
|
317
393
|
if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
|
|
@@ -320,9 +396,11 @@ const main = async () => {
|
|
|
320
396
|
resp += `- ${notification.severity}: ${notification.message}\n`;
|
|
321
397
|
});
|
|
322
398
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
399
|
+
if (response.status != 'FAILED') {
|
|
400
|
+
resp += `\n💡 **Next Steps:**\n`;
|
|
401
|
+
resp += `1. Use "execute_dql" tool to run the query (you can omit running the "verify_dql" tool)\n`;
|
|
402
|
+
resp += `2. If results don't match expectations, refine your natural language description and try again\n`;
|
|
403
|
+
}
|
|
326
404
|
return resp;
|
|
327
405
|
});
|
|
328
406
|
tool('explain_dql_in_natural_language', 'Explain Dynatrace Query Language (DQL) statements in natural language using Davis CoPilot AI.', {
|
|
@@ -366,7 +444,10 @@ const main = async () => {
|
|
|
366
444
|
const response = await (0, davis_copilot_1.chatWithDavisCopilot)(dtClient, text, conversationContext);
|
|
367
445
|
let resp = `🤖 Davis CoPilot Response:\n\n`;
|
|
368
446
|
resp += `**Your Question:** "${text}"\n\n`;
|
|
369
|
-
|
|
447
|
+
if (response.text) {
|
|
448
|
+
// Typically, text is empty if status is FAILED
|
|
449
|
+
resp += `**Answer:**\n${response.text}\n\n`;
|
|
450
|
+
}
|
|
370
451
|
resp += `**Status:** ${response.status}\n`;
|
|
371
452
|
resp += `**Message Token:** ${response.messageToken}\n`;
|
|
372
453
|
if (response.metadata?.sources && response.metadata.sources.length > 0) {
|
|
@@ -384,6 +465,9 @@ const main = async () => {
|
|
|
384
465
|
if (response.state?.conversationId) {
|
|
385
466
|
resp += `\n**Conversation ID:** ${response.state.conversationId}`;
|
|
386
467
|
}
|
|
468
|
+
if (response.status == 'FAILED') {
|
|
469
|
+
resp += `\n❌ **Your request was not successful**\n`;
|
|
470
|
+
}
|
|
387
471
|
return resp;
|
|
388
472
|
});
|
|
389
473
|
tool('create_workflow_for_notification', 'Create a notification for a team based on a problem type within Workflows in Dynatrace', {
|
|
@@ -441,7 +525,7 @@ const main = async () => {
|
|
|
441
525
|
program
|
|
442
526
|
.name('dynatrace-mcp-server')
|
|
443
527
|
.description('Dynatrace Model Context Protocol (MCP) Server')
|
|
444
|
-
.version(
|
|
528
|
+
.version((0, version_1.getPackageJsonVersion)())
|
|
445
529
|
.option('--http', 'enable HTTP server mode instead of stdio')
|
|
446
530
|
.option('--server', 'enable HTTP server mode (alias for --http)')
|
|
447
531
|
.option('-p, --port <number>', 'port for HTTP server', '3000')
|
|
@@ -482,13 +566,8 @@ const main = async () => {
|
|
|
482
566
|
httpServer.listen(httpPort, host, () => {
|
|
483
567
|
console.error(`Dynatrace MCP Server running on HTTP at http://${host}:${httpPort}`);
|
|
484
568
|
});
|
|
485
|
-
// Handle graceful shutdown
|
|
486
|
-
process.on('SIGINT', () =>
|
|
487
|
-
console.error('Shutting down HTTP server...');
|
|
488
|
-
httpServer.close(() => {
|
|
489
|
-
process.exit(0);
|
|
490
|
-
});
|
|
491
|
-
});
|
|
569
|
+
// Handle graceful shutdown for http server mode
|
|
570
|
+
process.on('SIGINT', shutdownHandler(async () => await telemetry.shutdown(), () => new Promise((resolve) => httpServer.close(() => resolve()))));
|
|
492
571
|
}
|
|
493
572
|
else {
|
|
494
573
|
// Default stdio mode
|
|
@@ -496,9 +575,21 @@ const main = async () => {
|
|
|
496
575
|
console.error('Connecting server to transport...');
|
|
497
576
|
await server.connect(transport);
|
|
498
577
|
console.error('Dynatrace MCP Server running on stdio');
|
|
578
|
+
// Handle graceful shutdown for stdio mode
|
|
579
|
+
process.on('SIGINT', shutdownHandler(async () => await telemetry.shutdown()));
|
|
580
|
+
process.on('SIGTERM', shutdownHandler(async () => await telemetry.shutdown()));
|
|
499
581
|
}
|
|
500
582
|
};
|
|
501
|
-
main().catch((error) => {
|
|
583
|
+
main().catch(async (error) => {
|
|
502
584
|
console.error('Fatal error in main():', error);
|
|
585
|
+
try {
|
|
586
|
+
// report error in main
|
|
587
|
+
const telemetry = (0, telemetry_openkit_1.createTelemetry)();
|
|
588
|
+
await telemetry.trackError(error, 'main_error');
|
|
589
|
+
await telemetry.shutdown();
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
console.warn('Failed to track fatal error:', e);
|
|
593
|
+
}
|
|
503
594
|
process.exit(1);
|
|
504
595
|
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createTelemetry = createTelemetry;
|
|
37
|
+
const openkit_js_1 = require("@dynatrace/openkit-js");
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const version_1 = require("./version");
|
|
41
|
+
/**
|
|
42
|
+
* Based on https://docs.dynatrace.com/docs/ingest-from/extend-dynatrace/openkit/instrument-your-application-using-dynatrace-openkit#openkit-basic-sample--javascript
|
|
43
|
+
*/
|
|
44
|
+
class DynatraceMcpTelemetry {
|
|
45
|
+
openKit = null;
|
|
46
|
+
session = null;
|
|
47
|
+
isEnabled;
|
|
48
|
+
initPromise;
|
|
49
|
+
constructor() {
|
|
50
|
+
this.isEnabled = process.env.DT_MCP_DISABLE_TELEMETRY !== 'true';
|
|
51
|
+
if (!this.isEnabled) {
|
|
52
|
+
throw new Error('Dynatrace Telemetry is disabled via DT_MCP_DISABLE_TELEMETRY=true');
|
|
53
|
+
}
|
|
54
|
+
// Default configuration for Dynatrace MCP Server Telemetry endpoints (DT Prod Self Mon)
|
|
55
|
+
const applicationId = process.env.DT_MCP_TELEMETRY_APPLICATION_ID || '5e2dbb56-076b-412e-8ffc-7babb7ae7c5d';
|
|
56
|
+
const endpointUrl = process.env.DT_MCP_TELEMETRY_ENDPOINT_URL || 'https://bf96767wvv.bf.dynatrace.com/mbeacon';
|
|
57
|
+
// get anonymized device id
|
|
58
|
+
const deviceId = process.env.DT_MCP_TELEMETRY_DEVICE_ID || this.generateDeviceId();
|
|
59
|
+
this.initPromise = this.initializeOpenKit(endpointUrl, applicationId, deviceId);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
* @param endpointUrl Dynatrace Endpoint for OpenKit Ingest
|
|
64
|
+
* @param applicationId Application Id for OpenKit Ingest
|
|
65
|
+
* @param deviceId Device or Session ID (should be anonymized)
|
|
66
|
+
* @returns true if initialization was successful, false otherwise
|
|
67
|
+
*/
|
|
68
|
+
async initializeOpenKit(endpointUrl, applicationId, deviceId) {
|
|
69
|
+
try {
|
|
70
|
+
console.error(`Connecting Dynatrace Telemetry via ${endpointUrl}. You can disable this by setting DT_MCP_DISABLE_TELEMETRY=true.`);
|
|
71
|
+
this.openKit = new openkit_js_1.OpenKitBuilder(endpointUrl, applicationId, parseInt(deviceId, 10))
|
|
72
|
+
.withApplicationVersion((0, version_1.getPackageJsonVersion)())
|
|
73
|
+
.withOperatingSystem(`${os.platform()} ${os.release()}`)
|
|
74
|
+
.withManufacturer('Dynatrace-OSS')
|
|
75
|
+
.withModelId('Dynatrace-MCP-Server')
|
|
76
|
+
.build();
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const timeoutInMilliseconds = 10 * 1000; // 10 seconds timeout
|
|
79
|
+
this.openKit.waitForInit((success) => {
|
|
80
|
+
if (success) {
|
|
81
|
+
this.session = this.openKit.createSession();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.error('Failed to initialize Dynatrace Telemetry: timeout or connection failed');
|
|
85
|
+
this.isEnabled = false;
|
|
86
|
+
}
|
|
87
|
+
resolve(success);
|
|
88
|
+
}, timeoutInMilliseconds);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('Failed to initialize Dynatrace Telemetry:', error);
|
|
93
|
+
console.error('If the error persists, please consider disabling telemetry by setting DT_MCP_DISABLE_TELEMETRY=true.');
|
|
94
|
+
this.isEnabled = false;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generates a random device identifier
|
|
100
|
+
* @returns deviceId - a string containing number for OpenKit
|
|
101
|
+
*/
|
|
102
|
+
generateDeviceId() {
|
|
103
|
+
// Generate a simple device ID based on hostname and some randomness
|
|
104
|
+
const hostname = os.hostname();
|
|
105
|
+
const random = crypto.randomBytes(8).toString('hex');
|
|
106
|
+
const hash = crypto.createHash('md5').update(`${hostname}-${random}`).digest('hex');
|
|
107
|
+
// Convert to a number (device ID must be a number for OpenKit)
|
|
108
|
+
return parseInt(hash.substring(0, 15), 16).toString();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Track Server Start
|
|
112
|
+
* @returns nothing
|
|
113
|
+
*/
|
|
114
|
+
async trackMcpServerStart() {
|
|
115
|
+
if (!this.isEnabled)
|
|
116
|
+
return;
|
|
117
|
+
await this.initPromise;
|
|
118
|
+
if (!this.session)
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
const action = this.session.enterAction('mcp_server_start');
|
|
122
|
+
action.reportEvent('server_started');
|
|
123
|
+
action.reportValue('version', (0, version_1.getPackageJsonVersion)());
|
|
124
|
+
action.reportValue('node_version', process.version);
|
|
125
|
+
action.reportValue('platform', process.platform);
|
|
126
|
+
action.leaveAction();
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn('Failed to track server start:', error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Track Tool Usage
|
|
134
|
+
* @param toolName name of the tool
|
|
135
|
+
* @param success whether or not the tool call was successful
|
|
136
|
+
* @param duration duration of the tool call
|
|
137
|
+
* @returns nothing
|
|
138
|
+
*/
|
|
139
|
+
async trackMcpToolUsage(toolName, success, duration) {
|
|
140
|
+
if (!this.isEnabled)
|
|
141
|
+
return;
|
|
142
|
+
await this.initPromise;
|
|
143
|
+
if (!this.session)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
const action = this.session.enterAction(`tool_${toolName}`);
|
|
147
|
+
action.reportEvent(success ? 'tool_success' : 'tool_error');
|
|
148
|
+
action.reportValue('tool_name', toolName);
|
|
149
|
+
action.reportValue('success', success ? 'true' : 'false');
|
|
150
|
+
if (duration !== undefined) {
|
|
151
|
+
action.reportValue('duration_ms', duration);
|
|
152
|
+
}
|
|
153
|
+
action.leaveAction();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.warn('Failed to track tool usage:', error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Track Errors
|
|
161
|
+
* @param error error message to be tracked
|
|
162
|
+
* @param context
|
|
163
|
+
* @returns nothing
|
|
164
|
+
*/
|
|
165
|
+
async trackError(error, context) {
|
|
166
|
+
if (!this.isEnabled)
|
|
167
|
+
return;
|
|
168
|
+
await this.initPromise;
|
|
169
|
+
if (!this.session)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const action = this.session.enterAction('error_occurred');
|
|
173
|
+
// reportError expects name and code, so we'll use error name and a generic error code
|
|
174
|
+
action.reportError(error.name || 'Error', 500);
|
|
175
|
+
if (context) {
|
|
176
|
+
action.reportValue('error_context', context);
|
|
177
|
+
}
|
|
178
|
+
action.reportValue('error_message', error.message);
|
|
179
|
+
if (error.stack) {
|
|
180
|
+
action.reportValue('error_stack', error.stack.substring(0, 1000)); // Limit stack trace length
|
|
181
|
+
}
|
|
182
|
+
action.leaveAction();
|
|
183
|
+
}
|
|
184
|
+
catch (trackingError) {
|
|
185
|
+
console.warn('Failed to track error:', trackingError);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async shutdown() {
|
|
189
|
+
if (!this.isEnabled)
|
|
190
|
+
return;
|
|
191
|
+
await this.initPromise;
|
|
192
|
+
try {
|
|
193
|
+
if (this.session) {
|
|
194
|
+
this.session.end();
|
|
195
|
+
}
|
|
196
|
+
if (this.openKit) {
|
|
197
|
+
await new Promise((resolve) => {
|
|
198
|
+
this.openKit.shutdown(() => resolve());
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
console.warn('Failed to shutdown usage tracking:', error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
class NoOpTelemetry {
|
|
208
|
+
async trackMcpServerStart() { }
|
|
209
|
+
async trackMcpToolUsage() { }
|
|
210
|
+
async trackError() { }
|
|
211
|
+
async shutdown() { }
|
|
212
|
+
}
|
|
213
|
+
function createTelemetry() {
|
|
214
|
+
try {
|
|
215
|
+
return new DynatraceMcpTelemetry();
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
// Failed to initialize
|
|
219
|
+
console.error(e);
|
|
220
|
+
// fallback to NoOp Telemetry
|
|
221
|
+
return new NoOpTelemetry();
|
|
222
|
+
}
|
|
223
|
+
}
|
package/dist/utils/user-agent.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getUserAgent = void 0;
|
|
4
|
-
const
|
|
4
|
+
const version_1 = require("./version");
|
|
5
5
|
/**
|
|
6
6
|
* Generate a user agent string for Dynatrace MCP Server
|
|
7
7
|
* @returns User agent string in format: dynatrace-mcp-server/vX.X.X (platform-arch)
|
|
8
8
|
*/
|
|
9
9
|
const getUserAgent = () => {
|
|
10
|
-
return `dynatrace-mcp-server/v${
|
|
10
|
+
return `dynatrace-mcp-server/v${(0, version_1.getPackageJsonVersion)()} (${process.platform}-${process.arch})`;
|
|
11
11
|
};
|
|
12
12
|
exports.getUserAgent = getUserAgent;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPackageJsonVersion = getPackageJsonVersion;
|
|
4
|
+
const package_json_1 = require("../../package.json");
|
|
5
|
+
/**
|
|
6
|
+
* Gets the current version of the Dynatrace MCP Server from package.json
|
|
7
|
+
* Note: we have package.json listed in exports, such that we can ensure it's always part of the bundle
|
|
8
|
+
* @returns The version string from package.json
|
|
9
|
+
*/
|
|
10
|
+
function getPackageJsonVersion() {
|
|
11
|
+
return package_json_1.version;
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynatrace-oss/dynatrace-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-rc.1",
|
|
4
|
+
"mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
|
|
4
5
|
"description": "Model Context Protocol (MCP) server for Dynatrace",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"Dynatrace",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"@dynatrace-sdk/client-platform-management-service": "^1.6.3",
|
|
51
52
|
"@dynatrace-sdk/client-query": "^1.18.1",
|
|
52
53
|
"@dynatrace-sdk/shared-errors": "^1.0.0",
|
|
54
|
+
"@dynatrace/openkit-js": "^4.1.0",
|
|
53
55
|
"@modelcontextprotocol/sdk": "^1.8.0",
|
|
54
56
|
"commander": "^14.0.0",
|
|
55
57
|
"dotenv": "^17.2.1",
|