@dynatrace-oss/dynatrace-mcp-server 1.4.0 → 1.5.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,8 +43,26 @@ Furthermore, you need to configure the URL to a Dynatrace environment:
43
43
 
44
44
  - `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`)
45
45
 
46
+ Authentication will be handled via Authorization Code Flow in your browser, you don't need to define a Platform Token nor an OAuth Client to get started.
47
+
46
48
  Once you are done, we recommend looking into [example prompts](#-example-prompts-), like `Get all details of the entity 'my-service'` or `Show me error logs`. Please mind that these prompts lead to executing DQL statements which may incur [costs](#costs) in accordance to your licence.
47
49
 
50
+ **VSCode**
51
+
52
+ ```json
53
+ {
54
+ "servers": {
55
+ "npx-dynatrace-mcp-server": {
56
+ "command": "npx",
57
+ "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
58
+ "env": {
59
+ "DT_ENVIRONMENT": "https://abc12345.apps.dynatrace.com"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
48
66
  ## Architecture
49
67
 
50
68
  ![Architecture](https://github.com/dynatrace-oss/dynatrace-mcp/blob/main/assets/dynatrace-mcp-arch.png?raw=true)
@@ -124,22 +142,6 @@ We recommend to always set it up for your current workspace instead of using it
124
142
 
125
143
  **VS Code**
126
144
 
127
- ```json
128
- {
129
- "servers": {
130
- "npx-dynatrace-mcp-server": {
131
- "command": "npx",
132
- "cwd": "${workspaceFolder}",
133
- "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
134
- "envFile": "${workspaceFolder}/.env"
135
- }
136
- }
137
- }
138
- ```
139
-
140
- Please note: In this config, [the `${workspaceFolder}` variable](https://code.visualstudio.com/docs/reference/variables-reference#_predefined-variables) is used.
141
- This only works if the config is stored in the current workspaces, e.g., `<your-repo>/.vscode/mcp.json`. Alternatively, this can also be stored in user-settings, and you can define `env` as follows:
142
-
143
145
  ```json
144
146
  {
145
147
  "servers": {
@@ -147,7 +149,7 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
147
149
  "command": "npx",
148
150
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
149
151
  "env": {
150
- "DT_ENVIRONMENT": ""
152
+ "DT_ENVIRONMENT": "https://abc12345.apps.dynatrace.com"
151
153
  }
152
154
  }
153
155
  }
@@ -163,7 +165,7 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
163
165
  "command": "npx",
164
166
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
165
167
  "env": {
166
- "DT_ENVIRONMENT": ""
168
+ "DT_ENVIRONMENT": "https://abc12345.apps.dynatrace.com"
167
169
  }
168
170
  }
169
171
  }
@@ -181,7 +183,7 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
181
183
  "command": "npx",
182
184
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
183
185
  "env": {
184
- "DT_ENVIRONMENT": ""
186
+ "DT_ENVIRONMENT": "https://abc12345.apps.dynatrace.com"
185
187
  }
186
188
  }
187
189
  }
@@ -201,7 +203,7 @@ The [Amazon Kiro](https://kiro.dev/) is an agentic IDE that helps you do your be
201
203
  "command": "npx",
202
204
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
203
205
  "env": {
204
- "DT_ENVIRONMENT": ""
206
+ "DT_ENVIRONMENT": "https://abc12345.apps.dynatrace.com"
205
207
  }
206
208
  }
207
209
  }
@@ -219,10 +221,11 @@ Using `gemini` CLI directly (recommended):
219
221
  ```bash
220
222
  gemini extensions install https://github.com/dynatrace-oss/dynatrace-mcp
221
223
  export DT_PLATFORM_TOKEN=... # optional
222
- export DT_ENVIRONMENT=https://...
223
224
  ```
224
225
 
225
- and verify that the server is running via
226
+ The command will ask for the value Dynatrace Environment.
227
+
228
+ Verify that the server is running via
226
229
 
227
230
  ```bash
228
231
  gemini mcp list
@@ -237,7 +240,7 @@ Or manually in your `~/.gemini/settings.json` or `.gemini/settings.json`:
237
240
  "command": "npx",
238
241
  "args": ["@dynatrace-oss/dynatrace-mcp-server@latest"],
239
242
  "env": {
240
- "DT_ENVIRONMENT": ""
243
+ "DT_ENVIRONMENT": "https://abc12345.apps.dynatrace.com"
241
244
  },
242
245
  "timeout": 30000,
243
246
  "trust": false
@@ -325,7 +328,7 @@ When just providing `DT_ENVIRONMENT`, the local MCP server will try to open a br
325
328
 
326
329
  For more information about the other authentication methods, please have a look at the documentation about
327
330
  [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
328
- [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.
331
+ [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 (service-users, backend-to-backend communication).
329
332
 
330
333
  In addition, depending on the features you use, the following variables can be configured:
331
334
 
@@ -338,6 +341,7 @@ The MCP server honors system proxy settings for corporate environments:
338
341
  - `https_proxy` or `HTTPS_PROXY` (optional, string, e.g., `http://proxy.example.com:8080`) - Proxy server URL for HTTPS requests
339
342
  - `http_proxy` or `HTTP_PROXY` (optional, string, e.g., `http://proxy.example.com:8080`) - Proxy server URL for HTTP requests
340
343
  - `no_proxy` or `NO_PROXY` (optional, string, e.g., `localhost,127.0.0.1,.local`) - Comma-separated list of hostnames or domains that should bypass the proxy
344
+ - `NODE_EXTRA_CA_CERTS` (optional, string, e.g., `C:\some-path\certificate.pem`) - When set, the well known "root" CAs (like VeriSign) will be extended with the extra certificates
341
345
 
342
346
  **Note:** The `no_proxy` environment variable is currently logged for informational purposes but not fully enforced by the underlying HTTP client. If you need to bypass the proxy for specific hosts, consider configuring your proxy server to handle these exclusions.
343
347
 
@@ -34,6 +34,7 @@ const createResultAndLog = (queryResult, logPrefix, budgetLimitGB) => {
34
34
  const result = {
35
35
  records: queryResult.records,
36
36
  metadata: queryResult.metadata,
37
+ types: queryResult.types ?? [],
37
38
  scannedBytes,
38
39
  scannedRecords: queryResult.metadata?.grail?.scannedRecords,
39
40
  executionTimeMilliseconds: queryResult.metadata?.grail?.executionTimeMilliseconds,
package/dist/index.js CHANGED
@@ -3,10 +3,14 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const client_platform_management_service_1 = require("@dynatrace-sdk/client-platform-management-service");
5
5
  const shared_errors_1 = require("@dynatrace-sdk/shared-errors");
6
+ // Dynamically imported below (ESM-only package)
7
+ // import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
6
8
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
9
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
8
10
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
11
  const node_http_1 = require("node:http");
12
+ const node_fs_1 = require("node:fs");
13
+ const node_path_1 = require("node:path");
10
14
  const commander_1 = require("commander");
11
15
  const zod_1 = require("zod");
12
16
  const version_1 = require("./utils/version");
@@ -76,6 +80,9 @@ const allRequiredScopes = scopesBase.concat([
76
80
  'document:documents:write', // Create and update documents
77
81
  ]);
78
82
  const main = async () => {
83
+ // Dynamic import: @modelcontextprotocol/ext-apps is ESM-only and can't be require()'d.
84
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
85
+ const { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } = await dynamicImport('@modelcontextprotocol/ext-apps/server');
79
86
  console.error(`Initializing Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
80
87
  // Configure proxy from environment variables early in the startup process
81
88
  (0, proxy_config_1.configureProxyFromEnvironment)();
@@ -181,9 +188,9 @@ const main = async () => {
181
188
  }
182
189
  // Ready to start the server
183
190
  console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
184
- // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
185
- const tool = (name, title, description, paramsSchema, annotations, cb) => {
186
- const wrappedCb = async (args) => {
191
+ // Wraps a string-returning tool callback with rate limiting, telemetry, and error handling
192
+ const wrapToolCallback = (name, cb) => {
193
+ return async (args, _extra) => {
187
194
  // Capture starttime for telemetry and rate limiting
188
195
  const startTime = Date.now();
189
196
  /**
@@ -210,9 +217,16 @@ const main = async () => {
210
217
  // call the tool
211
218
  const response = await cb(args);
212
219
  toolCallSuccessful = true;
213
- return {
214
- content: [{ type: 'text', text: response }],
220
+ // Support callbacks that return either a plain string or { text, _meta }
221
+ const text = typeof response === 'string' ? response : response.text;
222
+ const _meta = typeof response === 'string' ? undefined : response._meta;
223
+ const result = {
224
+ content: [{ type: 'text', text }],
215
225
  };
226
+ if (_meta) {
227
+ result._meta = _meta;
228
+ }
229
+ return result;
216
230
  }
217
231
  catch (error) {
218
232
  // Track error
@@ -239,12 +253,15 @@ const main = async () => {
239
253
  .catch((e) => console.warn('Failed to track tool usage:', e));
240
254
  }
241
255
  };
256
+ };
257
+ // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
258
+ const tool = (name, title, description, paramsSchema, annotations, cb) => {
242
259
  server.registerTool(name, {
243
260
  title: title,
244
261
  description: description,
245
262
  inputSchema: zod_1.z.object(paramsSchema),
246
263
  annotations: annotations,
247
- }, (args) => wrappedCb(args));
264
+ }, (args) => wrapToolCallback(name, cb)(args));
248
265
  };
249
266
  /**
250
267
  * Helper function to request human approval for potentially sensitive operations
@@ -498,26 +515,39 @@ const main = async () => {
498
515
  }
499
516
  return resp;
500
517
  });
501
- tool('execute_dql', 'Execute DQL', 'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
502
- 'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
503
- 'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
504
- dqlStatement: zod_1.z
505
- .string()
506
- .describe('DQL Statement (Ex: "fetch [logs, spans, events, metric.series, ...], from: now()-4h, to: now() [| filter <some-filter>] [| summarize count(), by:{some-fields}]", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }", or for entities via smartscape: "smartscapeNodes \"[*, HOST, PROCESS, ...]\" [| filter id == "<ENTITY-ID>"]"). ' +
507
- 'When querying data for a specific entity, call the `find_entity_by_name` tool first to get an appropriate filter like `dt.entity.service == "SERVICE-1234"` or `dt.entity.host == "HOST-1234"` to be used in the DQL statement. '),
508
- recordLimit: zod_1.z.number().optional().default(100).describe('Maximum number of records to return (default: 100)'),
509
- recordSizeLimitMB: zod_1.z
510
- .number()
511
- .optional()
512
- .default(1)
513
- .describe('Maximum size of the returned records in MB (default: 1MB)'),
514
- }, {
515
- // not readonly (DQL statements may modify things), not idempotent (may change over time)
516
- readOnlyHint: false,
517
- idempotentHint: false,
518
- // while we are not strictly talking to the open world here, the response from execute DQL could interpreted as a web-search, which often is referred to open-world
519
- openWorldHint: true,
520
- }, async ({ dqlStatement, recordLimit = 100, recordSizeLimitMB = 1 }) => {
518
+ // MCP App: Define the resource URI for the execute_dql interactive UI
519
+ // The ui:// scheme tells hosts this is an MCP App resource.
520
+ const executeDqlResourceUri = 'ui://execute-dql/execute-dql.html';
521
+ // Register the execute_dql tool with MCP App UI support.
522
+ registerAppTool(server, 'execute_dql', {
523
+ title: 'Execute DQL',
524
+ description: 'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace Grail by executing a Dynatrace Query Language (DQL) statement. ' +
525
+ 'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
526
+ 'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \\"logs\\""',
527
+ inputSchema: {
528
+ dqlStatement: zod_1.z
529
+ .string()
530
+ .describe('DQL Statement (Ex: "fetch [logs, spans, events, metric.series, ...], from: now()-4h, to: now() [| filter <some-filter>] [| summarize count(), by:{some-fields}]", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }", or for entities via smartscape: "smartscapeNodes \\"[*, HOST, PROCESS, ...]\\" [| filter id == \\"<ENTITY-ID>\\"]"). ' +
531
+ 'When querying data for a specific entity, call the `find_entity_by_name` tool first to get an appropriate filter like `dt.entity.service == "SERVICE-1234"` or `dt.entity.host == "HOST-1234"` to be used in the DQL statement. '),
532
+ recordLimit: zod_1.z.number().optional().default(100).describe('Maximum number of records to return (default: 100)'),
533
+ recordSizeLimitMB: zod_1.z
534
+ .number()
535
+ .optional()
536
+ .default(1)
537
+ .describe('Maximum size of the returned records in MB (default: 1MB)'),
538
+ },
539
+ annotations: {
540
+ // not readonly (DQL statements may modify things), not idempotent (may change over time)
541
+ readOnlyHint: false,
542
+ idempotentHint: false,
543
+ // while we are not strictly talking to the open world here, the response from execute DQL could interpreted as a web-search, which often is referred to open-world
544
+ openWorldHint: true,
545
+ },
546
+ _meta: {
547
+ // MCP App
548
+ ui: { resourceUri: executeDqlResourceUri },
549
+ },
550
+ }, wrapToolCallback('execute_dql', async ({ dqlStatement, recordLimit = 100, recordSizeLimitMB = 1 }) => {
521
551
  // Create a HTTP Client that has all storage:*:read scopes
522
552
  const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
523
553
  'storage:logs:read', // Read logs for reliability guardian validations
@@ -535,17 +565,20 @@ const main = async () => {
535
565
  if (!response) {
536
566
  return 'DQL execution failed or returned no result.';
537
567
  }
538
- let result = `šŸ“Š **DQL Query Results**\n\n`;
539
- // Budget warning comes first if present
568
+ // Build warnings array for structured metadata
569
+ const warnings = [];
540
570
  if (response.budgetWarning) {
541
- result += `${response.budgetWarning}\n\n`;
571
+ warnings.push(response.budgetWarning);
542
572
  }
573
+ // Build human-readable text result
574
+ let result = `šŸ“Š **DQL Query Results**\n\n`;
543
575
  // Cost and Performance Information
544
576
  if (response.scannedRecords !== undefined) {
545
577
  result += `- **Scanned Records:** ${response.scannedRecords.toLocaleString()}\n`;
546
578
  }
547
579
  if (response.scannedBytes !== undefined) {
548
- const scannedGB = response.scannedBytes / (1000 * 1000 * 1000);
580
+ // calculate scanned gigabytes for better readability in warnings and result text
581
+ const scannedGB = response.scannedBytes !== undefined ? response.scannedBytes / (1000 * 1000 * 1000) : 0;
549
582
  result += `- **Scanned Bytes:** ${scannedGB.toFixed(2)} GB`;
550
583
  // Show budget status if available
551
584
  if (response.budgetState) {
@@ -561,27 +594,58 @@ const main = async () => {
561
594
  }
562
595
  result += '\n';
563
596
  if (scannedGB > 500) {
564
- result += ` āš ļø **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`;
597
+ warnings.push(`Very High Data Usage: 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.`);
565
598
  }
566
599
  else if (scannedGB > 50) {
567
- result += ` āš ļø **High Data Usage Warning:** This query scanned ${scannedGB.toFixed(2)} GB of data, which may impact your Dynatrace consumption.\n`;
600
+ warnings.push(`High Data Usage: This query scanned ${scannedGB.toFixed(2)} GB of data, which may impact your Dynatrace consumption.`);
568
601
  }
569
- else if (scannedGB > 5) {
602
+ // Add informational messages (not warnings) about data usage
603
+ else if (scannedGB > 5 && scannedGB <= 50) {
570
604
  result += ` šŸ’” **Moderate Data Usage:** This query scanned ${scannedGB.toFixed(2)} GB of data.\n`;
571
605
  }
572
606
  else if (response.scannedBytes === 0) {
573
607
  result += ` šŸ’” **No Data consumed:** This query did not consume any data.\n`;
574
608
  }
609
+ if (response.sampled) {
610
+ warnings.push('Sampling Used: Results may be approximate');
611
+ }
612
+ if (response.records.length === recordLimit) {
613
+ warnings.push(`Record Limit Reached: The result set was limited to ${recordLimit} records. Consider changing your query with a smaller timeframe, an aggregation or a more concise filter. Alternatively, increase the recordLimit if you expect more results.`);
614
+ }
575
615
  }
576
- if (response.sampled !== undefined && response.sampled) {
577
- result += `- **āš ļø Sampling Used:** Yes (results may be approximate)\n`;
578
- }
579
- if (response.records.length === recordLimit) {
580
- result += `- **āš ļø Record Limit Reached:** The result set was limited to ${recordLimit} records. Consider changing your query with a smaller timeframe, an aggregation or a more concise filter. Alternatively, increase the recordLimit if you expect more results.\n`;
616
+ // Add all warnings to result
617
+ if (warnings.length > 0) {
618
+ result += '\n';
619
+ warnings.forEach((warning) => {
620
+ result += `- **āš ļø ${warning}**\n`;
621
+ });
581
622
  }
582
623
  result += `\nšŸ“‹ **Query Results**: (${response.records?.length || 0} records):\n\n`;
583
624
  result += `\`\`\`json\n${JSON.stringify(response.records, null, 2)}\n\`\`\``;
584
- return result;
625
+ // Return structured data in _meta for MCP App UI instead of embedding in text
626
+ return {
627
+ text: result,
628
+ _meta: {
629
+ records: response.records,
630
+ types: response.types,
631
+ analysisTimeframe: response.metadata?.grail?.analysisTimeframe,
632
+ scannedRecords: response.scannedRecords,
633
+ scannedBytes: response.scannedBytes,
634
+ sampled: response.sampled,
635
+ environmentUrl: dtEnvironment,
636
+ budgetState: response.budgetState,
637
+ warnings,
638
+ recordLimit,
639
+ recordLimitReached: response.records.length === recordLimit,
640
+ },
641
+ };
642
+ }));
643
+ // MCP App: Register the HTML resource for the execute_dql interactive UI (MCP App)
644
+ registerAppResource(server, 'DQL Results Viewer', executeDqlResourceUri, {}, async () => {
645
+ const html = (0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, 'ui', 'execute-dql', 'index.html'), 'utf-8');
646
+ return {
647
+ contents: [{ uri: executeDqlResourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
648
+ };
585
649
  });
586
650
  tool('generate_dql_from_natural_language', '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.', {
587
651
  text: zod_1.z