@dynatrace-oss/dynatrace-mcp-server 1.4.0 → 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.
@@ -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
@@ -581,7 +611,25 @@ const main = async () => {
581
611
  }
582
612
  result += `\nšŸ“‹ **Query Results**: (${response.records?.length || 0} records):\n\n`;
583
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
+ }
584
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
+ };
585
633
  });
586
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.', {
587
635
  text: zod_1.z