@dynatrace-oss/dynatrace-mcp-server 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -321,6 +321,7 @@ Depending on the features you are using, the following scopes are needed:
|
|
|
321
321
|
- `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
|
|
322
322
|
- `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
|
|
323
323
|
- `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
|
|
324
|
+
- `storage:smartscape:read` - needed for `execute_dql` tool to read Smartscape Data
|
|
324
325
|
- `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
|
|
325
326
|
- `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
|
|
326
327
|
- `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findMonitoredEntitiesByName = exports.generateDqlSearchEntityCommand = void 0;
|
|
3
|
+
exports.findMonitoredEntitiesByName = exports.findMonitoredEntityViaSmartscapeByName = exports.generateDqlSearchEntityCommand = void 0;
|
|
4
4
|
const execute_dql_1 = require("./execute-dql");
|
|
5
5
|
const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
|
|
6
6
|
/**
|
|
@@ -22,10 +22,35 @@ const generateDqlSearchEntityCommand = (entityNames, extendedSearch) => {
|
|
|
22
22
|
};
|
|
23
23
|
exports.generateDqlSearchEntityCommand = generateDqlSearchEntityCommand;
|
|
24
24
|
/**
|
|
25
|
-
* Find a monitored entity by name via DQL
|
|
25
|
+
* Find a monitored entity via "smartscapeNodes" by name via DQL
|
|
26
26
|
* @param dtClient
|
|
27
|
-
* @param
|
|
28
|
-
* @returns
|
|
27
|
+
* @param entityNames Array of entitiy names to search for
|
|
28
|
+
* @returns An array with the entity details like id, name and type
|
|
29
|
+
*/
|
|
30
|
+
const findMonitoredEntityViaSmartscapeByName = async (dtClient, entityNames) => {
|
|
31
|
+
const dql = `smartscapeNodes "*" | search "*${entityNames.join('*" OR "*')}*" | fields id, name, type`;
|
|
32
|
+
console.error(`Executing DQL: ${dql}`);
|
|
33
|
+
try {
|
|
34
|
+
const smartscapeResult = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
|
|
35
|
+
if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
|
|
36
|
+
// return smartscape results if we found something
|
|
37
|
+
return smartscapeResult;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// ignore errors here, as smartscapeNodes may not be ready for all environments/users
|
|
42
|
+
console.error('Error while querying smartscapeNodes:', error);
|
|
43
|
+
}
|
|
44
|
+
console.error('No results from smartscapeNodes');
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
exports.findMonitoredEntityViaSmartscapeByName = findMonitoredEntityViaSmartscapeByName;
|
|
48
|
+
/**
|
|
49
|
+
* Find a monitored entity via "dt.entity.${entityType}" by name via DQL
|
|
50
|
+
* @param dtClient
|
|
51
|
+
* @param entityNames Array of entitiy names to search for
|
|
52
|
+
* @param extendedSearch If true, search over all entity types, otherwise only basic ones
|
|
53
|
+
* @returns An array with the entity details like id, name and type
|
|
29
54
|
*/
|
|
30
55
|
const findMonitoredEntitiesByName = async (dtClient, entityNames, extendedSearch) => {
|
|
31
56
|
// construct a DQL statement for searching the entityName over all entity types
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ const getDynatraceEnv_1 = require("./getDynatraceEnv");
|
|
|
27
27
|
const telemetry_openkit_1 = require("./utils/telemetry-openkit");
|
|
28
28
|
const dynatrace_entity_types_1 = require("./utils/dynatrace-entity-types");
|
|
29
29
|
const grail_budget_tracker_1 = require("./utils/grail-budget-tracker");
|
|
30
|
+
const dynatrace_connection_utils_1 = require("./utils/dynatrace-connection-utils");
|
|
30
31
|
// Load environment variables from .env file if available, and suppress warnings/logging to stdio
|
|
31
32
|
// as it breaks MCP communication when using stdio transport
|
|
32
33
|
const dotEnvOutput = (0, dotenv_1.config)({ quiet: true });
|
|
@@ -42,6 +43,7 @@ else {
|
|
|
42
43
|
console.error(`.env file loaded successfully - loaded ${dotEnvOutput.parsed ? Object.keys(dotEnvOutput.parsed).length : 0} environment variables: ${Object.keys(dotEnvOutput.parsed || {}).join(', ')}`);
|
|
43
44
|
}
|
|
44
45
|
const DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID = 'dt0s08.dt-app-local'; // ToDo: Register our own oauth client
|
|
46
|
+
// Base Scopes for MCP Server tools
|
|
45
47
|
let scopesBase = [
|
|
46
48
|
'app-engine:apps:run', // needed for environmentInformationClient
|
|
47
49
|
'app-engine:functions:run', // needed for environmentInformationClient
|
|
@@ -53,7 +55,8 @@ const allRequiredScopes = scopesBase.concat([
|
|
|
53
55
|
'storage:events:read', // Read events from Grail
|
|
54
56
|
'storage:buckets:read', // Read all system data stored on Grail
|
|
55
57
|
'storage:security.events:read', // Read Security events from Grail
|
|
56
|
-
'storage:entities:read', // Read Entities
|
|
58
|
+
'storage:entities:read', // Read classic Entities
|
|
59
|
+
'storage:smartscape:read', // Read Smartscape Entities from Grail
|
|
57
60
|
'storage:logs:read', // Read logs for reliability guardian validations
|
|
58
61
|
'storage:metrics:read', // Read metrics for reliability guardian validations
|
|
59
62
|
'storage:bizevents:read', // Read bizevents for reliability guardian validations
|
|
@@ -74,57 +77,6 @@ const allRequiredScopes = scopesBase.concat([
|
|
|
74
77
|
// Communication scopes
|
|
75
78
|
'email:emails:send', // Send emails
|
|
76
79
|
]);
|
|
77
|
-
/**
|
|
78
|
-
* Performs a connection test to the Dynatrace environment.
|
|
79
|
-
* Throws an error if the connection or authentication fails.
|
|
80
|
-
*/
|
|
81
|
-
async function testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken) {
|
|
82
|
-
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, oauthClientId && !oauthClientSecret ? allRequiredScopes : scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
83
|
-
const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
|
|
84
|
-
// This call will fail if authentication is incorrect.
|
|
85
|
-
await environmentInformationClient.getEnvironmentInformation();
|
|
86
|
-
}
|
|
87
|
-
function handleClientRequestError(error) {
|
|
88
|
-
let additionalErrorInformation = '';
|
|
89
|
-
if (error.response.status === 403) {
|
|
90
|
-
additionalErrorInformation =
|
|
91
|
-
'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
|
|
92
|
-
}
|
|
93
|
-
return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Try to connect to Dynatrace environment with retries and exponential backoff.
|
|
97
|
-
*/
|
|
98
|
-
async function retryTestDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken) {
|
|
99
|
-
let retryCount = 0;
|
|
100
|
-
const maxRetries = 3; // Max retries
|
|
101
|
-
const delayMs = 2000; // Initial delay of 2 seconds
|
|
102
|
-
while (true) {
|
|
103
|
-
try {
|
|
104
|
-
console.error(`Testing connection to Dynatrace environment: ${dtEnvironment}... (Attempt ${retryCount + 1} of ${maxRetries})`);
|
|
105
|
-
await testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
106
|
-
console.error(`Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
console.error(`Error: Could not connect to the Dynatrace environment at ${dtEnvironment}.`);
|
|
111
|
-
if ((0, shared_errors_1.isClientRequestError)(error)) {
|
|
112
|
-
console.error(handleClientRequestError(error));
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
console.error(`Error: ${error.message}`);
|
|
116
|
-
}
|
|
117
|
-
retryCount++;
|
|
118
|
-
if (retryCount >= maxRetries) {
|
|
119
|
-
console.error(`Fatal: Maximum number of connection retries (${maxRetries}) exceeded. Exiting.`);
|
|
120
|
-
throw new Error(`Failed to connect to Dynatrace environment ${dtEnvironment} after ${maxRetries} attempts. Most likely your configuration is incorrect. Last error: ${error.message}`, { cause: error });
|
|
121
|
-
}
|
|
122
|
-
const delay = Math.pow(2, retryCount) * delayMs; // Exponential backoff
|
|
123
|
-
console.error(`Retrying in ${delay / 1000} seconds...`);
|
|
124
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
80
|
const main = async () => {
|
|
129
81
|
console.error(`Initializing Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
130
82
|
// read Environment variables
|
|
@@ -139,25 +91,10 @@ const main = async () => {
|
|
|
139
91
|
// Unpack environment variables
|
|
140
92
|
let { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId, grailBudgetGB } = dynatraceEnv;
|
|
141
93
|
// Infer OAuth auth code flow if no OAuth Client credentials are provided
|
|
142
|
-
// -> configure default OAuth client ID for auth code flow
|
|
143
94
|
if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
|
|
144
95
|
console.error('No OAuth credentials or platform token provided - switching to OAuth authorization code flow.');
|
|
145
96
|
oauthClientId = DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID; // Default OAuth client ID for auth code flow
|
|
146
97
|
}
|
|
147
|
-
// Test connection on startup
|
|
148
|
-
try {
|
|
149
|
-
// Depending on the authentication type, there are multiple pitfalls
|
|
150
|
-
// * For Platform Tokens, we can just try to access "get environment info" and we will know whether it works
|
|
151
|
-
// * For Oauth Client Credentials flow, we can also try to request an access token upfront with limited scopes, and verify whether everything works
|
|
152
|
-
// * for Oauth Auth Code flow, we can only verify whether the client ID is valid and the OAuth verifier call works, but we can't verify whether the user will be able to authenticate successfully
|
|
153
|
-
await retryTestDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
console.error(err.message);
|
|
157
|
-
process.exit(2);
|
|
158
|
-
}
|
|
159
|
-
// Ready to start the server
|
|
160
|
-
console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
161
98
|
// Initialize usage tracking
|
|
162
99
|
const telemetry = (0, telemetry_openkit_1.createTelemetry)();
|
|
163
100
|
await telemetry.trackMcpServerStart();
|
|
@@ -189,6 +126,51 @@ const main = async () => {
|
|
|
189
126
|
return await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, oauthClientId && !oauthClientSecret ? allRequiredScopes : scopes, // Always use all scopes for maximum reusability
|
|
190
127
|
oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
191
128
|
};
|
|
129
|
+
// Try to establish a Dynatrace connection upfront, to see if everything is configured properly
|
|
130
|
+
console.error(`Testing connection to Dynatrace environment: ${dtEnvironment}...`);
|
|
131
|
+
// First, we will try a simple "fetch" to connect to dtEnvironment, without authentication
|
|
132
|
+
// This should help to see if DNS lookup works, TCP connection can be established, and TLS handshake works
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch(`${dtEnvironment}`).then((response) => response.text());
|
|
135
|
+
// check response
|
|
136
|
+
if (response && response.length > 0) {
|
|
137
|
+
if (response.includes('Authentication required')) {
|
|
138
|
+
// all good - we reached the environment and authentication is required, which is going to be the next step
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.error(`⚠️ Tried to contact ${dtEnvironment}, got the following response: ${response}`);
|
|
142
|
+
// Note: We won't error out yet, but this information could already be helpful for troubleshooting
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw new Error('No response received');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, error.message);
|
|
151
|
+
console.error(error);
|
|
152
|
+
process.exit(3);
|
|
153
|
+
}
|
|
154
|
+
// Second, we will try with proper authentication
|
|
155
|
+
try {
|
|
156
|
+
const dtClient = await createAuthenticatedHttpClient(scopesBase);
|
|
157
|
+
const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
|
|
158
|
+
await environmentInformationClient.getEnvironmentInformation();
|
|
159
|
+
console.error(`✅ Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if ((0, shared_errors_1.isClientRequestError)(error)) {
|
|
163
|
+
console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, (0, dynatrace_connection_utils_1.handleClientRequestError)(error));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, error.message);
|
|
167
|
+
// Logging more exhaustive error details for troubleshooting
|
|
168
|
+
console.error(error);
|
|
169
|
+
}
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
// Ready to start the server
|
|
173
|
+
console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
192
174
|
// quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
|
|
193
175
|
const tool = (name, description, paramsSchema, annotations, cb) => {
|
|
194
176
|
const wrappedCb = async (args) => {
|
|
@@ -210,11 +192,11 @@ const main = async () => {
|
|
|
210
192
|
// check if it's an error originating from the Dynatrace SDK / API Gateway and provide an appropriate message to the user
|
|
211
193
|
if ((0, shared_errors_1.isClientRequestError)(error)) {
|
|
212
194
|
return {
|
|
213
|
-
content: [{ type: 'text', text: handleClientRequestError(error) }],
|
|
195
|
+
content: [{ type: 'text', text: (0, dynatrace_connection_utils_1.handleClientRequestError)(error) }],
|
|
214
196
|
isError: true,
|
|
215
197
|
};
|
|
216
198
|
}
|
|
217
|
-
// else: We don't know what kind of error happened - best
|
|
199
|
+
// else: We don't know what kind of error happened - best case we can log the error and provide error.message as a tool response
|
|
218
200
|
console.log(error);
|
|
219
201
|
return {
|
|
220
202
|
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
@@ -381,22 +363,42 @@ const main = async () => {
|
|
|
381
363
|
}, {
|
|
382
364
|
readOnlyHint: true,
|
|
383
365
|
}, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
|
|
384
|
-
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read'));
|
|
366
|
+
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read', 'storage:smartscape:read'));
|
|
367
|
+
const smartscapeResult = await (0, find_monitored_entity_by_name_1.findMonitoredEntityViaSmartscapeByName)(dtClient, entityNames);
|
|
368
|
+
if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
|
|
369
|
+
// Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
|
|
370
|
+
const validSmartscapeEntities = smartscapeResult.records.filter((entity) => !!(entity && entity.id && entity.type && entity.name));
|
|
371
|
+
let resp = `Found ${validSmartscapeEntities.length} monitored entities via Smartscape! Displaying the first ${Math.min(maxEntitiesToDisplay, validSmartscapeEntities.length)} valid entities:\n`;
|
|
372
|
+
validSmartscapeEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
|
|
373
|
+
resp += `- Entity '${entity.name}' of entity-type '${entity.type}' has entity id '${entity.id}' and tags ${entity['tags'] ? JSON.stringify(entity['tags']) : 'none'} - DQL Filter: '| filter dt.smartscape.${String(entity.type).toLowerCase()} == "${entity.id}"'\n`;
|
|
374
|
+
});
|
|
375
|
+
resp +=
|
|
376
|
+
'\n\n**Next Steps:**\n' +
|
|
377
|
+
'1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statement: "smartscapeNodes \"<entity-type>\" | filter id == <entity-id>"\n' +
|
|
378
|
+
'2. Perform a sanity check that found entities are actually the ones you are looking for, by comparing name and by type (hosts vs. containers vs. apps vs. functions) and technology (Java, TypeScript, .NET) with what is available in the local source code repo.\n' +
|
|
379
|
+
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.smartscape.<entity-type> == <entity-id> | limit 20"\n' +
|
|
380
|
+
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n' +
|
|
381
|
+
'5. Explore dependency & relationships with: "smartscapeEdges \"*\" | filter source_id == <entity-id> or target_id == <entity-id>" to list inbound/outbound edges (depends_on, dependency_of, owned_by, part_of) for graph context\n';
|
|
382
|
+
return resp;
|
|
383
|
+
}
|
|
384
|
+
// If no result from Smartscape, try the classic entities API
|
|
385
385
|
const result = await (0, find_monitored_entity_by_name_1.findMonitoredEntitiesByName)(dtClient, entityNames, extendedSearch);
|
|
386
386
|
if (result && result.records && result.records.length > 0) {
|
|
387
|
-
|
|
387
|
+
// Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
|
|
388
|
+
const validClassicEntities = result.records.filter((entity) => !!(entity && entity.id && entity.type && entity.name));
|
|
389
|
+
let resp = `Found ${validClassicEntities.length} monitored entities! Displaying the first ${Math.min(maxEntitiesToDisplay, validClassicEntities.length)} entities:\n`;
|
|
388
390
|
// iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
|
|
389
|
-
|
|
391
|
+
validClassicEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
|
|
390
392
|
if (entity && entity.id) {
|
|
391
393
|
const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(String(entity.id));
|
|
392
|
-
resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']}' has entity id '${entity.id}' and tags ${entity['tags'] ? entity['tags'] : 'none'} -
|
|
394
|
+
resp += `- Entity '${entity['entity.name']}' of entity-type '${entity['entity.type']}' has entity id '${entity.id}' and tags ${entity['tags'] ? entity['tags'] : 'none'} - DQL Filter: '| filter ${entityType} == "${entity.id}"'\n`;
|
|
393
395
|
}
|
|
394
396
|
});
|
|
395
397
|
resp +=
|
|
396
398
|
'\n\n**Next Steps:**\n' +
|
|
397
|
-
'1.
|
|
399
|
+
'1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statements: "describe(dt.entity.<entity-type>)", and "fetch dt.entity.<entity-type> | filter id == <entity-id> | fieldsAdd <field-1>, <field-2>, ..."\n' +
|
|
398
400
|
'2. Perform a sanity check that found entities are actually the ones you are looking for, by comparing name and by type (hosts vs. containers vs. apps vs. functions) and technology (Java, TypeScript, .NET) with what is available in the local source code repo.\n' +
|
|
399
|
-
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.entity.<entity-type> == <entity-id>"\n' +
|
|
401
|
+
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.entity.<entity-type> == <entity-id> | limit 20"\n' +
|
|
400
402
|
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n';
|
|
401
403
|
return resp;
|
|
402
404
|
}
|
|
@@ -445,20 +447,26 @@ const main = async () => {
|
|
|
445
447
|
}
|
|
446
448
|
return resp;
|
|
447
449
|
});
|
|
448
|
-
tool('execute_dql', 'Get Logs, Metrics, Spans or
|
|
450
|
+
tool('execute_dql', 'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
|
|
449
451
|
'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
|
|
450
452
|
'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
|
|
451
453
|
dqlStatement: zod_1.z
|
|
452
454
|
.string()
|
|
453
|
-
.describe('DQL Statement (Ex: "fetch [logs, spans, events], from: now()-4h, to: now() | filter <some-filter> | summarize count(), by:{some-fields}
|
|
455
|
+
.describe('DQL Statement (Ex: "fetch [logs, spans, events, metric.series, ...], from: now()-4h, to: now() [| filter <some-filter>] [| summarize count(), by:{some-fields}]", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }", or for entities via smartscape: "smartscapeNodes \"[*, HOST, PROCESS, ...]\" [| filter id == "<ENTITY-ID>"]"). ' +
|
|
454
456
|
'When querying data for a specific entity, call the `find_entity_by_name` tool first to get an appropriate filter like `dt.entity.service == "SERVICE-1234"` or `dt.entity.host == "HOST-1234"` to be used in the DQL statement. '),
|
|
457
|
+
recordLimit: zod_1.z.number().optional().default(100).describe('Maximum number of records to return (default: 100)'),
|
|
458
|
+
recordSizeLimitMB: zod_1.z
|
|
459
|
+
.number()
|
|
460
|
+
.optional()
|
|
461
|
+
.default(1)
|
|
462
|
+
.describe('Maximum size of the returned records in MB (default: 1MB)'),
|
|
455
463
|
}, {
|
|
456
464
|
// not readonly (DQL statements may modify things), not idempotent (may change over time)
|
|
457
465
|
readOnlyHint: false,
|
|
458
466
|
idempotentHint: false,
|
|
459
467
|
// while we are not strictly talking to the open world here, the response from execute DQL could interpreted as a web-search, which often is referred to open-world
|
|
460
468
|
openWorldHint: true,
|
|
461
|
-
}, async ({ dqlStatement }) => {
|
|
469
|
+
}, async ({ dqlStatement, recordLimit = 100, recordSizeLimitMB = 1 }) => {
|
|
462
470
|
// Create a HTTP Client that has all storage:*:read scopes
|
|
463
471
|
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
|
|
464
472
|
'storage:logs:read', // Read logs for reliability guardian validations
|
|
@@ -470,8 +478,9 @@ const main = async () => {
|
|
|
470
478
|
'storage:system:read', // Read System Data from Grail
|
|
471
479
|
'storage:user.events:read', // Read User events from Grail
|
|
472
480
|
'storage:user.sessions:read', // Read User sessions from Grail
|
|
473
|
-
'storage:security.events:read'
|
|
474
|
-
|
|
481
|
+
'storage:security.events:read', // Read Security events from Grail
|
|
482
|
+
'storage:smartscape:read'));
|
|
483
|
+
const response = await (0, execute_dql_1.executeDql)(dtClient, { query: dqlStatement, maxResultRecords: recordLimit, maxResultBytes: recordSizeLimitMB * 1024 * 1024 }, grailBudgetGB);
|
|
475
484
|
if (!response) {
|
|
476
485
|
return 'DQL execution failed or returned no result.';
|
|
477
486
|
}
|
|
@@ -509,6 +518,9 @@ const main = async () => {
|
|
|
509
518
|
if (response.sampled !== undefined && response.sampled) {
|
|
510
519
|
result += `- **⚠️ Sampling Used:** Yes (results may be approximate)\n`;
|
|
511
520
|
}
|
|
521
|
+
if (response.records.length === recordLimit) {
|
|
522
|
+
result += `- **⚠️ Record Limit Reached:** The result set was limited to ${recordLimit} records. Consider changing your query with a smaller timeframe, an aggregation or a more concise filter. Alternatively, increase the recordLimit if you expect more results.\n`;
|
|
523
|
+
}
|
|
512
524
|
result += `\n📋 **Query Results**: (${response.records?.length || 0} records):\n\n`;
|
|
513
525
|
result += `\`\`\`json\n${JSON.stringify(response.records, null, 2)}\n\`\`\``;
|
|
514
526
|
return result;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleClientRequestError = handleClientRequestError;
|
|
4
|
+
function handleClientRequestError(error) {
|
|
5
|
+
let additionalErrorInformation = '';
|
|
6
|
+
if (error.response.status === 403) {
|
|
7
|
+
additionalErrorInformation =
|
|
8
|
+
'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
|
|
9
|
+
}
|
|
10
|
+
return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
|
|
11
|
+
}
|
|
@@ -215,8 +215,8 @@ function createTelemetry() {
|
|
|
215
215
|
return new DynatraceMcpTelemetry();
|
|
216
216
|
}
|
|
217
217
|
catch (e) {
|
|
218
|
-
// Failed to initialize
|
|
219
|
-
console.error(e);
|
|
218
|
+
// Failed to initialize (unexpected). Log concise message without stack trace spam.
|
|
219
|
+
console.error('Dynatrace Telemetry initialization failed:', e.message);
|
|
220
220
|
// fallback to NoOp Telemetry
|
|
221
221
|
return new NoOpTelemetry();
|
|
222
222
|
}
|