@dynatrace-oss/dynatrace-mcp-server 0.1.4 → 0.2.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
@@ -102,7 +102,7 @@ A **Dynatrace OAuth Client** is needed to communicate with your Dynatrace Enviro
102
102
  [creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients),
103
103
  and set up the following environment variables in order for this MCP to work:
104
104
 
105
- * `DT_ENVIRONMENT` (string, e.g., https://abcd1234.apps.dynatrace.com) - URL to your Dynatrace Platform
105
+ * `DT_ENVIRONMENT` (string, e.g., https://abc12345.apps.dynatrace.com) - URL to your Dynatrace Platform (do not use Dynatrace classic URLs like `abc12345.live.dynatrace.com`)
106
106
  * `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
107
107
  * `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
108
108
  * OAuth Client Scopes:
@@ -114,7 +114,6 @@ and set up the following environment variables in order for this MCP to work:
114
114
  * `environment-api:problems:read` - get problems
115
115
  * `environment-api:metrics:read` - read metrics
116
116
  * `environment-api:slo:read` - read SLOs
117
- * `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
118
117
  * `storage:buckets:read` - Read all system data stored on Grail
119
118
  * `storage:logs:read` - Read logs for reliability guardian validations
120
119
  * `storage:metrics:read` - Read metrics for reliability guardian validations
@@ -125,6 +124,9 @@ and set up the following environment variables in order for this MCP to work:
125
124
  * `storage:system:read` - Read System Data from Grail
126
125
  * `storage:user.events:read` - Read User events from Grail
127
126
  * `storage:user.sessions:read` - Read User sessions from Grail
127
+ * `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
128
+
129
+ **Note**: Please ensure that `settings:objects:read` is used, and *not* the similarly named scope `app-settings:objects:read`.
128
130
 
129
131
  In addition, depending on the features you use, the following variables can be configured:
130
132
 
@@ -172,9 +174,53 @@ Can you fetch recent events from our "production-cluster"
172
174
  to help identify what might be causing these deployment issues?
173
175
  ```
174
176
 
177
+ ## Troubleshooting
178
+
179
+ ### Authentication Issues
180
+
181
+ In most cases, something is wrong with the OAuth Client. Please ensure that you have added all scopes as requested above.
182
+ In addition, please ensure that your user also has all necessary permissions on your Dynatrace Environment.
183
+
184
+ In case of any problems, you can troubleshoot SSO/OAuth issues based on our [Dynatrace Developer Documentation](https://developer.dynatrace.com/develop/access-platform-apis-from-outside/#get-bearer-token-and-call-app-function) and providing the list of scopes.
185
+
186
+ It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
187
+
188
+ 1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes):
189
+ ```bash
190
+ curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
191
+ --header 'Content-Type: application/x-www-form-urlencoded' \
192
+ --data-urlencode 'grant_type=client_credentials' \
193
+ --data-urlencode 'client_id={your-client-id}' \
194
+ --data-urlencode 'client_secret={your-client-secret}' \
195
+ --data-urlencode 'scope=app-engine:apps:run app-engine:functions:run'
196
+ ```
197
+
198
+ 2. Use `access_token` from the response of the above call as the bearer-token in the next call:
199
+ ```bash
200
+ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environment \
201
+ -H 'accept: application/json' \
202
+ -H 'Authorization: Bearer {your-bearer-token}'
203
+ ```
204
+
205
+ 3. You should retrieve a result like this:
206
+ ```json
207
+ {
208
+ "environmentId": "abc12345",
209
+ "createTime": "2023-01-01T00:10:57.123Z",
210
+ "blockTime": "2025-12-07T00:00:00Z",
211
+ "state": "ACTIVE"
212
+ }
213
+ ```
214
+
215
+
216
+ ### Problem accessing data on Grail
217
+
218
+ Grail has a dedicated section about permissions in the Dynatrace Docs. Please refer to https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model/assign-permissions-in-grail for more details.
219
+
220
+
175
221
  ## Development
176
222
 
177
- For development purposes, you can use VSCode and GitHub Copilot.
223
+ For local development purposes, you can use VSCode and GitHub Copilot.
178
224
 
179
225
  First, enable Copilot for your Workspace `.vscode/settings.json`:
180
226
  ```json
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findMonitoredEntityByName = void 0;
4
+ const execute_dql_1 = require("./execute-dql");
5
+ const findMonitoredEntityByName = async (dtClient, entityName) => {
6
+ const dql = `fetch dt.entity.application | search "*${entityName}*" | fieldsAdd entity.type
7
+ | append [fetch dt.entity.service | search "*${entityName}*" | fieldsAdd entity.type]
8
+ | append [fetch dt.entity.host | search "*${entityName}*" | fieldsAdd entity.type]
9
+ | append [fetch dt.entity.process_group | search "*${entityName}*" | fieldsAdd entity.type]
10
+ | append [fetch dt.entity.cloud_application | search "*${entityName}*" | fieldsAdd entity.type]`;
11
+ const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, dql);
12
+ if (dqlResponse && dqlResponse.length > 0) {
13
+ let resp = 'The following monitored entities were found:\n';
14
+ // iterate over dqlResponse and create a string with the entity names
15
+ dqlResponse.forEach((entity) => {
16
+ if (entity) {
17
+ resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']} has entity id '${entity.id}'\n`;
18
+ }
19
+ });
20
+ return resp;
21
+ }
22
+ else {
23
+ return 'No monitored entity found with the specified name.';
24
+ }
25
+ };
26
+ exports.findMonitoredEntityByName = findMonitoredEntityByName;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDynatraceEnv = getDynatraceEnv;
4
+ /**
5
+ * Reads and validates required environment variables for Dynatrace MCP.
6
+ * Throws an Error if validation fails.
7
+ */
8
+ function getDynatraceEnv(env = process.env) {
9
+ const oauthClient = env.OAUTH_CLIENT_ID;
10
+ const oauthClientSecret = env.OAUTH_CLIENT_SECRET;
11
+ const dtEnvironment = env.DT_ENVIRONMENT;
12
+ const slackConnectionId = env.SLACK_CONNECTION_ID || "fake-slack-connection-id";
13
+ if (!oauthClient || !oauthClientSecret || !dtEnvironment) {
14
+ throw new Error("Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables");
15
+ }
16
+ if (!dtEnvironment.startsWith("https://")) {
17
+ throw new Error("Please set DT_ENVIRONMENT to a valid Dynatrace Environment URL (e.g., https://<environment-id>.apps.dynatrace.com)");
18
+ }
19
+ if (!dtEnvironment.includes("apps.dynatrace.com") && !dtEnvironment.includes("apps.dynatracelabs.com")) {
20
+ throw new Error("Please set DT_ENVIRONMENT to a valid Dynatrace Platform Environment URL (e.g., https://<environment-id>.apps.dynatrace.com)");
21
+ }
22
+ return { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId };
23
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const getDynatraceEnv_1 = require("./getDynatraceEnv");
4
+ describe("getDynatraceEnv", () => {
5
+ const baseEnv = {
6
+ OAUTH_CLIENT_ID: "dt0s02.SAMPLE",
7
+ OAUTH_CLIENT_SECRET: "dt0s02.SAMPLE.abcd1234",
8
+ DT_ENVIRONMENT: "https://abc123.apps.dynatrace.com",
9
+ SLACK_CONNECTION_ID: "slack-conn-id"
10
+ };
11
+ it("returns all required values when environment is valid", () => {
12
+ const env = { ...baseEnv };
13
+ const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
14
+ expect(result).toEqual({
15
+ oauthClient: env.OAUTH_CLIENT_ID,
16
+ oauthClientSecret: env.OAUTH_CLIENT_SECRET,
17
+ dtEnvironment: env.DT_ENVIRONMENT,
18
+ slackConnectionId: env.SLACK_CONNECTION_ID
19
+ });
20
+ });
21
+ it("uses default slackConnectionId if not set", () => {
22
+ const env = { ...baseEnv, SLACK_CONNECTION_ID: undefined };
23
+ const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
24
+ expect(result.slackConnectionId).toBe("fake-slack-connection-id");
25
+ });
26
+ it("throws if OAUTH_CLIENT_ID is missing", () => {
27
+ const env = { ...baseEnv, OAUTH_CLIENT_ID: undefined };
28
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_ID/);
29
+ });
30
+ it("throws if OAUTH_CLIENT_SECRET is missing", () => {
31
+ const env = { ...baseEnv, OAUTH_CLIENT_SECRET: undefined };
32
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_SECRET/);
33
+ });
34
+ it("throws if DT_ENVIRONMENT is missing", () => {
35
+ const env = { ...baseEnv, DT_ENVIRONMENT: undefined };
36
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/DT_ENVIRONMENT/);
37
+ });
38
+ it("throws if DT_ENVIRONMENT does not start with https://", () => {
39
+ const env = { ...baseEnv, DT_ENVIRONMENT: "http://abc123.apps.dynatrace.com" };
40
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/https:\/\//);
41
+ });
42
+ it("throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (any URL)", () => {
43
+ const env = { ...baseEnv, DT_ENVIRONMENT: "https://abc123.example.com" };
44
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/Dynatrace Platform Environment URL/);
45
+ });
46
+ it("throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (contains live)", () => {
47
+ const env = { ...baseEnv, DT_ENVIRONMENT: "https://abc123.live.dynatrace.com" };
48
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/Dynatrace Platform Environment URL/);
49
+ });
50
+ it("accepts DT_ENVIRONMENT with apps.dynatracelabs.com", () => {
51
+ const env = { ...baseEnv, DT_ENVIRONMENT: "https://xyz789.apps.dynatracelabs.com" };
52
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
53
+ });
54
+ it("accepts DT_ENVIRONMENT with apps.dynatrace.com", () => {
55
+ const env = { ...baseEnv, DT_ENVIRONMENT: "https://env123.apps.dynatrace.com" };
56
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
57
+ });
58
+ });
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ const update_workflow_1 = require("./capabilities/update-workflow");
20
20
  const get_vulnerability_details_1 = require("./capabilities/get-vulnerability-details");
21
21
  const execute_dql_1 = require("./capabilities/execute-dql");
22
22
  const send_slack_message_1 = require("./capabilities/send-slack-message");
23
+ const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-entity-by-name");
24
+ const getDynatraceEnv_1 = require("./getDynatraceEnv");
23
25
  (0, dotenv_1.config)();
24
26
  let scopes = [
25
27
  'app-engine:apps:run', // needed for environmentInformationClient
@@ -55,15 +57,15 @@ if (process.env.USE_WORKFLOWS) {
55
57
  }
56
58
  const main = async () => {
57
59
  // read Environment variables
58
- const oauthClient = process.env.OAUTH_CLIENT_ID;
59
- const oauthClientSecret = process.env.OAUTH_CLIENT_SECRET;
60
- const dtEnvironment = process.env.DT_ENVIRONMENT;
61
- const slackConnectionId = process.env.SLACK_CONNECTION_ID || "fake-slack-connection-id";
62
- // ensure oauthClient and dtEnvironment are set
63
- if (!oauthClient || !oauthClientSecret || !dtEnvironment) {
64
- console.error("Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables");
60
+ let dynatraceEnv;
61
+ try {
62
+ dynatraceEnv = (0, getDynatraceEnv_1.getDynatraceEnv)();
63
+ }
64
+ catch (err) {
65
+ console.error(err.message);
65
66
  process.exit(1);
66
67
  }
68
+ const { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId } = dynatraceEnv;
67
69
  // create an oauth-client
68
70
  const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopes);
69
71
  console.error("Starting Dynatrace MCP Server...");
@@ -208,6 +210,12 @@ const main = async () => {
208
210
  resp += `Tell the user to access the link ${dtEnvironment}/ui/apps/dynatrace.davis.problems/problem/${result.problemId} to get more insights into the problem.\n`;
209
211
  return resp;
210
212
  });
213
+ tool("find_entity_by_name", "Get the entityId of a monitored entity based on the name of the entity on Dynatrace", {
214
+ entityName: zod_1.z.string()
215
+ }, async ({ entityName }) => {
216
+ const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
217
+ return entityResponse;
218
+ });
211
219
  tool("get_entity_details", "Get details of a monitored entity based on the entityId on Dynatrace", {
212
220
  entityId: zod_1.z.string().optional()
213
221
  }, async ({ entityId }) => {
@@ -241,13 +249,26 @@ const main = async () => {
241
249
  const logs = await (0, get_logs_for_entity_1.getLogsForEntity)(dtClient, entityName);
242
250
  return `Logs:\n${JSON.stringify(logs?.map(logLine => logLine ? logLine.content : 'Empty log'))}`;
243
251
  });
244
- tool("verify_dql", "Verify a DQL statement on Dynatrace", {
252
+ tool("verify_dql", "Verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. This is useful to ensure that the DQL statement is valid and can be executed without errors.", {
245
253
  dqlStatement: zod_1.z.string()
246
254
  }, async ({ dqlStatement }) => {
247
255
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
248
- return `Parsing DQL Statement resulted in: ${JSON.stringify(response)}`;
256
+ let resp = 'DQL Statement Verification:\n';
257
+ if (response.notifications && response.notifications.length > 0) {
258
+ resp += `Please consider the following notifications for adapting the your DQL statement:\n`;
259
+ response.notifications.forEach((notification) => {
260
+ resp += `* ${notification.severity}: ${notification.message}\n`;
261
+ });
262
+ }
263
+ if (response.valid) {
264
+ resp += `The DQL statement is valid - you can use the "execute_dql" tool.\n`;
265
+ }
266
+ else {
267
+ resp += `The DQL statement is invalid. Please adapt your statement.\n`;
268
+ }
269
+ return resp;
249
270
  });
250
- tool("execute_dql", "Get Logs, Metrics, Spans, Events from Dynatrace by executing a DQL statement. Please use verify_dql tool before you execute a DQL statement.", {
271
+ tool("execute_dql", 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. Always use "verify_dql" tool before you execute a DQL statement. A valid statement looks like this: "fetch [logs, metrics, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}. Adapt filters for certain attributes: `traceId` could be `trace_id` or `trace.id`.', {
251
272
  dqlStatement: zod_1.z.string()
252
273
  }, async ({ dqlStatement }) => {
253
274
  const response = await (0, execute_dql_1.executeDql)(dtClient, dqlStatement);
@@ -1 +1 @@
1
- {"root":["../src/dynatrace-clients.ts","../src/index.ts","../src/capabilities/create-workflow-for-problem-notification.ts","../src/capabilities/execute-dql.ts","../src/capabilities/get-events-for-cluster.ts","../src/capabilities/get-logs-for-entity.ts","../src/capabilities/get-monitored-entity-details.ts","../src/capabilities/get-ownership-information.ts","../src/capabilities/get-problem-details.ts","../src/capabilities/get-vulnerability-details.ts","../src/capabilities/list-problems.ts","../src/capabilities/list-vulnerabilities.ts","../src/capabilities/send-slack-message.ts","../src/capabilities/update-workflow.ts"],"version":"5.8.2"}
1
+ {"root":["../src/dynatrace-clients.ts","../src/getDynatraceEnv.test.ts","../src/getDynatraceEnv.ts","../src/index.ts","../src/capabilities/create-workflow-for-problem-notification.ts","../src/capabilities/execute-dql.ts","../src/capabilities/find-monitored-entity-by-name.ts","../src/capabilities/get-events-for-cluster.ts","../src/capabilities/get-logs-for-entity.ts","../src/capabilities/get-monitored-entity-details.ts","../src/capabilities/get-ownership-information.ts","../src/capabilities/get-problem-details.ts","../src/capabilities/get-vulnerability-details.ts","../src/capabilities/list-problems.ts","../src/capabilities/list-vulnerabilities.ts","../src/capabilities/send-slack-message.ts","../src/capabilities/update-workflow.ts"],"version":"5.8.2"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Model Context Protocol (MCP) server for Dynatrace",
5
5
  "keywords": [
6
6
  "Dynatrace",
@@ -36,7 +36,8 @@
36
36
  "scripts": {
37
37
  "build": "tsc --build",
38
38
  "prepare": "npm run build",
39
- "watch": "tsc --watch"
39
+ "watch": "tsc --watch",
40
+ "test": "jest"
40
41
  },
41
42
  "author": "Dynatrace",
42
43
  "license": "MIT",
@@ -52,7 +53,10 @@
52
53
  "zod-to-json-schema": "^3.24.5"
53
54
  },
54
55
  "devDependencies": {
56
+ "@types/jest": "^30.0.0",
55
57
  "@types/node": "^22",
58
+ "jest": "^30.0.0",
59
+ "ts-jest": "^29.4.0",
56
60
  "ts-node": "^10.9.2",
57
61
  "typescript": "^5.6.2"
58
62
  }