@dynatrace-oss/dynatrace-mcp-server 0.9.2 → 0.10.0

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
@@ -321,6 +321,7 @@ Depending on the features you are using, the following scopes are needed:
321
321
  - `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
322
322
  - `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
323
323
  - `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
324
+ - `storage:smartscape:read` - needed for `execute_dql` tool to read Smartscape Data
324
325
  - `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
325
326
  - `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
326
327
  - `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.findMonitoredEntitiesByName = exports.generateDqlSearchEntityCommand = void 0;
3
+ exports.findMonitoredEntitiesByName = exports.findMonitoredEntityViaSmartscapeByName = exports.generateDqlSearchEntityCommand = void 0;
4
4
  const execute_dql_1 = require("./execute-dql");
5
5
  const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
6
6
  /**
@@ -22,10 +22,35 @@ const generateDqlSearchEntityCommand = (entityNames, extendedSearch) => {
22
22
  };
23
23
  exports.generateDqlSearchEntityCommand = generateDqlSearchEntityCommand;
24
24
  /**
25
- * Find a monitored entity by name via DQL
25
+ * Find a monitored entity via "smartscapeNodes" by name via DQL
26
26
  * @param dtClient
27
- * @param entityName
28
- * @returns A string with the entity details like id, name and type, or an error message if no entity was found
27
+ * @param entityNames Array of entitiy names to search for
28
+ * @returns An array with the entity details like id, name and type
29
+ */
30
+ const findMonitoredEntityViaSmartscapeByName = async (dtClient, entityNames) => {
31
+ const dql = `smartscapeNodes "*" | search "*${entityNames.join('*" OR "*')}*" | fields id, name, type`;
32
+ console.error(`Executing DQL: ${dql}`);
33
+ try {
34
+ const smartscapeResult = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
35
+ if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
36
+ // return smartscape results if we found something
37
+ return smartscapeResult;
38
+ }
39
+ }
40
+ catch (error) {
41
+ // ignore errors here, as smartscapeNodes may not be ready for all environments/users
42
+ console.error('Error while querying smartscapeNodes:', error);
43
+ }
44
+ console.error('No results from smartscapeNodes');
45
+ return null;
46
+ };
47
+ exports.findMonitoredEntityViaSmartscapeByName = findMonitoredEntityViaSmartscapeByName;
48
+ /**
49
+ * Find a monitored entity via "dt.entity.${entityType}" by name via DQL
50
+ * @param dtClient
51
+ * @param entityNames Array of entitiy names to search for
52
+ * @param extendedSearch If true, search over all entity types, otherwise only basic ones
53
+ * @returns An array with the entity details like id, name and type
29
54
  */
30
55
  const findMonitoredEntitiesByName = async (dtClient, entityNames, extendedSearch) => {
31
56
  // construct a DQL statement for searching the entityName over all entity types
package/dist/index.js CHANGED
@@ -91,7 +91,6 @@ const main = async () => {
91
91
  // Unpack environment variables
92
92
  let { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId, grailBudgetGB } = dynatraceEnv;
93
93
  // Infer OAuth auth code flow if no OAuth Client credentials are provided
94
- // -> configure default OAuth client ID for auth code flow
95
94
  if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
96
95
  console.error('No OAuth credentials or platform token provided - switching to OAuth authorization code flow.');
97
96
  oauthClientId = DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID; // Default OAuth client ID for auth code flow
@@ -364,22 +363,42 @@ const main = async () => {
364
363
  }, {
365
364
  readOnlyHint: true,
366
365
  }, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
367
- const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read'));
366
+ const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read', 'storage:smartscape:read'));
367
+ const smartscapeResult = await (0, find_monitored_entity_by_name_1.findMonitoredEntityViaSmartscapeByName)(dtClient, entityNames);
368
+ if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
369
+ // Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
370
+ const validSmartscapeEntities = smartscapeResult.records.filter((entity) => !!(entity && entity.id && entity.type && entity.name));
371
+ let resp = `Found ${validSmartscapeEntities.length} monitored entities via Smartscape! Displaying the first ${Math.min(maxEntitiesToDisplay, validSmartscapeEntities.length)} valid entities:\n`;
372
+ validSmartscapeEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
373
+ 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`;
374
+ });
375
+ resp +=
376
+ '\n\n**Next Steps:**\n' +
377
+ '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' +
378
+ '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' +
379
+ '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' +
380
+ '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n' +
381
+ '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';
382
+ return resp;
383
+ }
384
+ // If no result from Smartscape, try the classic entities API
368
385
  const result = await (0, find_monitored_entity_by_name_1.findMonitoredEntitiesByName)(dtClient, entityNames, extendedSearch);
369
386
  if (result && result.records && result.records.length > 0) {
370
- let resp = `Found ${result.records.length} monitored entities! Displaying the first ${maxEntitiesToDisplay} entities:\n`;
387
+ // Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
388
+ const validClassicEntities = result.records.filter((entity) => !!(entity && entity.id && entity.type && entity.name));
389
+ let resp = `Found ${validClassicEntities.length} monitored entities! Displaying the first ${Math.min(maxEntitiesToDisplay, validClassicEntities.length)} entities:\n`;
371
390
  // iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
372
- result.records.slice(0, maxEntitiesToDisplay).forEach((entity) => {
391
+ validClassicEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
373
392
  if (entity && entity.id) {
374
393
  const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(String(entity.id));
375
- resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']}' has entity id '${entity.id}' and tags ${entity['tags'] ? entity['tags'] : 'none'} - Use the DQL Filter: '| filter ${entityType} == "${entity.id}"'\n`;
394
+ resp += `- Entity '${entity['entity.name']}' of entity-type '${entity['entity.type']}' has entity id '${entity.id}' and tags ${entity['tags'] ? entity['tags'] : 'none'} - DQL Filter: '| filter ${entityType} == "${entity.id}"'\n`;
376
395
  }
377
396
  });
378
397
  resp +=
379
398
  '\n\n**Next Steps:**\n' +
380
- '1. Try to fetch more details about the entity, using the `execute_dql` tool with "describe(dt.entity.<entity-type>)", and "fetch dt.entity.<entity-type> | filter id == <entity-id> | fieldsAdd <field-1>, <field2>, ..."\n' +
399
+ '1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statements: "describe(dt.entity.<entity-type>)", and "fetch dt.entity.<entity-type> | filter id == <entity-id> | fieldsAdd <field-1>, <field-2>, ..."\n' +
381
400
  '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' +
382
- '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.entity.<entity-type> == <entity-id>"\n' +
401
+ '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.entity.<entity-type> == <entity-id> | limit 20"\n' +
383
402
  '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n';
384
403
  return resp;
385
404
  }
@@ -428,20 +447,26 @@ const main = async () => {
428
447
  }
429
448
  return resp;
430
449
  });
431
- tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
450
+ tool('execute_dql', 'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
432
451
  'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
433
452
  'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
434
453
  dqlStatement: zod_1.z
435
454
  .string()
436
- .describe('DQL Statement (Ex: "fetch [logs, spans, events], 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) }"). ' +
455
+ .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>"]"). ' +
437
456
  '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. '),
457
+ recordLimit: zod_1.z.number().optional().default(100).describe('Maximum number of records to return (default: 100)'),
458
+ recordSizeLimitMB: zod_1.z
459
+ .number()
460
+ .optional()
461
+ .default(1)
462
+ .describe('Maximum size of the returned records in MB (default: 1MB)'),
438
463
  }, {
439
464
  // not readonly (DQL statements may modify things), not idempotent (may change over time)
440
465
  readOnlyHint: false,
441
466
  idempotentHint: false,
442
467
  // 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
443
468
  openWorldHint: true,
444
- }, async ({ dqlStatement }) => {
469
+ }, async ({ dqlStatement, recordLimit = 100, recordSizeLimitMB = 1 }) => {
445
470
  // Create a HTTP Client that has all storage:*:read scopes
446
471
  const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
447
472
  'storage:logs:read', // Read logs for reliability guardian validations
@@ -453,8 +478,9 @@ const main = async () => {
453
478
  'storage:system:read', // Read System Data from Grail
454
479
  'storage:user.events:read', // Read User events from Grail
455
480
  'storage:user.sessions:read', // Read User sessions from Grail
456
- 'storage:security.events:read'));
457
- const response = await (0, execute_dql_1.executeDql)(dtClient, { query: dqlStatement }, grailBudgetGB);
481
+ 'storage:security.events:read', // Read Security events from Grail
482
+ 'storage:smartscape:read'));
483
+ const response = await (0, execute_dql_1.executeDql)(dtClient, { query: dqlStatement, maxResultRecords: recordLimit, maxResultBytes: recordSizeLimitMB * 1024 * 1024 }, grailBudgetGB);
458
484
  if (!response) {
459
485
  return 'DQL execution failed or returned no result.';
460
486
  }
@@ -492,6 +518,9 @@ const main = async () => {
492
518
  if (response.sampled !== undefined && response.sampled) {
493
519
  result += `- **⚠️ Sampling Used:** Yes (results may be approximate)\n`;
494
520
  }
521
+ if (response.records.length === recordLimit) {
522
+ 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`;
523
+ }
495
524
  result += `\n📋 **Query Results**: (${response.records?.length || 0} records):\n\n`;
496
525
  result += `\`\`\`json\n${JSON.stringify(response.records, null, 2)}\n\`\`\``;
497
526
  return result;
@@ -215,8 +215,8 @@ function createTelemetry() {
215
215
  return new DynatraceMcpTelemetry();
216
216
  }
217
217
  catch (e) {
218
- // Failed to initialize
219
- console.error(e);
218
+ // Failed to initialize (unexpected). Log concise message without stack trace spam.
219
+ console.error('Dynatrace Telemetry initialization failed:', e.message);
220
220
  // fallback to NoOp Telemetry
221
221
  return new NoOpTelemetry();
222
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Dynatrace",
6
6
  "keywords": [