@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 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
- ## 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
 
@@ -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
- **Note**: We will be providing a way to monitor Query Usage of the dynatrace-mcp-server in the future.
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
- "OAUTH_CLIENT_ID": "",
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
- "mobile-mcp": {
220
+ "dynatrace-mcp-server": {
190
221
  "command": "npx",
191
222
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
192
223
  "env": {
193
- "OAUTH_CLIENT_ID": "",
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
- "mobile-mcp": {
239
+ "dynatrace-mcp-server": {
210
240
  "command": "npx",
211
241
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
212
242
  "env": {
213
- "OAUTH_CLIENT_ID": "",
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 **OAuth Client** or **Platform Tokens** (v0.5.0 and newer) via the following environment variables:
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
- - `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)
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 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).
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, 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.
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
- 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.
533
+ **For Platform Tokens:**
500
534
 
501
- It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
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 without metadata and without notifications, or undefined if the query failed or no result was returned.
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.records;
54
+ // yes - return response result immediately
55
+ return createResultAndLog(response.result, 'execute_dql - Metadata:');
33
56
  }
34
- // else: We might have to poll
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
- // done - let's return it
68
+ // check if we got a result from the polling endpoint
46
69
  if (pollResponse.result) {
47
- return pollResponse.result.records;
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
- // else: whatever happened - we have an error
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 package_json_1 = require("../package.json");
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${package_json_1.version}...`);
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${package_json_1.version}...`);
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: package_json_1.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
- return `DQL Response: ${JSON.stringify(response)}`;
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
- resp += `**Generated DQL:**\n\`\`\`\n${response.dql}\n\`\`\`\n\n`;
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
- resp += `\n💡 **Next Steps:**\n`;
324
- resp += `1. Use "execute_dql" tool to run the query (you can omit running the "verify_dql" tool)\n`;
325
- resp += `2. If results don't match expectations, refine your natural language description and try again\n`;
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
- resp += `**Answer:**\n${response.text}\n\n`;
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(package_json_1.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
+ }
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getUserAgent = void 0;
4
- const package_json_1 = require("../../package.json");
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${package_json_1.version} (${process.platform}-${process.arch})`;
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.5.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",