@dynatrace-oss/dynatrace-mcp-server 1.3.1 ā 1.5.0-beta.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 +2 -0
- package/dist/capabilities/execute-dql.js +1 -0
- package/dist/index.js +82 -31
- package/dist/ui/execute-dql/index.html +253 -0
- package/dist/utils/environment-url-parser.js +28 -0
- package/dist/utils/environment-url-parser.test.js +29 -0
- package/dist/utils/telemetry-openkit.js +7 -3
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
</a>
|
|
25
25
|
</h4>
|
|
26
26
|
|
|
27
|
+
> š **Try out our brand new [Remote Dynatrace MCP Server](https://www.dynatrace.com/hub/detail/dynatrace-mcp-server/)!** Now available - no local setup required, connect instantly to your Dynatrace environment from any MCP-compatible client.
|
|
28
|
+
|
|
27
29
|
The local _Dynatrace MCP server_ allows AI Assistants to interact with the [Dynatrace](https://www.dynatrace.com/) observability platform,
|
|
28
30
|
bringing real-time observability data directly into your development workflow.
|
|
29
31
|
|
|
@@ -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");
|
|
@@ -31,6 +35,7 @@ const dynatrace_connection_utils_1 = require("./utils/dynatrace-connection-utils
|
|
|
31
35
|
const proxy_config_1 = require("./utils/proxy-config");
|
|
32
36
|
const list_exceptions_1 = require("./capabilities/list-exceptions");
|
|
33
37
|
const notebooks_1 = require("./capabilities/notebooks");
|
|
38
|
+
const environment_url_parser_1 = require("./utils/environment-url-parser");
|
|
34
39
|
const DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID = 'dt0s12.local-dt-mcp-server';
|
|
35
40
|
// Rate limiting state: store timestamps of tool calls
|
|
36
41
|
let toolCallTimestamps = [];
|
|
@@ -75,6 +80,9 @@ const allRequiredScopes = scopesBase.concat([
|
|
|
75
80
|
'document:documents:write', // Create and update documents
|
|
76
81
|
]);
|
|
77
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');
|
|
78
86
|
console.error(`Initializing Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
79
87
|
// Configure proxy from environment variables early in the startup process
|
|
80
88
|
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
@@ -94,8 +102,10 @@ const main = async () => {
|
|
|
94
102
|
console.error('No OAuth credentials or platform token provided - switching to OAuth authorization code flow.');
|
|
95
103
|
oauthClientId = DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID; // Default OAuth client ID for auth code flow
|
|
96
104
|
}
|
|
105
|
+
// Parse environment information for telemetry
|
|
106
|
+
const environmentInfo = (0, environment_url_parser_1.parseEnvironmentUrl)(dtEnvironment);
|
|
97
107
|
// Initialize usage tracking
|
|
98
|
-
const telemetry = (0, telemetry_openkit_1.createTelemetry)();
|
|
108
|
+
const telemetry = (0, telemetry_openkit_1.createTelemetry)(environmentInfo);
|
|
99
109
|
await telemetry.trackMcpServerStart();
|
|
100
110
|
// Create a shutdown handler that takes shutdown operations as parameters
|
|
101
111
|
const shutdownHandler = (...shutdownOps) => {
|
|
@@ -178,9 +188,9 @@ const main = async () => {
|
|
|
178
188
|
}
|
|
179
189
|
// Ready to start the server
|
|
180
190
|
console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
191
|
+
// Wraps a string-returning tool callback with rate limiting, telemetry, and error handling
|
|
192
|
+
const wrapToolCallback = (name, cb) => {
|
|
193
|
+
return async (args, _extra) => {
|
|
184
194
|
// Capture starttime for telemetry and rate limiting
|
|
185
195
|
const startTime = Date.now();
|
|
186
196
|
/**
|
|
@@ -207,9 +217,16 @@ const main = async () => {
|
|
|
207
217
|
// call the tool
|
|
208
218
|
const response = await cb(args);
|
|
209
219
|
toolCallSuccessful = true;
|
|
210
|
-
return {
|
|
211
|
-
|
|
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 }],
|
|
212
225
|
};
|
|
226
|
+
if (_meta) {
|
|
227
|
+
result._meta = _meta;
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
213
230
|
}
|
|
214
231
|
catch (error) {
|
|
215
232
|
// Track error
|
|
@@ -236,12 +253,15 @@ const main = async () => {
|
|
|
236
253
|
.catch((e) => console.warn('Failed to track tool usage:', e));
|
|
237
254
|
}
|
|
238
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) => {
|
|
239
259
|
server.registerTool(name, {
|
|
240
260
|
title: title,
|
|
241
261
|
description: description,
|
|
242
262
|
inputSchema: zod_1.z.object(paramsSchema),
|
|
243
263
|
annotations: annotations,
|
|
244
|
-
}, (args) =>
|
|
264
|
+
}, (args) => wrapToolCallback(name, cb)(args));
|
|
245
265
|
};
|
|
246
266
|
/**
|
|
247
267
|
* Helper function to request human approval for potentially sensitive operations
|
|
@@ -420,13 +440,13 @@ const main = async () => {
|
|
|
420
440
|
const validSmartscapeEntities = smartscapeResult.records.filter((entity) => !!(entity && entity.id && entity.type && entity.name));
|
|
421
441
|
let resp = `Found ${validSmartscapeEntities.length} monitored entities via Smartscape! Displaying the first ${Math.min(maxEntitiesToDisplay, validSmartscapeEntities.length)} valid entities:\n`;
|
|
422
442
|
validSmartscapeEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
|
|
423
|
-
resp += `- Entity '${entity.name}' of entity-type '${entity.type}' has entity id '${entity.id}' and tags ${entity['tags'] ? JSON.stringify(entity['tags']) : 'none'} - DQL Filter: '| filter dt.smartscape.${String(entity.type).toLowerCase()} == "${entity.id}"'\n`;
|
|
443
|
+
resp += `- Entity '${entity.name}' of entity-type '${entity.type}' has entity id '${entity.id}' and tags ${entity['tags'] ? JSON.stringify(entity['tags']) : 'none'} - DQL Filter: '| filter dt.smartscape.${String(entity.type).toLowerCase()} == toSmartscapeId("${entity.id}")'\n`;
|
|
424
444
|
});
|
|
425
445
|
resp +=
|
|
426
446
|
'\n\n**Next Steps:**\n' +
|
|
427
447
|
'1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statement: "smartscapeNodes \"<entity-type>\" | filter id == <entity-id>"\n' +
|
|
428
448
|
'2. Perform a sanity check that found entities are actually the ones you are looking for, by comparing name and by type (hosts vs. containers vs. apps vs. functions) and technology (Java, TypeScript, .NET) with what is available in the local source code repo.\n' +
|
|
429
|
-
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.smartscape.<entity-type> == <entity-id> | limit 20"\n' +
|
|
449
|
+
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.smartscape.<entity-type> == toSmartscapeId(<entity-id>) | limit 20"\n' +
|
|
430
450
|
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n' +
|
|
431
451
|
'5. Explore dependency & relationships with: "smartscapeEdges \"*\" | filter source_id == <entity-id> or target_id == <entity-id>" to list inbound/outbound edges (depends_on, dependency_of, owned_by, part_of) for graph context\n';
|
|
432
452
|
return resp;
|
|
@@ -495,26 +515,39 @@ const main = async () => {
|
|
|
495
515
|
}
|
|
496
516
|
return resp;
|
|
497
517
|
});
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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 }) => {
|
|
518
551
|
// Create a HTTP Client that has all storage:*:read scopes
|
|
519
552
|
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
|
|
520
553
|
'storage:logs:read', // Read logs for reliability guardian validations
|
|
@@ -578,7 +611,25 @@ const main = async () => {
|
|
|
578
611
|
}
|
|
579
612
|
result += `\nš **Query Results**: (${response.records?.length || 0} records):\n\n`;
|
|
580
613
|
result += `\`\`\`json\n${JSON.stringify(response.records, null, 2)}\n\`\`\``;
|
|
614
|
+
// Include field type definitions for chart rendering in the UI
|
|
615
|
+
if (response.types && response.types.length > 0) {
|
|
616
|
+
result += `\n\nš **Field Types**:\n\n`;
|
|
617
|
+
result += `\`\`\`json:types\n${JSON.stringify(response.types)}\n\`\`\``;
|
|
618
|
+
}
|
|
619
|
+
// Include analysisTimeframe metadata for chart rendering in the UI.
|
|
620
|
+
// This is needed when timeseries results lack explicit timeframe/interval
|
|
621
|
+
// columns (e.g. timeseries queries with fieldsRemove or custom projections).
|
|
622
|
+
if (response.metadata?.grail?.analysisTimeframe) {
|
|
623
|
+
result += `\n\n\`\`\`json:analysisTimeframe\n${JSON.stringify(response.metadata.grail.analysisTimeframe)}\n\`\`\``;
|
|
624
|
+
}
|
|
581
625
|
return result;
|
|
626
|
+
}));
|
|
627
|
+
// MCP App: Register the HTML resource for the execute_dql interactive UI (MCP App)
|
|
628
|
+
registerAppResource(server, 'DQL Results Viewer', executeDqlResourceUri, {}, async () => {
|
|
629
|
+
const html = (0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, 'ui', 'execute-dql', 'index.html'), 'utf-8');
|
|
630
|
+
return {
|
|
631
|
+
contents: [{ uri: executeDqlResourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
|
|
632
|
+
};
|
|
582
633
|
});
|
|
583
634
|
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.', {
|
|
584
635
|
text: zod_1.z
|
|
@@ -1145,8 +1196,8 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
|
|
|
1145
1196
|
main().catch(async (error) => {
|
|
1146
1197
|
console.error('Fatal error in main():', error);
|
|
1147
1198
|
try {
|
|
1148
|
-
// report error in main
|
|
1149
|
-
const telemetry = (0, telemetry_openkit_1.createTelemetry)();
|
|
1199
|
+
// report error in main - use unknown environment info since we might not have parsed it yet
|
|
1200
|
+
const telemetry = (0, telemetry_openkit_1.createTelemetry)({ environmentId: 'unknown', stage: 'unknown' });
|
|
1150
1201
|
await telemetry.trackError(error, 'main_error');
|
|
1151
1202
|
await telemetry.shutdown();
|
|
1152
1203
|
}
|