@dynatrace-oss/dynatrace-mcp-server 0.6.1 → 0.7.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.
@@ -16,8 +16,29 @@
16
16
  * in Dynatrace, including problem events, security issues, logs, metrics, and spans.
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.generateDqlFromNaturalLanguage = void 0;
19
+ exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.generateDqlFromNaturalLanguage = exports.isDavisCopilotSkillAvailable = exports.DAVIS_COPILOT_DOCS = void 0;
20
20
  const client_davis_copilot_1 = require("@dynatrace-sdk/client-davis-copilot");
21
+ // Documentation links for Davis Copilot
22
+ exports.DAVIS_COPILOT_DOCS = {
23
+ ENABLE_COPILOT: 'https://docs.dynatrace.com/docs/discover-dynatrace/platform/davis-ai/copilot/copilot-getting-started#enable-davis-copilot',
24
+ };
25
+ /**
26
+ * Check if a specific Davis Copilot skill is available
27
+ * Returns true if the skill is available, false otherwise
28
+ */
29
+ const isDavisCopilotSkillAvailable = async (dtClient, skill) => {
30
+ try {
31
+ const client = new client_davis_copilot_1.PublicClient(dtClient);
32
+ const response = await client.listAvailableSkills();
33
+ const availableSkills = response.skills || [];
34
+ return availableSkills.includes(skill);
35
+ }
36
+ catch (error) {
37
+ // If Davis Copilot is not enabled or any other error occurs, return false
38
+ return false;
39
+ }
40
+ };
41
+ exports.isDavisCopilotSkillAvailable = isDavisCopilotSkillAvailable;
21
42
  /**
22
43
  * Generate DQL from natural language
23
44
  * Converts plain English descriptions into powerful Dynatrace Query Language (DQL) statements.
@@ -1,17 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.findMonitoredEntityByName = exports.generateDqlSearchEntityCommand = void 0;
3
+ exports.findMonitoredEntitiesByName = 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
  /**
7
- * Construct a DQL statement like "fetch <entityType> | search "*<entityName>*" | fieldsAdd entity.type" for each entity type,
7
+ * Construct a DQL statement like "fetch <entityType> | search "*<entityName1>*" OR "*<entityName2>*" | fieldsAdd entity.type" for each entity type,
8
8
  * and join them with " | append [ ... ]"
9
9
  * @param entityName
10
10
  * @returns DQL Statement for searching all entity types
11
11
  */
12
- const generateDqlSearchEntityCommand = (entityName) => {
13
- const fetchDqlCommands = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.map((entityType, index) => {
14
- const dql = `fetch ${entityType} | search "*${entityName}*" | fieldsAdd entity.type`;
12
+ const generateDqlSearchEntityCommand = (entityNames, extendedSearch) => {
13
+ // If extendedSearch is true, use all entity types, otherwise use only basic ones
14
+ const fetchDqlCommands = (extendedSearch ? dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL : dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_BASICS).map((entityType, index) => {
15
+ const dql = `fetch ${entityType} | search "*${entityNames.join('*" OR "*')}*" | fieldsAdd entity.type | expand tags`;
15
16
  if (index === 0) {
16
17
  return dql;
17
18
  }
@@ -26,27 +27,11 @@ exports.generateDqlSearchEntityCommand = generateDqlSearchEntityCommand;
26
27
  * @param entityName
27
28
  * @returns A string with the entity details like id, name and type, or an error message if no entity was found
28
29
  */
29
- const findMonitoredEntityByName = async (dtClient, entityName) => {
30
- if (!entityName) {
31
- return 'You need to provide an entity name to search for.';
32
- }
30
+ const findMonitoredEntitiesByName = async (dtClient, entityNames, extendedSearch) => {
33
31
  // construct a DQL statement for searching the entityName over all entity types
34
- const dql = (0, exports.generateDqlSearchEntityCommand)(entityName);
32
+ const dql = (0, exports.generateDqlSearchEntityCommand)(entityNames, extendedSearch);
35
33
  // Get response from API
36
34
  // Note: This may be slow, as we are appending multiple entity types above
37
- const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
38
- if (dqlResponse && dqlResponse.records && dqlResponse.records.length > 0) {
39
- let resp = 'The following monitored entities were found:\n';
40
- // iterate over dqlResponse and create a string with the entity names
41
- dqlResponse.records.forEach((entity) => {
42
- if (entity) {
43
- resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']} has entity id '${entity.id}'\n`;
44
- }
45
- });
46
- return resp;
47
- }
48
- else {
49
- return 'No monitored entity found with the specified name.';
50
- }
35
+ return await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
51
36
  };
52
- exports.findMonitoredEntityByName = findMonitoredEntityByName;
37
+ exports.findMonitoredEntitiesByName = findMonitoredEntitiesByName;
@@ -5,27 +5,43 @@ const find_monitored_entity_by_name_1 = require("./find-monitored-entity-by-name
5
5
  describe('generateDqlSearchCommand', () => {
6
6
  beforeEach(() => {
7
7
  // Ensure we have at least some entity types for testing
8
- expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.length).toBeGreaterThan(0);
8
+ expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length).toBeGreaterThan(0);
9
9
  });
10
- it('should include all entity types from DYNATRACE_ENTITY_TYPES', () => {
10
+ it('should include all entity types from DYNATRACE_ENTITY_TYPES_ALL', () => {
11
11
  const entityName = 'test';
12
- const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityName);
12
+ const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)([entityName], true);
13
13
  console.log(result);
14
14
  // Check that all entity types are included in the DQL
15
- dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.forEach((entityType) => {
15
+ dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.forEach((entityType) => {
16
+ expect(result).toContain(`fetch ${entityType}`);
17
+ });
18
+ });
19
+ it('should include entity types from DYNATRACE_ENTITY_TYPES_BASICS', () => {
20
+ const entityName = 'test';
21
+ const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)([entityName], false);
22
+ console.log(result);
23
+ // Check that all entity types are included in the DQL
24
+ dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_BASICS.forEach((entityType) => {
16
25
  expect(result).toContain(`fetch ${entityType}`);
17
26
  });
18
27
  });
19
28
  it('should structure the DQL correctly with first fetch and subsequent appends', () => {
20
29
  const entityName = 'test';
21
- const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityName);
30
+ const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)([entityName], true);
22
31
  // First entity type should not have append prefix
23
- const firstEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES[0];
24
- expect(result).toContain(`fetch ${firstEntityType} | search "*${entityName}*" | fieldsAdd entity.type`);
32
+ const firstEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL[0];
33
+ expect(result).toContain(`fetch ${firstEntityType} | search "*${entityName}*" | fieldsAdd entity.type | expand tags`);
25
34
  // Subsequent entity types should have append prefix (if there are more than 1)
26
- if (dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.length > 1) {
27
- const secondEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES[1];
28
- expect(result).toContain(` | append [ fetch ${secondEntityType} | search "*${entityName}*" | fieldsAdd entity.type ]`);
35
+ if (dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length > 1) {
36
+ const secondEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL[1];
37
+ expect(result).toContain(` | append [ fetch ${secondEntityType} | search "*${entityName}*" | fieldsAdd entity.type | expand tags ]`);
29
38
  }
30
39
  });
40
+ it('should handle multiple entityNames correctly', () => {
41
+ const entityNames = ['test1', 'test2', 'example'];
42
+ const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityNames, true);
43
+ // Check that the search part includes all entity names joined by OR
44
+ const searchPart = `search "*test1*" OR "*test2*" OR "*example*"`;
45
+ expect(result).toContain(searchPart);
46
+ });
31
47
  });
@@ -11,16 +11,20 @@ function getDynatraceEnv(env = process.env) {
11
11
  const dtPlatformToken = env.DT_PLATFORM_TOKEN;
12
12
  const dtEnvironment = env.DT_ENVIRONMENT;
13
13
  const slackConnectionId = env.SLACK_CONNECTION_ID || 'fake-slack-connection-id';
14
- const grailBudgetGB = parseFloat(env.DT_GRAIL_QUERY_BUDGET_GB || '1000'); // Default to 1000 GB
14
+ let grailBudgetGB = parseFloat(env.DT_GRAIL_QUERY_BUDGET_GB || '1000'); // Default to 1000 GB
15
15
  if (!dtEnvironment) {
16
16
  throw new Error('Please set DT_ENVIRONMENT environment variable to your Dynatrace Platform Environment');
17
17
  }
18
18
  if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
19
19
  throw new Error('Please set either OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, or DT_PLATFORM_TOKEN environment variables');
20
20
  }
21
+ // For dev and hardening stages, set unlimited budget (-1) unless explicitly overridden
22
+ if (dtEnvironment.includes('apps.dynatracelabs.com') && !env.DT_GRAIL_QUERY_BUDGET_GB) {
23
+ grailBudgetGB = -1;
24
+ }
21
25
  // ToDo: Allow the case of -1 for unlimited Budget
22
- if (isNaN(grailBudgetGB) || (grailBudgetGB <= 0 && grailBudgetGB !== -1)) {
23
- throw new Error('DT_GRAIL_QUERY_BUDGET_GB must be a positive number representing GB budget for Grail queries');
26
+ if (isNaN(grailBudgetGB) || (grailBudgetGB < 0 && grailBudgetGB !== -1)) {
27
+ throw new Error('DT_GRAIL_QUERY_BUDGET_GB must be a positive number or -1 (for unlimited) representing GB budget for Grail queries');
24
28
  }
25
29
  if (!dtEnvironment.startsWith('https://')) {
26
30
  throw new Error('Please set DT_ENVIRONMENT to a valid Dynatrace Environment URL (e.g., https://<environment-id>.apps.dynatrace.com)');
@@ -71,4 +71,36 @@ describe('getDynatraceEnv', () => {
71
71
  };
72
72
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
73
73
  });
74
+ it('Defaults the Grail Budget to 1000', () => {
75
+ const env = {
76
+ ...baseEnv,
77
+ GRAIL_BUDGET_GB: undefined,
78
+ DT_ENVIRONMENT: 'https://abc123.apps.dynatrace.com',
79
+ };
80
+ const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
81
+ expect(result).toEqual({
82
+ oauthClientId: env.OAUTH_CLIENT_ID,
83
+ oauthClientSecret: env.OAUTH_CLIENT_SECRET,
84
+ dtEnvironment: env.DT_ENVIRONMENT,
85
+ dtPlatformToken: env.DT_PLATFORM_TOKEN,
86
+ slackConnectionId: env.SLACK_CONNECTION_ID,
87
+ grailBudgetGB: 1000, // Default value
88
+ });
89
+ });
90
+ it('Resets the Grail Budget if a dev/sprint URL is used', () => {
91
+ const env = {
92
+ ...baseEnv,
93
+ GRAIL_BUDGET_GB: undefined,
94
+ DT_ENVIRONMENT: 'https://abc123.dev.apps.dynatracelabs.com',
95
+ };
96
+ const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
97
+ expect(result).toEqual({
98
+ oauthClientId: env.OAUTH_CLIENT_ID,
99
+ oauthClientSecret: env.OAUTH_CLIENT_SECRET,
100
+ dtEnvironment: env.DT_ENVIRONMENT,
101
+ dtPlatformToken: env.DT_PLATFORM_TOKEN,
102
+ slackConnectionId: env.SLACK_CONNECTION_ID,
103
+ grailBudgetGB: -1, // Default value for dynatracelabs.com
104
+ });
105
+ });
74
106
  });
package/dist/index.js CHANGED
@@ -14,7 +14,6 @@ const version_1 = require("./utils/version");
14
14
  const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
15
15
  const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
16
16
  const list_problems_1 = require("./capabilities/list-problems");
17
- const get_monitored_entity_details_1 = require("./capabilities/get-monitored-entity-details");
18
17
  const get_ownership_information_1 = require("./capabilities/get-ownership-information");
19
18
  const get_events_for_cluster_1 = require("./capabilities/get-events-for-cluster");
20
19
  const create_workflow_for_problem_notification_1 = require("./capabilities/create-workflow-for-problem-notification");
@@ -26,6 +25,7 @@ const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-e
26
25
  const davis_copilot_1 = require("./capabilities/davis-copilot");
27
26
  const getDynatraceEnv_1 = require("./getDynatraceEnv");
28
27
  const telemetry_openkit_1 = require("./utils/telemetry-openkit");
28
+ const dynatrace_entity_types_1 = require("./utils/dynatrace-entity-types");
29
29
  const grail_budget_tracker_1 = require("./utils/grail-budget-tracker");
30
30
  // Load environment variables from .env file if available, and suppress warnings/logging to stdio
31
31
  // as it breaks MCP communication when using stdio transport
@@ -139,6 +139,7 @@ const main = async () => {
139
139
  }, {
140
140
  capabilities: {
141
141
  tools: {},
142
+ elicitation: {},
142
143
  },
143
144
  });
144
145
  // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
@@ -183,6 +184,38 @@ const main = async () => {
183
184
  };
184
185
  server.tool(name, description, paramsSchema, annotations, (args) => wrappedCb(args));
185
186
  };
187
+ /**
188
+ * Helper function to request human approval for potentially sensitive operations
189
+ * @param operation - Description of the operation requiring approval
190
+ * @returns Promise<boolean> - true if approved, false if declined or cancelled
191
+ */
192
+ const requestHumanApproval = async (operation) => {
193
+ try {
194
+ const result = await server.server.elicitInput({
195
+ message: `Please review: ${operation}`,
196
+ requestedSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ approval: {
200
+ type: 'boolean',
201
+ title: 'Approve this operation?',
202
+ description: 'Select true to approve this operation, or false to decline.',
203
+ default: false,
204
+ },
205
+ },
206
+ required: ['approval'],
207
+ },
208
+ });
209
+ if (result.action === 'accept' && result.content?.approval === true) {
210
+ return true;
211
+ }
212
+ return false;
213
+ }
214
+ catch (error) {
215
+ console.error('Failed to elicit human approval:', error);
216
+ return false; // Default to deny if elicitation fails
217
+ }
218
+ };
186
219
  /** Tool Definitions below */
187
220
  tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, {
188
221
  readOnlyHint: true,
@@ -196,7 +229,7 @@ const main = async () => {
196
229
  resp += `You can reach it via ${dtEnvironment}\n`;
197
230
  return resp;
198
231
  });
199
- tool('list_vulnerabilities', 'List all non-muted vulnerabilities from Dynatrace for the last 30 days. An additional filter can be provided using DQL filter.', {
232
+ tool('list_vulnerabilities', 'Retrieve all active (non-muted) vulnerabilities from Dynatrace for the last 30 days. An additional filter can be provided using DQL filter (filter for a specific entity type and id).', {
200
233
  riskScore: zod_1.z
201
234
  .number()
202
235
  .optional()
@@ -205,7 +238,8 @@ const main = async () => {
205
238
  additionalFilter: zod_1.z
206
239
  .string()
207
240
  .optional()
208
- .describe('Additional filter for DQL statement for vulnerabilities, e.g., \'vulnerability.stack == "CODE_LIBRARY"\' or \'vulnerability.risk.level == "CRITICAL"\' or \'affected_entity.name contains "prod"\' or \'vulnerability.davis_assessment.exposure_status == "PUBLIC_NETWORK"\''),
241
+ .describe('Additional DQL-based filter for accessing vulnerabilities, e.g., by entity type (preferred), like \'dt.entity.<service|host|application|$type> == "<entity-id>"\', by entity name (not recommended) \'affected_entity.name contains "<entity-name>"\' , or by tags \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\'. ' +
242
+ 'You can also filter by vulnerability details like \'vulnerability.stack == "CODE_LIBRARY"\' or \'vulnerability.risk.level == "CRITICAL"\' or \'vulnerability.davis_assessment.exposure_status == "PUBLIC_NETWORK"\''),
209
243
  maxVulnerabilitiesToDisplay: zod_1.z
210
244
  .number()
211
245
  .default(25)
@@ -247,11 +281,11 @@ const main = async () => {
247
281
  `\n3. Last but not least, tell the user to visit ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities/vulnerabilities/<vulnerability-id> for full details.`;
248
282
  return resp;
249
283
  });
250
- tool('list_problems', 'List all problems (dt.davis.problems) known on Dynatrace, sorted by their recency, for the last 12h. An additional filter can be provided using DQL filter.', {
284
+ tool('list_problems', 'List all problems (dt.davis.problems) known on Dynatrace, sorted by their recency, for the last 12h. An additional DQL based filter, like filtering for specific entities, can be provided.', {
251
285
  additionalFilter: zod_1.z
252
286
  .string()
253
287
  .optional()
254
- .describe('Additional filter for DQL statement for dt.davis.problems, e.g., \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\''),
288
+ .describe('Additional DQL filter for dt.davis.problems - filter by entity type (preferred), like \'dt.entity.<service|host|application|$type> == "<entity-id>"\', or by entity tags \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\''),
255
289
  maxProblemsToDisplay: zod_1.z.number().default(10).describe('Maximum number of problems to display in the response.'),
256
290
  }, {
257
291
  readOnlyHint: true,
@@ -287,65 +321,61 @@ const main = async () => {
287
321
  return 'No problems found';
288
322
  }
289
323
  });
290
- tool('find_entity_by_name', 'Get the entityId of a monitored entity (service, host, process-group, application, kubernetes-node, ...) within the topology based on the name of the entity on Dynatrace', {
291
- entityName: zod_1.z.string().describe('Name of the entity to search for, e.g., "my-service" or "my-host"'),
292
- }, {
293
- readOnlyHint: true,
294
- }, async ({ entityName }) => {
295
- const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
296
- const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
297
- return entityResponse;
298
- });
299
- tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
300
- entityId: zod_1.z.string().optional(),
324
+ tool('find_entity_by_name', 'Find the entityId and type of a monitored entity (service, host, process-group, application, kubernetes-node, custom-app, ...) within the topology on Dynatrace, based on the name of the entity. Run this before querying data like logs, metrics, problems, events. If no entity name is known, make an educated guess with common identifiers like package.json `id`/`name`, helm chart names, kubernetes manfiest names, and alike.', {
325
+ entityNames: zod_1.z
326
+ .array(zod_1.z.string())
327
+ .describe('Names of the entities to search for - try with one name at first (identifiers like package.json id), and only try with multiple names if the first search was unsuccessful'),
328
+ maxEntitiesToDisplay: zod_1.z.number().default(10).describe('Maximum number of entities to display in the response.'),
329
+ extendedSearch: zod_1.z
330
+ .boolean()
331
+ .optional()
332
+ .default(false)
333
+ .describe('Set this to true if you want a comprehensive search over all available entity types.'),
301
334
  }, {
302
335
  readOnlyHint: true,
303
- }, async ({ entityId }) => {
336
+ }, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
304
337
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
305
- const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
306
- if (!entityDetails) {
307
- return `No entity found with entityId: ${entityId}`;
308
- }
309
- let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
310
- `Properties: ${JSON.stringify(entityDetails.allProperties)}\n`;
311
- if (entityDetails.type == 'SERVICE') {
312
- resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`;
313
- }
314
- else if (entityDetails.type == 'HOST') {
315
- resp += `You can find more information about the host at ${dtEnvironment}/ui/apps/dynatrace.infraops/hosts/${entityDetails.entityId}`;
316
- }
317
- else if (entityDetails.type == 'KUBERNETES_CLUSTER') {
318
- resp += `You can find more information about the cluster at ${dtEnvironment}/ui/apps/dynatrace.infraops/kubernetes/${entityDetails.entityId}`;
319
- }
320
- else if (entityDetails.type == 'CLOUD_APPLICATION') {
321
- resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}`;
322
- }
323
- resp += `\n\n**Filter**:`;
324
- // Use entityTypeTable as the filter (e.g., fetch logs | filter dt.entity.service == "SERVICE-1234")
325
- if (entityDetails.entityTypeTable) {
326
- resp += ` You can use the following filter to get relevant information from other tools: \`| filter ${entityDetails.entityTypeTable} == "${entityDetails.entityId}"\`. `;
338
+ const result = await (0, find_monitored_entity_by_name_1.findMonitoredEntitiesByName)(dtClient, entityNames, extendedSearch);
339
+ if (result && result.records && result.records.length > 0) {
340
+ let resp = `Found ${result.records.length} monitored entities! Displaying the first ${maxEntitiesToDisplay} entities:\n`;
341
+ // iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
342
+ result.records.slice(0, maxEntitiesToDisplay).forEach((entity) => {
343
+ if (entity && entity.id) {
344
+ const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(String(entity.id));
345
+ 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`;
346
+ }
347
+ });
348
+ resp +=
349
+ '\n\n**Next Steps:**\n' +
350
+ '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' +
351
+ '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' +
352
+ '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' +
353
+ '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n';
354
+ return resp;
327
355
  }
328
356
  else {
329
- resp += ` Try to use search command as follows: \`| search "${entityDetails.entityId}"\`. `;
357
+ return 'No monitored entity found with the specified name. Try to broaden your search term or check for typos.';
330
358
  }
331
- resp += `\n\n**Next Steps**\n\n`;
332
- resp += `1. Find available metrics for this entity, by using execute_dql tool with the following DQL statement: "fetch metric.series" and the filter defined above\n`;
333
- resp += `2. Find out whether any problems exist for this entity using the list_problems tool\n`;
334
- resp += `3. Explore logs for this entity by using execute_dql with "fetch logs" and applying the filter mentioned above'\n`;
335
- return resp;
336
359
  });
337
360
  tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
338
- channel: zod_1.z.string().optional(),
339
- message: zod_1.z.string().optional(),
361
+ channel: zod_1.z.string(),
362
+ message: zod_1.z
363
+ .string()
364
+ .describe('Slack markdown supported. Avoid sending sensitive data like log lines. Focus on context, insights, links, and summaries.'),
340
365
  }, {
341
366
  // not read-only, not open-world, not destructive
342
367
  readOnlyHint: false,
343
368
  }, async ({ channel, message }) => {
369
+ // Request human approval before sending the message
370
+ const approved = await requestHumanApproval(`Send information via Slack to ${channel}`);
371
+ if (!approved) {
372
+ return 'Operation cancelled: Human approval was not granted for sending this Slack message.';
373
+ }
344
374
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
345
375
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
346
376
  return `Message sent to Slack channel: ${JSON.stringify(response)}`;
347
377
  });
348
- tool('verify_dql', 'Verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. This step is recommended for DQL statements that have been dynamically created by non-expert tools. For statements coming from the `generate_dql_from_natural_language` tool as well as from documentation, this step can be omitted.', {
378
+ tool('verify_dql', 'Syntactically verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. Recommended for generated DQL statements. Skip for statements created by `generate_dql_from_natural_language` tool, as well as from documentation.', {
349
379
  dqlStatement: zod_1.z.string(),
350
380
  }, {
351
381
  readOnlyHint: true,
@@ -364,16 +394,17 @@ const main = async () => {
364
394
  resp += `The DQL statement is valid - you can use the "execute_dql" tool.\n`;
365
395
  }
366
396
  else {
367
- resp += `The DQL statement is invalid. Please adapt your statement.\n`;
397
+ resp += `The DQL statement is invalid. Please adapt your statement. Consider using "generate_dql_from_natural_language" tool for help.\n`;
368
398
  }
369
399
  return resp;
370
400
  });
371
401
  tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
372
- 'You can also use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
373
- 'Note: For more information about available fields for filters and aggregation, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
402
+ 'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
403
+ 'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
374
404
  dqlStatement: zod_1.z
375
405
  .string()
376
- .describe('DQL Statement (Ex: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}.", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }")'),
406
+ .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) }"). ' +
407
+ '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. '),
377
408
  }, {
378
409
  // not readonly (DQL statements may modify things), not idempotent (may change over time)
379
410
  readOnlyHint: false,
@@ -444,6 +475,11 @@ const main = async () => {
444
475
  idempotentHint: true,
445
476
  }, async ({ text }) => {
446
477
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:nl2dql:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
478
+ // Check if the nl2dql skill is available
479
+ const isAvailable = await (0, davis_copilot_1.isDavisCopilotSkillAvailable)(dtClient, 'nl2dql');
480
+ if (!isAvailable) {
481
+ return `❌ The DQL generation skill is not available. Please visit: ${davis_copilot_1.DAVIS_COPILOT_DOCS.ENABLE_COPILOT}`;
482
+ }
447
483
  const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
448
484
  let resp = `🔤 Natural Language to DQL:\n\n`;
449
485
  resp += `**Query:** "${text}"\n\n`;
@@ -473,6 +509,11 @@ const main = async () => {
473
509
  idempotentHint: true,
474
510
  }, async ({ dql }) => {
475
511
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:dql2nl:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
512
+ // Check if the dql2nl skill is available
513
+ const isAvailable = await (0, davis_copilot_1.isDavisCopilotSkillAvailable)(dtClient, 'dql2nl');
514
+ if (!isAvailable) {
515
+ return `❌ The DQL explanation skill is not available. Please visit: ${davis_copilot_1.DAVIS_COPILOT_DOCS.ENABLE_COPILOT}`;
516
+ }
476
517
  const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
477
518
  let resp = `📝 DQL to Natural Language:\n\n`;
478
519
  resp += `**DQL Query:**\n\`\`\`\n${dql}\n\`\`\`\n\n`;
@@ -488,7 +529,7 @@ const main = async () => {
488
529
  }
489
530
  return resp;
490
531
  });
491
- tool('chat_with_davis_copilot', 'Use this tool in case no specific tool is available. Get an answer to any Dynatrace related question as well as troubleshooting, and guidance. *(Note: Davis CoPilot AI is GA, but the Davis CoPilot APIs are in preview)*', {
532
+ tool('chat_with_davis_copilot', 'Use this tool to ask any Dynatrace related question, in case no other more specific tool is available.', {
492
533
  text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
493
534
  context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
494
535
  instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
@@ -498,6 +539,11 @@ const main = async () => {
498
539
  openWorldHint: true, // web-search like characteristics
499
540
  }, async ({ text, context, instruction }) => {
500
541
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:conversations:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
542
+ // Check if the conversation skill is available
543
+ const isAvailable = await (0, davis_copilot_1.isDavisCopilotSkillAvailable)(dtClient, 'conversation');
544
+ if (!isAvailable) {
545
+ return `❌ The conversation skill is not available. Please visit: ${davis_copilot_1.DAVIS_COPILOT_DOCS.ENABLE_COPILOT}`;
546
+ }
501
547
  const conversationContext = [];
502
548
  if (context) {
503
549
  conversationContext.push({
@@ -550,6 +596,11 @@ const main = async () => {
550
596
  readOnlyHint: false,
551
597
  idempotentHint: false, // creating the same workflow multiple times is possible
552
598
  }, async ({ problemType, teamName, channel, isPrivate }) => {
599
+ // ask for human approval
600
+ const approved = await requestHumanApproval(`Create a workflow for notifying team ${teamName} via ${channel} about ${problemType} problems`);
601
+ if (!approved) {
602
+ return 'Operation cancelled: Human approval was not granted for creating this workflow.';
603
+ }
553
604
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
554
605
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
555
606
  let resp = `Workflow Created: ${response?.id} with name ${response?.title}.\nYou can access the Workflow via the following link: ${dtEnvironment}/ui/apps/dynatrace.automations/workflows/${response?.id}.\nTell the user to inspect the Workflow by visiting the link.\n`;
@@ -571,6 +622,11 @@ const main = async () => {
571
622
  readOnlyHint: false,
572
623
  idempotentHint: true, // making the same workflow public multiple times yields the same result
573
624
  }, async ({ workflowId }) => {
625
+ // ask for human approval
626
+ const approved = await requestHumanApproval(`Make workflow ${workflowId} publicly available to everyone on the Dynatrace Environment`);
627
+ if (!approved) {
628
+ return 'Operation cancelled: Human approval was not granted for making this workflow public.';
629
+ }
574
630
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
575
631
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
576
632
  isPrivate: false,
@@ -631,7 +687,9 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
631
687
  ccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for CC recipients'),
632
688
  bccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for BCC recipients'),
633
689
  subject: zod_1.z.string().describe('Subject line of the email'),
634
- body: zod_1.z.string().describe('Body content of the email (plain text only)'),
690
+ body: zod_1.z
691
+ .string()
692
+ .describe('Body content of the email (plain text only). Avoid sending sensitive data like log lines. Focus on context, insights, links, and summaries.'),
635
693
  }, {
636
694
  openWorldHint: true, // email is as close to the open-world as we can get with our system
637
695
  }, async ({ toRecipients, ccRecipients, bccRecipients, subject, body }) => {
@@ -640,6 +698,12 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
640
698
  if (totalRecipients > 10) {
641
699
  throw new Error(`Total recipients (${totalRecipients}) exceeds maximum limit of 10 across TO, CC, and BCC fields`);
642
700
  }
701
+ // Request human approval before sending the email
702
+ const allRecipients = [...toRecipients, ...(ccRecipients || []), ...(bccRecipients || [])];
703
+ const approved = await requestHumanApproval(`Send information via Email to ${allRecipients.join(', ')}`);
704
+ if (!approved) {
705
+ return 'Operation cancelled: Human approval was not granted for sending this email.';
706
+ }
643
707
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('email:emails:send'), oauthClientId, oauthClientSecret, dtPlatformToken);
644
708
  const emailRequest = {
645
709
  toRecipients: { emailAddresses: toRecipients },
@@ -8,7 +8,7 @@
8
8
  * @see https://docs.dynatrace.com/docs/discover-dynatrace/references/semantic-dictionary/model/dt-entities
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.DYNATRACE_ENTITY_TYPES = void 0;
11
+ exports.DYNATRACE_ENTITY_TYPES_ALL = exports.DYNATRACE_ENTITY_TYPES_BASICS = void 0;
12
12
  exports.getEntityTypeFromId = getEntityTypeFromId;
13
13
  /**
14
14
  * Entity ID prefixes mapped to their corresponding Dynatrace entity types
@@ -19,28 +19,23 @@ exports.getEntityTypeFromId = getEntityTypeFromId;
19
19
  *
20
20
  * Recommendation: Use `verify_dql` and/or `execute_dql` to ensure that the entity type can be queried correctly.
21
21
  */
22
- const ENTITY_ID_PREFIX_TO_TYPE_MAP = {
22
+ const ENTITY_ID_PREFIX_TO_TYPE_MAP_BASICS = {
23
23
  // Core Applications and Services
24
24
  APPLICATION: 'dt.entity.application', // Verified!
25
25
  SERVICE: 'dt.entity.service', // Verified!
26
- SERVICE_INSTANCE: 'dt.entity.service_instance', // Verified!
27
26
  MOBILE_APPLICATION: 'dt.entity.mobile_application', // Verified! (0 rows found, manually verified that entity exists)
28
27
  CUSTOM_APPLICATION: 'dt.entity.custom_application', // Verified!
29
28
  // Infrastructure
30
29
  HOST: 'dt.entity.host', // Verified!
31
30
  HOST_GROUP: 'dt.entity.host_group', // Verified!
32
31
  PROCESS_GROUP: 'dt.entity.process_group', // Verified!
33
- PROCESS_GROUP_INSTANCE: 'dt.entity.process_group_instance', // Verified!
34
32
  DISK: 'dt.entity.disk', // Verified!
35
33
  NETWORK_INTERFACE: 'dt.entity.network_interface', // Verified!
36
34
  // Cloud Services
37
35
  CLOUD_APPLICATION: 'dt.entity.cloud_application', // Verified!
38
- CLOUD_APPLICATION_INSTANCE: 'dt.entity.cloud_application_instance', // Verified!
39
36
  CLOUD_APPLICATION_NAMESPACE: 'dt.entity.cloud_application_namespace', // Verified!
40
37
  // Containers and Container Groups
41
38
  CONTAINER_GROUP: 'dt.entity.container_group', // Verified!
42
- CONTAINER_GROUP_INSTANCE: 'dt.entity.container_group_instance', // Verified!
43
- DCG_INSTANCE: 'dt.entity.docker_container_group_instance', // Verified! (0 rows found, manually verified that entity exists, but this might be deprecated / old)
44
39
  // Environment
45
40
  ENVIRONMENT: 'dt.entity.environment', // Verified!
46
41
  // Operating System
@@ -55,6 +50,18 @@ const ENTITY_ID_PREFIX_TO_TYPE_MAP = {
55
50
  GEOLOCATION: 'dt.entity.geolocation', // Verified!
56
51
  // Database Services
57
52
  RELATIONAL_DATABASE_SERVICE: 'dt.entity.relational_database_service', // Verified - might need an additional integration/config to work properly though
53
+ // Kubernetes Entities
54
+ KUBERNETES_NODE: 'dt.entity.kubernetes_node', // Verified!
55
+ KUBERNETES_CLUSTER: 'dt.entity.kubernetes_cluster', // Verified!
56
+ KUBERNETES_SERVICE: 'dt.entity.kubernetes_service', // Verified!
57
+ };
58
+ const ENTITY_ID_PREFIX_TO_TYPE_MAP_ALL = {
59
+ ...ENTITY_ID_PREFIX_TO_TYPE_MAP_BASICS,
60
+ SERVICE_INSTANCE: 'dt.entity.service_instance', // Verified!
61
+ PROCESS_GROUP_INSTANCE: 'dt.entity.process_group_instance', // Verified!
62
+ CLOUD_APPLICATION_INSTANCE: 'dt.entity.cloud_application_instance', // Verified!
63
+ DCG_INSTANCE: 'dt.entity.docker_container_group_instance', // Verified! (0 rows found, manually verified that entity exists, but this might be deprecated / old)
64
+ CONTAINER_GROUP_INSTANCE: 'dt.entity.container_group_instance', // Verified!
58
65
  // AWS Services
59
66
  EC2_INSTANCE: 'dt.entity.ec2_instance', // Verified!
60
67
  AWS_LAMBDA_FUNCTION: 'dt.entity.aws_lambda_function', // Verified!
@@ -66,12 +73,9 @@ const ENTITY_ID_PREFIX_TO_TYPE_MAP = {
66
73
  // Virtual Machines
67
74
  AZURE_VM: 'dt.entity.azure_vm', // Verified
68
75
  OPENSTACK_VM: 'dt.entity.openstack_vm', // Needs manual verification - available only if OpenStack integration is configured
69
- // Kubernetes Entities
70
- KUBERNETES_NODE: 'dt.entity.kubernetes_node', // Verified!
71
- KUBERNETES_CLUSTER: 'dt.entity.kubernetes_cluster', // Verified!
72
- KUBERNETES_SERVICE: 'dt.entity.kubernetes_service', // Verified!
73
76
  };
74
- exports.DYNATRACE_ENTITY_TYPES = Object.values(ENTITY_ID_PREFIX_TO_TYPE_MAP).sort();
77
+ exports.DYNATRACE_ENTITY_TYPES_BASICS = Object.values(ENTITY_ID_PREFIX_TO_TYPE_MAP_BASICS).sort();
78
+ exports.DYNATRACE_ENTITY_TYPES_ALL = Object.values(ENTITY_ID_PREFIX_TO_TYPE_MAP_ALL).sort();
75
79
  /**
76
80
  * Maps a Dynatrace entity ID to its corresponding entity type.
77
81
  *
@@ -97,5 +101,5 @@ function getEntityTypeFromId(entityId) {
97
101
  }
98
102
  const prefix = entityId.substring(0, hyphenIndex);
99
103
  // Look up the entity type in our mapping
100
- return ENTITY_ID_PREFIX_TO_TYPE_MAP[prefix] || null;
104
+ return ENTITY_ID_PREFIX_TO_TYPE_MAP_ALL[prefix] || null;
101
105
  }
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const dynatrace_entity_types_1 = require("./dynatrace-entity-types");
4
4
  describe('DYNATRACE_ENTITY_TYPES', () => {
5
5
  it('should be sorted alphabetically', () => {
6
- const sortedTypes = [...dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES].sort();
7
- expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES).toEqual(sortedTypes);
6
+ const sortedTypes = [...dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL].sort();
7
+ expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL).toEqual(sortedTypes);
8
8
  });
9
9
  it('should have unique values', () => {
10
- const uniqueTypes = [...new Set(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES)];
11
- expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.length).toBe(uniqueTypes.length);
10
+ const uniqueTypes = [...new Set(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL)];
11
+ expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length).toBe(uniqueTypes.length);
12
12
  });
13
13
  });
14
14
  describe('getEntityTypeFromId', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Dynatrace",
6
6
  "keywords": [
@@ -1,47 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getMonitoredEntityDetails = void 0;
4
- const execute_dql_1 = require("./execute-dql");
5
- const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
6
- /**
7
- * Get monitored entity details by entity ID via DQL
8
- * @param dtClient
9
- * @param entityId
10
- * @returns Details about the monitored entity, or undefined in case we couldn't find it
11
- */
12
- const getMonitoredEntityDetails = async (dtClient, entityId) => {
13
- // Try to determine the entity type directly from the entity ID (e.g., PROCESS_GROUP-F84E4759809ADA84 -> dt.entity.process_group)
14
- const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(entityId);
15
- if (!entityType) {
16
- console.error(`Couldn't determine entity type for ID: ${entityId}. Please raise an issue at https://github.com/dynatrace-oss/dynatrace-mcp/issues if you believe this is a bug.`);
17
- return;
18
- }
19
- // construct DQL statement like `fetch dt.entity.hosts | filter id == "HOST-1234"`
20
- const dql = `fetch ${entityType} | filter id == "${entityId}" | expand tags | fieldsAdd entity.type`;
21
- // Get response from API
22
- const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
23
- // verify response and length
24
- if (!dqlResponse || !dqlResponse.records || dqlResponse.records.length === 0) {
25
- console.error(`No entity found for ID: ${entityId}`);
26
- return;
27
- }
28
- // in case we have more than one entity -> log it
29
- if (dqlResponse.records.length > 1) {
30
- console.error(`Multiple entities (${dqlResponse.records.length}) found for entity ID: ${entityId}. Returning the first one.`);
31
- }
32
- const entity = dqlResponse.records[0];
33
- // make typescript happy; entity should never be null though
34
- if (!entity) {
35
- console.error(`No entity found for ID: ${entityId}`);
36
- return;
37
- }
38
- // return entity details
39
- return {
40
- entityId: String(entity.id),
41
- entityTypeTable: entityType,
42
- displayName: String(entity['entity.name']),
43
- type: String(entity['entity.type']),
44
- allProperties: entity || undefined,
45
- };
46
- };
47
- exports.getMonitoredEntityDetails = getMonitoredEntityDetails;