@dynatrace-oss/dynatrace-mcp-server 0.6.1 → 0.8.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 +2 -0
- package/dist/authentication/dynatrace-clients.js +1 -1
- package/dist/capabilities/davis-copilot.js +22 -1
- package/dist/capabilities/find-monitored-entity-by-name.js +10 -25
- package/dist/capabilities/find-monitored-entity-by-name.test.js +26 -10
- package/dist/capabilities/get-events-for-cluster.js +15 -5
- package/dist/capabilities/update-workflow.js +2 -2
- package/dist/getDynatraceEnv.js +7 -3
- package/dist/getDynatraceEnv.test.js +32 -0
- package/dist/index.js +161 -59
- package/dist/utils/dynatrace-entity-types.js +17 -13
- package/dist/utils/dynatrace-entity-types.test.js +4 -4
- package/package.json +1 -1
- package/dist/capabilities/get-monitored-entity-details.js +0 -47
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ bringing real-time observability data directly into your development workflow.
|
|
|
25
25
|
|
|
26
26
|
If you need help, please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
|
|
27
27
|
|
|
28
|
+
https://github.com/user-attachments/assets/25c05db1-8e09-4a7f-add2-ed486ffd4b5a
|
|
29
|
+
|
|
28
30
|
## Quickstart
|
|
29
31
|
|
|
30
32
|
You can add this MCP server to your MCP Client like VSCode, Claude, Cursor, Amazon Q, Windsurf, ChatGPT, or Github Copilot via the npmjs package `@dynatrace-oss/dynatrace-mcp-server`, and type `stdio`.
|
|
@@ -34,7 +34,7 @@ const requestToken = async (clientId, clientSecret, ssoAuthUrl, scopes) => {
|
|
|
34
34
|
return await res.json();
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
|
-
* Create a Dynatrace Http Client (from the http-client SDK) based on the provided authentication
|
|
37
|
+
* Create a Dynatrace Http Client (from the http-client SDK) based on the provided authentication credentials
|
|
38
38
|
* @param environmentUrl
|
|
39
39
|
* @param scopes
|
|
40
40
|
* @param clientId
|
|
@@ -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.
|
|
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 "*<
|
|
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 = (
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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)(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
8
|
+
expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length).toBeGreaterThan(0);
|
|
9
9
|
});
|
|
10
|
-
it('should include all entity types from
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
-
const secondEntityType = 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
|
});
|
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getEventsForCluster = void 0;
|
|
4
4
|
const execute_dql_1 = require("./execute-dql");
|
|
5
|
-
const getEventsForCluster = async (dtClient, clusterId) => {
|
|
6
|
-
let dql =
|
|
7
|
-
if (!clusterId) {
|
|
8
|
-
//
|
|
9
|
-
dql
|
|
5
|
+
const getEventsForCluster = async (dtClient, clusterId, kubernetesEntityId, eventType) => {
|
|
6
|
+
let dql = 'fetch events';
|
|
7
|
+
if (!clusterId && !kubernetesEntityId) {
|
|
8
|
+
// If no clusterId or kubernetesEntityId is provided, return all kubernetes related events
|
|
9
|
+
dql += ` | filter isNotNull(k8s.cluster.uid)`;
|
|
10
10
|
}
|
|
11
|
+
else if (clusterId || kubernetesEntityId) {
|
|
12
|
+
// filter by clusterId or kubernetesEntityId if provided
|
|
13
|
+
dql += `| filter k8s.cluster.uid == "${clusterId}" or dt.entity.kubernetes_cluster == "${kubernetesEntityId}"`;
|
|
14
|
+
}
|
|
15
|
+
// filter by eventType if provided
|
|
16
|
+
if (eventType) {
|
|
17
|
+
dql += ` | filter eventType == "${eventType}"`;
|
|
18
|
+
}
|
|
19
|
+
// sort by timestamp
|
|
20
|
+
dql += ' | sort timestamp desc';
|
|
11
21
|
return (0, execute_dql_1.executeDql)(dtClient, { query: dql });
|
|
12
22
|
};
|
|
13
23
|
exports.getEventsForCluster = getEventsForCluster;
|
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.updateWorkflow = void 0;
|
|
4
4
|
const client_automation_1 = require("@dynatrace-sdk/client-automation");
|
|
5
5
|
const updateWorkflow = async (dtClient, workflowId, body) => {
|
|
6
|
-
const
|
|
7
|
-
return await
|
|
6
|
+
const workflowsClient = new client_automation_1.WorkflowsClient(dtClient);
|
|
7
|
+
return await workflowsClient.updateWorkflow({
|
|
8
8
|
id: workflowId,
|
|
9
9
|
body: body,
|
|
10
10
|
});
|
package/dist/getDynatraceEnv.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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', '
|
|
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
|
|
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
|
|
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
|
|
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', '
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 manifest 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 ({
|
|
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
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
resp +=
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
resp
|
|
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
|
-
|
|
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()
|
|
339
|
-
message: zod_1.z
|
|
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', '
|
|
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
|
-
'
|
|
373
|
-
'
|
|
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
|
|
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,
|
|
@@ -581,13 +637,50 @@ const main = async () => {
|
|
|
581
637
|
clusterId: zod_1.z
|
|
582
638
|
.string()
|
|
583
639
|
.optional()
|
|
584
|
-
.describe(`The Kubernetes
|
|
640
|
+
.describe(`The Kubernetes Cluster Id, referred to as k8s.cluster.uid, usually seen when using "kubectl" - this is NOT the Dynatrace environment and not the Dynatrace Kubernetes Entity Id. Leave empty if you don't know the Cluster Id.`),
|
|
641
|
+
kubernetesEntityId: zod_1.z
|
|
642
|
+
.string()
|
|
643
|
+
.optional()
|
|
644
|
+
.describe(`The Dynatrace Kubernetes Entity Id, referred to as dt.entity.kubernetes_cluster. Leave empty if you don't know the Entity Id, or use the "find_entity_by_name" tool to find the cluster by name.`),
|
|
645
|
+
eventType: zod_1.z
|
|
646
|
+
.enum([
|
|
647
|
+
'OMPLIANCE_FINDING',
|
|
648
|
+
'COMPLIANCE_SCAN_COMPLETED',
|
|
649
|
+
'CUSTOM_INFO',
|
|
650
|
+
'DETECTION_FINDING',
|
|
651
|
+
'ERROR_EVENT',
|
|
652
|
+
'OSI_UNEXPECTEDLY_UNAVAILABLE',
|
|
653
|
+
'PROCESS_RESTART',
|
|
654
|
+
'RESOURCE_CONTENTION_EVENT',
|
|
655
|
+
'SERVICE_CLIENT_ERROR_RATE_INCREASED',
|
|
656
|
+
'SERVICE_CLIENT_SLOWDOWN',
|
|
657
|
+
'SERVICE_ERROR_RATE_INCREASED',
|
|
658
|
+
'SERVICE_SLOWDOWN',
|
|
659
|
+
'SERVICE_UNEXPECTED_HIGH_LOAD',
|
|
660
|
+
'SERVICE_UNEXPECTED_LOW_LOAD',
|
|
661
|
+
])
|
|
662
|
+
.optional(),
|
|
663
|
+
maxEventsToDisplay: zod_1.z.number().default(10).describe('Maximum number of events to display in the response.'),
|
|
585
664
|
}, {
|
|
586
665
|
readOnlyHint: true,
|
|
587
|
-
}, async ({ clusterId }) => {
|
|
666
|
+
}, async ({ clusterId, kubernetesEntityId, eventType, maxEventsToDisplay }) => {
|
|
588
667
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
589
|
-
const
|
|
590
|
-
|
|
668
|
+
const result = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId, kubernetesEntityId, eventType);
|
|
669
|
+
if (result && result.records && result.records.length > 0) {
|
|
670
|
+
let resp = `Found ${result.records.length} events! Displaying the top ${maxEventsToDisplay} events:\n`;
|
|
671
|
+
// iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
|
|
672
|
+
result.records.slice(0, maxEventsToDisplay).forEach((event) => {
|
|
673
|
+
if (event) {
|
|
674
|
+
resp += `- Event ${event['event.id']} (${event['event.type']}) on Kubernetes Entity ID ${event['dt.entity.kubernetes_cluster']} with status ${event['event.status']}: ${event['event.name']} - started at ${event['event.start']}, ended at ${event['event.end']}, duration: ${event['duration']}\n`;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
resp +=
|
|
678
|
+
`\nNext Steps:` +
|
|
679
|
+
`\n1. Consider filtering by \`eventType\` to find specific events of interest.` +
|
|
680
|
+
`\n2. Use "execute_dql" tool with the following query to get more details about a specific event: "fetch events | filter event.id == \"<event-id>\""`;
|
|
681
|
+
return resp;
|
|
682
|
+
}
|
|
683
|
+
return 'No events found for the specified Kubernetes cluster. Try to leave clusterId and kubernetesEntityId empty to get events from all clusters.';
|
|
591
684
|
});
|
|
592
685
|
tool('get_ownership', 'Get detailed Ownership information for one or multiple entities on Dynatrace', {
|
|
593
686
|
entityIds: zod_1.z.string().optional().describe('Comma separated list of entityIds'),
|
|
@@ -631,7 +724,9 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
|
|
|
631
724
|
ccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for CC recipients'),
|
|
632
725
|
bccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for BCC recipients'),
|
|
633
726
|
subject: zod_1.z.string().describe('Subject line of the email'),
|
|
634
|
-
body: zod_1.z
|
|
727
|
+
body: zod_1.z
|
|
728
|
+
.string()
|
|
729
|
+
.describe('Body content of the email (plain text only). Avoid sending sensitive data like log lines. Focus on context, insights, links, and summaries.'),
|
|
635
730
|
}, {
|
|
636
731
|
openWorldHint: true, // email is as close to the open-world as we can get with our system
|
|
637
732
|
}, async ({ toRecipients, ccRecipients, bccRecipients, subject, body }) => {
|
|
@@ -640,6 +735,12 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
|
|
|
640
735
|
if (totalRecipients > 10) {
|
|
641
736
|
throw new Error(`Total recipients (${totalRecipients}) exceeds maximum limit of 10 across TO, CC, and BCC fields`);
|
|
642
737
|
}
|
|
738
|
+
// Request human approval before sending the email
|
|
739
|
+
const allRecipients = [...toRecipients, ...(ccRecipients || []), ...(bccRecipients || [])];
|
|
740
|
+
const approved = await requestHumanApproval(`Send information via Email to ${allRecipients.join(', ')}`);
|
|
741
|
+
if (!approved) {
|
|
742
|
+
return 'Operation cancelled: Human approval was not granted for sending this email.';
|
|
743
|
+
}
|
|
643
744
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('email:emails:send'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
644
745
|
const emailRequest = {
|
|
645
746
|
toRecipients: { emailAddresses: toRecipients },
|
|
@@ -710,7 +811,8 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
|
|
|
710
811
|
}
|
|
711
812
|
catch (error) {
|
|
712
813
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
713
|
-
|
|
814
|
+
// Respond with a JSON-RPC Parse error
|
|
815
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
|
|
714
816
|
return;
|
|
715
817
|
}
|
|
716
818
|
}
|
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
7
|
-
expect(dynatrace_entity_types_1.
|
|
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.
|
|
11
|
-
expect(dynatrace_entity_types_1.
|
|
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,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;
|