@dynatrace-oss/dynatrace-mcp-server 0.6.0 → 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.
package/README.md CHANGED
@@ -277,6 +277,43 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
277
277
 
278
278
  This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
279
279
 
280
+ **Google Gemini CLI**
281
+
282
+ The [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) is Google's official command-line AI assistant that supports MCP server integration. You can add the Dynatrace MCP server using either the built-in management commands or manual configuration.
283
+
284
+ Using `gemini` CLI directly (recommended):
285
+
286
+ ```bash
287
+ gemini extensions install https://github.com/dynatrace-oss/dynatrace-mcp
288
+ export DT_PLATFORM_TOKEN=...
289
+ export DT_ENVIRONMENT=https://...
290
+ ```
291
+
292
+ and verify that the server is running via
293
+
294
+ ```bash
295
+ gemini mcp list
296
+ ```
297
+
298
+ Or manually in your `~/.gemini/settings.json` or `.gemini/settings.json`:
299
+
300
+ ```json
301
+ {
302
+ "mcpServers": {
303
+ "dynatrace": {
304
+ "command": "npx",
305
+ "args": ["@dynatrace-oss/dynatrace-mcp-server@latest"],
306
+ "env": {
307
+ "DT_PLATFORM_TOKEN": "",
308
+ "DT_ENVIRONMENT": ""
309
+ },
310
+ "timeout": 30000,
311
+ "trust": false
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
280
317
  ### HTTP Server Mode (Alternative)
281
318
 
282
319
  For scenarios where you need to run the MCP server as an HTTP service instead of using stdio (e.g., for stateful sessions, load balancing, or integration with web clients), you can use the HTTP server mode:
@@ -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,8 +25,22 @@ 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
- (0, dotenv_1.config)();
30
+ // Load environment variables from .env file if available, and suppress warnings/logging to stdio
31
+ // as it breaks MCP communication when using stdio transport
32
+ const dotEnvOutput = (0, dotenv_1.config)({ quiet: true });
33
+ if (dotEnvOutput.error) {
34
+ // Only log error if it's not about missing .env file
35
+ if (dotEnvOutput.error.code !== 'ENOENT') {
36
+ console.error('Error loading .env file:', dotEnvOutput.error);
37
+ process.exit(1);
38
+ }
39
+ }
40
+ else {
41
+ // Successfully loaded .env file
42
+ console.error(`.env file loaded successfully - loaded ${dotEnvOutput.parsed ? Object.keys(dotEnvOutput.parsed).length : 0} environment variables: ${Object.keys(dotEnvOutput.parsed || {}).join(', ')}`);
43
+ }
31
44
  let scopesBase = [
32
45
  'app-engine:apps:run', // needed for environmentInformationClient
33
46
  'app-engine:functions:run', // needed for environmentInformationClient
@@ -126,6 +139,7 @@ const main = async () => {
126
139
  }, {
127
140
  capabilities: {
128
141
  tools: {},
142
+ elicitation: {},
129
143
  },
130
144
  });
131
145
  // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
@@ -170,6 +184,38 @@ const main = async () => {
170
184
  };
171
185
  server.tool(name, description, paramsSchema, annotations, (args) => wrappedCb(args));
172
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
+ };
173
219
  /** Tool Definitions below */
174
220
  tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, {
175
221
  readOnlyHint: true,
@@ -183,7 +229,7 @@ const main = async () => {
183
229
  resp += `You can reach it via ${dtEnvironment}\n`;
184
230
  return resp;
185
231
  });
186
- 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).', {
187
233
  riskScore: zod_1.z
188
234
  .number()
189
235
  .optional()
@@ -192,7 +238,8 @@ const main = async () => {
192
238
  additionalFilter: zod_1.z
193
239
  .string()
194
240
  .optional()
195
- .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"\''),
196
243
  maxVulnerabilitiesToDisplay: zod_1.z
197
244
  .number()
198
245
  .default(25)
@@ -234,11 +281,11 @@ const main = async () => {
234
281
  `\n3. Last but not least, tell the user to visit ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities/vulnerabilities/<vulnerability-id> for full details.`;
235
282
  return resp;
236
283
  });
237
- 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.', {
238
285
  additionalFilter: zod_1.z
239
286
  .string()
240
287
  .optional()
241
- .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")\''),
242
289
  maxProblemsToDisplay: zod_1.z.number().default(10).describe('Maximum number of problems to display in the response.'),
243
290
  }, {
244
291
  readOnlyHint: true,
@@ -274,65 +321,61 @@ const main = async () => {
274
321
  return 'No problems found';
275
322
  }
276
323
  });
277
- 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', {
278
- entityName: zod_1.z.string().describe('Name of the entity to search for, e.g., "my-service" or "my-host"'),
279
- }, {
280
- readOnlyHint: true,
281
- }, async ({ entityName }) => {
282
- const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
283
- const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
284
- return entityResponse;
285
- });
286
- tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
287
- 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.'),
288
334
  }, {
289
335
  readOnlyHint: true,
290
- }, async ({ entityId }) => {
336
+ }, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
291
337
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
292
- const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
293
- if (!entityDetails) {
294
- return `No entity found with entityId: ${entityId}`;
295
- }
296
- let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
297
- `Properties: ${JSON.stringify(entityDetails.allProperties)}\n`;
298
- if (entityDetails.type == 'SERVICE') {
299
- resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`;
300
- }
301
- else if (entityDetails.type == 'HOST') {
302
- resp += `You can find more information about the host at ${dtEnvironment}/ui/apps/dynatrace.infraops/hosts/${entityDetails.entityId}`;
303
- }
304
- else if (entityDetails.type == 'KUBERNETES_CLUSTER') {
305
- resp += `You can find more information about the cluster at ${dtEnvironment}/ui/apps/dynatrace.infraops/kubernetes/${entityDetails.entityId}`;
306
- }
307
- else if (entityDetails.type == 'CLOUD_APPLICATION') {
308
- resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}`;
309
- }
310
- resp += `\n\n**Filter**:`;
311
- // Use entityTypeTable as the filter (e.g., fetch logs | filter dt.entity.service == "SERVICE-1234")
312
- if (entityDetails.entityTypeTable) {
313
- 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;
314
355
  }
315
356
  else {
316
- 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.';
317
358
  }
318
- resp += `\n\n**Next Steps**\n\n`;
319
- 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`;
320
- resp += `2. Find out whether any problems exist for this entity using the list_problems tool\n`;
321
- resp += `3. Explore logs for this entity by using execute_dql with "fetch logs" and applying the filter mentioned above'\n`;
322
- return resp;
323
359
  });
324
360
  tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
325
- channel: zod_1.z.string().optional(),
326
- 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.'),
327
365
  }, {
328
366
  // not read-only, not open-world, not destructive
329
367
  readOnlyHint: false,
330
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
+ }
331
374
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
332
375
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
333
376
  return `Message sent to Slack channel: ${JSON.stringify(response)}`;
334
377
  });
335
- 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.', {
336
379
  dqlStatement: zod_1.z.string(),
337
380
  }, {
338
381
  readOnlyHint: true,
@@ -351,16 +394,17 @@ const main = async () => {
351
394
  resp += `The DQL statement is valid - you can use the "execute_dql" tool.\n`;
352
395
  }
353
396
  else {
354
- 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`;
355
398
  }
356
399
  return resp;
357
400
  });
358
401
  tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
359
- 'You can also use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
360
- '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\""', {
361
404
  dqlStatement: zod_1.z
362
405
  .string()
363
- .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. '),
364
408
  }, {
365
409
  // not readonly (DQL statements may modify things), not idempotent (may change over time)
366
410
  readOnlyHint: false,
@@ -431,6 +475,11 @@ const main = async () => {
431
475
  idempotentHint: true,
432
476
  }, async ({ text }) => {
433
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
+ }
434
483
  const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
435
484
  let resp = `🔤 Natural Language to DQL:\n\n`;
436
485
  resp += `**Query:** "${text}"\n\n`;
@@ -460,6 +509,11 @@ const main = async () => {
460
509
  idempotentHint: true,
461
510
  }, async ({ dql }) => {
462
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
+ }
463
517
  const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
464
518
  let resp = `📝 DQL to Natural Language:\n\n`;
465
519
  resp += `**DQL Query:**\n\`\`\`\n${dql}\n\`\`\`\n\n`;
@@ -475,7 +529,7 @@ const main = async () => {
475
529
  }
476
530
  return resp;
477
531
  });
478
- 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.', {
479
533
  text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
480
534
  context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
481
535
  instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
@@ -485,6 +539,11 @@ const main = async () => {
485
539
  openWorldHint: true, // web-search like characteristics
486
540
  }, async ({ text, context, instruction }) => {
487
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
+ }
488
547
  const conversationContext = [];
489
548
  if (context) {
490
549
  conversationContext.push({
@@ -537,6 +596,11 @@ const main = async () => {
537
596
  readOnlyHint: false,
538
597
  idempotentHint: false, // creating the same workflow multiple times is possible
539
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
+ }
540
604
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
541
605
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
542
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`;
@@ -558,6 +622,11 @@ const main = async () => {
558
622
  readOnlyHint: false,
559
623
  idempotentHint: true, // making the same workflow public multiple times yields the same result
560
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
+ }
561
630
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
562
631
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
563
632
  isPrivate: false,
@@ -618,7 +687,9 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
618
687
  ccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for CC recipients'),
619
688
  bccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for BCC recipients'),
620
689
  subject: zod_1.z.string().describe('Subject line of the email'),
621
- 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.'),
622
693
  }, {
623
694
  openWorldHint: true, // email is as close to the open-world as we can get with our system
624
695
  }, async ({ toRecipients, ccRecipients, bccRecipients, subject, body }) => {
@@ -627,6 +698,12 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
627
698
  if (totalRecipients > 10) {
628
699
  throw new Error(`Total recipients (${totalRecipients}) exceeds maximum limit of 10 across TO, CC, and BCC fields`);
629
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
+ }
630
707
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('email:emails:send'), oauthClientId, oauthClientSecret, dtPlatformToken);
631
708
  const emailRequest = {
632
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.0",
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;