@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 +28 -24
- package/dist/capabilities/execute-dql.js +1 -0
- package/dist/index.js +103 -39
- package/dist/ui/execute-dql/index.html +252 -0
- package/dist/utils/environment-url-parser.js +17 -0
- package/dist/utils/environment-url-parser.test.js +43 -0
- package/package.json +17 -4
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
|

|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
185
|
-
const
|
|
186
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
539
|
-
|
|
568
|
+
// Build warnings array for structured metadata
|
|
569
|
+
const warnings = [];
|
|
540
570
|
if (response.budgetWarning) {
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
+
warnings.push(`High Data Usage: This query scanned ${scannedGB.toFixed(2)} GB of data, which may impact your Dynatrace consumption.`);
|
|
568
601
|
}
|
|
569
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|