@dynatrace-oss/dynatrace-mcp-server 0.3.0 → 0.4.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
@@ -110,39 +110,35 @@ A **Dynatrace OAuth Client** is needed to communicate with your Dynatrace Enviro
110
110
  [creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients),
111
111
  and set up the following environment variables in order for this MCP to work:
112
112
 
113
- * `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`)
114
- * `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
115
- * `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
116
- * OAuth Client Scopes:
117
- * `app-engine:apps:run` - needed for environmentInformationClient
118
- * `app-engine:functions:run` - needed for environmentInformationClient
119
- * `hub:catalog:read` - get details about installed Apps on Dynatrace Environment
120
- * `environment-api:security-problems:read` - needed for reading security problems
121
- * `environment-api:entities:read` - read monitored entities
122
- * `environment-api:problems:read` - get problems
123
- * `environment-api:metrics:read` - read metrics
124
- * `environment-api:slo:read` - read SLOs
125
- * `storage:buckets:read` - Read all system data stored on Grail
126
- * `storage:logs:read` - Read logs for reliability guardian validations
127
- * `storage:metrics:read` - Read metrics for reliability guardian validations
128
- * `storage:bizevents:read` - Read bizevents for reliability guardian validations
129
- * `storage:spans:read` - Read spans from Grail
130
- * `storage:entities:read` - Read Entities from Grail
131
- * `storage:events:read` - Read Events from Grail
132
- * `storage:system:read` - Read System Data from Grail
133
- * `storage:user.events:read` - Read User events from Grail
134
- * `storage:user.sessions:read` - Read User sessions from Grail
135
- * `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
136
-
137
- **Note**: Please ensure that `settings:objects:read` is used, and *not* the similarly named scope `app-settings:objects:read`.
113
+ - `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`)
114
+ - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
115
+ - `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
116
+ - OAuth Client Scopes:
117
+ - `app-engine:apps:run` - needed for environmentInformationClient
118
+ - `app-engine:functions:run` - needed for environmentInformationClient
119
+ - `hub:catalog:read` - get details about installed Apps on Dynatrace Environment
120
+ - `environment-api:security-problems:read` - needed for reading security problems
121
+ - `environment-api:entities:read` - read monitored entities
122
+ - `environment-api:problems:read` - get problems
123
+ - `environment-api:metrics:read` - read metrics
124
+ - `environment-api:slo:read` - read SLOs
125
+ - `storage:buckets:read` - Read all system data stored on Grail
126
+ - `storage:logs:read` - Read logs for reliability guardian validations
127
+ - `storage:metrics:read` - Read metrics for reliability guardian validations
128
+ - `storage:bizevents:read` - Read bizevents for reliability guardian validations
129
+ - `storage:spans:read` - Read spans from Grail
130
+ - `storage:entities:read` - Read Entities from Grail
131
+ - `storage:events:read` - Read Events from Grail
132
+ - `storage:system:read` - Read System Data from Grail
133
+ - `storage:user.events:read` - Read User events from Grail
134
+ - `storage:user.sessions:read` - Read User sessions from Grail
135
+ - `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
136
+
137
+ **Note**: Please ensure that `settings:objects:read` is used, and _not_ the similarly named scope `app-settings:objects:read`.
138
138
 
139
139
  In addition, depending on the features you use, the following variables can be configured:
140
140
 
141
- * `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
142
- * `USE_APP_SETTINGS` (boolean, `true` or `false`; default: `false`)
143
- * Requires scope `app-settings:objects:read` to read settings-objects from app settings
144
- * `USE_WORKFLOWS` (boolean, `true` or `false`; default: `false`)
145
- * Requires scopes `automation:workflows:read`, `automation:workflows:write` and `automation:workflows:run` to read, write and execute Workflows
141
+ - `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
146
142
 
147
143
  ## ✨ Example prompts ✨
148
144
 
@@ -150,32 +146,41 @@ Use these example prompts as a starting point. Just copy them into your IDE or a
150
146
  and extend them as needed. They’re here to help you imagine how real-time observability and automation work together in the MCP context in your IDE.
151
147
 
152
148
  **Find open vulnerabilities on production, setup alert.**
149
+
153
150
  ```
154
151
  I have this code snippet here in my IDE, where I get a dependency vulnerability warning for my code.
155
152
  Check if I see any open vulnerability/cve on production.
156
153
  Analyze a specific production problem.
157
154
  Setup a workflow that sends Slack alerts to the #devops-alerts channel when availability problems occur.
158
155
  ```
156
+
159
157
  **Debug intermittent 503 errors.**
158
+
160
159
  ```
161
160
  Our load balancer is intermittently returning 503 errors during peak traffic.
162
161
  Pull all recent problems detected for our front-end services and
163
162
  run a query to correlate error rates with service instance health indicators.
164
163
  I suspect we have circuit breakers triggering, but need confirmation from the telemetry data.
165
164
  ```
165
+
166
166
  **Correlate memory issue with logs.**
167
+
167
168
  ```
168
169
  There's a problem with high memory usage on one of our hosts.
169
170
  Get the problem details and then fetch related logs to help understand
170
171
  what's causing the memory spike? Which file in this repo is this related to?
171
172
  ```
173
+
172
174
  **Trace request flow analysis.**
175
+
173
176
  ```
174
177
  Our users are experiencing slow checkout processes.
175
178
  Can you execute a DQL query to show me the full request trace for our checkout flow,
176
179
  so I can identify which service is causing the bottleneck?
177
180
  ```
181
+
178
182
  **Analyze Kubernetes cluster events.**
183
+
179
184
  ```
180
185
  Our application deployments seem to be failing intermittently.
181
186
  Can you fetch recent events from our "production-cluster"
@@ -194,6 +199,7 @@ In case of any problems, you can troubleshoot SSO/OAuth issues based on our [Dyn
194
199
  It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
195
200
 
196
201
  1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes):
202
+
197
203
  ```bash
198
204
  curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
199
205
  --header 'Content-Type: application/x-www-form-urlencoded' \
@@ -204,6 +210,7 @@ curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
204
210
  ```
205
211
 
206
212
  2. Use `access_token` from the response of the above call as the bearer-token in the next call:
213
+
207
214
  ```bash
208
215
  curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environment \
209
216
  -H 'accept: application/json' \
@@ -211,6 +218,7 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
211
218
  ```
212
219
 
213
220
  3. You should retrieve a result like this:
221
+
214
222
  ```json
215
223
  {
216
224
  "environmentId": "abc12345",
@@ -220,35 +228,32 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
220
228
  }
221
229
  ```
222
230
 
223
-
224
231
  ### Problem accessing data on Grail
225
232
 
226
233
  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.
227
234
 
228
-
229
235
  ## Development
230
236
 
231
237
  For local development purposes, you can use VSCode and GitHub Copilot.
232
238
 
233
239
  First, enable Copilot for your Workspace `.vscode/settings.json`:
240
+
234
241
  ```json
235
242
  {
236
243
  "github.copilot.enable": {
237
244
  "*": true
238
245
  }
239
246
  }
240
-
241
247
  ```
242
248
 
243
249
  Second, add the MCP to `.vscode/mcp.json`:
250
+
244
251
  ```json
245
252
  {
246
253
  "servers": {
247
254
  "my-dynatrace-mcp-server": {
248
255
  "command": "node",
249
- "args": [
250
- "${workspaceFolder}/dist/index.js"
251
- ],
256
+ "args": ["${workspaceFolder}/dist/index.js"],
252
257
  "envFile": "${workspaceFolder}/.env"
253
258
  }
254
259
  }
@@ -259,7 +264,7 @@ Third, create a `.env` file in this repository (you can copy from `.env.template
259
264
 
260
265
  Last but not least, switch to Agent Mode in CoPilot and reload tools.
261
266
 
262
-
263
267
  ## Notes
268
+
264
269
  This product is not officially supported by Dynatrace.
265
270
  Please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOAuthClient = exports.ExtendedOauthClient = void 0;
4
+ const http_client_1 = require("@dynatrace-sdk/http-client");
5
+ const dt_app_1 = require("dt-app");
6
+ const package_json_1 = require("../../package.json");
7
+ /**
8
+ * Uses the provided oauth Client ID and Secret and requests a token via client-credentials flow
9
+ * @param clientId - OAuth Client ID for Dynatrace
10
+ * @param clientSecret - Oauth Client Secret for Dynatrace
11
+ * @param ssoAuthUrl - SSO Authentication URL
12
+ * @param scopes - List of requested scopes
13
+ * @returns
14
+ */
15
+ const requestToken = async (clientId, clientSecret, ssoAuthUrl, scopes) => {
16
+ const res = await fetch(ssoAuthUrl, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/x-www-form-urlencoded',
20
+ },
21
+ body: new URLSearchParams({
22
+ grant_type: 'client_credentials',
23
+ client_id: clientId,
24
+ client_secret: clientSecret,
25
+ scope: scopes.join(' '),
26
+ }),
27
+ });
28
+ // check if the response was okay (HTTP 2xx) or not (HTTP 4xx or 5xx)
29
+ if (!res.ok) {
30
+ // log the error
31
+ console.error(`Failed to fetch token: ${res.status} ${res.statusText}`);
32
+ }
33
+ // and return the JSON result, as it contains additional information
34
+ return await res.json();
35
+ };
36
+ /**
37
+ * ExtendedOAuthClient that takes parameters for clientId, secret, scopes, environmentUrl, authUrl, and the version of the dynatrace-mcp-server
38
+ */
39
+ class ExtendedOauthClient extends http_client_1._OAuthHttpClient {
40
+ userAgent;
41
+ constructor(config, userAgent) {
42
+ super(config);
43
+ this.userAgent = userAgent;
44
+ }
45
+ send(options) {
46
+ // add the user-agent header to the request
47
+ options.headers = {
48
+ ...options.headers,
49
+ 'User-Agent': this.userAgent,
50
+ };
51
+ // call the parent send method
52
+ return super.send(options);
53
+ }
54
+ }
55
+ exports.ExtendedOauthClient = ExtendedOauthClient;
56
+ /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes
57
+ * This uses a client-credentials flow to request a token from the SSO endpoint.
58
+ */
59
+ const createOAuthClient = async (clientId, clientSecret, environmentUrl, scopes) => {
60
+ if (!clientId) {
61
+ throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"');
62
+ }
63
+ if (!clientSecret) {
64
+ throw new Error('Failed to retrieve OAuth client secret from env "DT_APP_OAUTH_CLIENT_SECRET"');
65
+ }
66
+ if (!environmentUrl) {
67
+ throw new Error('Failed to retrieve environment URL from env "DT_ENVIRONMENT"');
68
+ }
69
+ console.error(`Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`);
70
+ const ssoBaseUrl = await (0, dt_app_1.getSSOUrl)(environmentUrl);
71
+ const ssoAuthUrl = new URL('/sso/oauth2/token', ssoBaseUrl).toString();
72
+ console.error(`Using SSO auth URL: ${ssoAuthUrl}`);
73
+ // try to request a token, just to verify that everything is set up correctly
74
+ const tokenResponse = await requestToken(clientId, clientSecret, ssoAuthUrl, scopes);
75
+ // in case we didn't get a token, or error / error_description / issueId is set, we throw an error
76
+ if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) {
77
+ throw new Error(`Failed to retrieve OAuth token (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}. Note: Your OAuth client is most likely not configured correctly and/or is missing scopes.`);
78
+ }
79
+ console.error(`Successfully retrieved token from SSO!`);
80
+ const userAgent = `dynatrace-mcp-server/v${package_json_1.version} (${process.platform}-${process.arch})`;
81
+ return new ExtendedOauthClient({
82
+ scopes,
83
+ clientId,
84
+ secret: clientSecret,
85
+ environmentUrl,
86
+ authUrl: ssoAuthUrl,
87
+ }, userAgent);
88
+ };
89
+ exports.createOAuthClient = createOAuthClient;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callAppFunction = void 0;
4
+ /** Helper function to call an app-function via platform-api */
5
+ const callAppFunction = async (dtClient, appId, functionName, payload) => {
6
+ console.error(`Sending payload ${JSON.stringify(payload)}`);
7
+ const response = await dtClient.send({
8
+ url: `/platform/app-engine/app-functions/v1/apps/${appId}/api/${functionName}`,
9
+ method: 'POST',
10
+ headers: {
11
+ 'Accept': 'application/json',
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: payload,
15
+ statusValidator: (status) => {
16
+ return [200].includes(status);
17
+ },
18
+ });
19
+ return await response.body('json');
20
+ };
21
+ exports.callAppFunction = callAppFunction;
@@ -12,18 +12,18 @@ const createWorkflowForProblemNotification = async (dtClient, teamName, channel,
12
12
  slowdown: false,
13
13
  resource: false,
14
14
  custom: false,
15
- info: false
15
+ info: false,
16
16
  };
17
17
  // default trigger config
18
18
  let triggerConfig = {
19
19
  type: 'event',
20
20
  value: {
21
21
  eventType: 'events',
22
- query: ''
23
- }
22
+ query: '',
23
+ },
24
24
  };
25
25
  // special case: Security Problems
26
- if (problemType.toUpperCase().indexOf("SECURITY") !== -1) {
26
+ if (problemType.toUpperCase().indexOf('SECURITY') !== -1) {
27
27
  triggerConfig.value.query = `event.kind=="SECURITY_EVENT"
28
28
  and event.type=="VULNERABILITY_STATUS_CHANGE_EVENT"
29
29
  and event.level == "ENTITY"
@@ -65,8 +65,8 @@ const createWorkflowForProblemNotification = async (dtClient, teamName, channel,
65
65
  triggerConfig = {
66
66
  type: 'davis-problem',
67
67
  value: {
68
- categories
69
- }
68
+ categories,
69
+ },
70
70
  };
71
71
  }
72
72
  let notificationWorkflow = {
@@ -76,28 +76,28 @@ const createWorkflowForProblemNotification = async (dtClient, teamName, channel,
76
76
  type: 'SIMPLE',
77
77
  // define the send_notification task
78
78
  tasks: {
79
- "send_notification": {
80
- name: "Send notification",
81
- action: "dynatrace.slack:slack-send-message",
82
- description: "Sends a notification to a Slack channel",
79
+ send_notification: {
80
+ name: 'Send notification',
81
+ action: 'dynatrace.slack:slack-send-message',
82
+ description: 'Sends a notification to a Slack channel',
83
83
  input: {
84
- connectionId: "slack-connection-id",
84
+ connectionId: 'slack-connection-id',
85
85
  channel: `{{ \"${channel}\" }}`,
86
86
  message: `🚨 Alert for Team ${teamName}\n*Problem Type*: ${problemType}\n*Problem ID*: {{ event()["display_id"] }}\n*Status*: {{ event()["event.status"] }}\n\n<{{ environment().url }}/ui/apps/dynatrace.davis.problems/problem/{{ event()["event.id"] }}|Click here for details>`,
87
87
  },
88
88
  active: true,
89
- }
89
+ },
90
90
  },
91
91
  // define a trigger
92
92
  trigger: {
93
93
  eventTrigger: {
94
94
  isActive: true,
95
95
  triggerConfiguration: triggerConfig,
96
- }
97
- }
96
+ },
97
+ },
98
98
  };
99
99
  return await workflowsclient.createWorkflow({
100
- body: notificationWorkflow
100
+ body: notificationWorkflow,
101
101
  });
102
102
  };
103
103
  exports.createWorkflowForProblemNotification = createWorkflowForProblemNotification;
@@ -7,7 +7,7 @@ const verifyDqlStatement = async (dtClient, dqlStatement) => {
7
7
  const response = await queryAssistanceClient.queryVerify({
8
8
  body: {
9
9
  query: dqlStatement,
10
- }
10
+ },
11
11
  });
12
12
  return response;
13
13
  };
@@ -17,7 +17,7 @@ const executeDql = async (dtClient, dqlStatement) => {
17
17
  const response = await queryExecutionClient.queryExecute({
18
18
  body: {
19
19
  query: dqlStatement,
20
- }
20
+ },
21
21
  });
22
22
  if (response.result) {
23
23
  // return response result immediately
@@ -29,7 +29,7 @@ const executeDql = async (dtClient, dqlStatement) => {
29
29
  let pollResponse;
30
30
  do {
31
31
  // sleep for 2 seconds
32
- await new Promise(resolve => setTimeout(resolve, 2000));
32
+ await new Promise((resolve) => setTimeout(resolve, 2000));
33
33
  pollResponse = await queryExecutionClient.queryPoll({
34
34
  requestToken: response.requestToken,
35
35
  });
@@ -5,7 +5,7 @@ const client_classic_environment_v2_1 = require("@dynatrace-sdk/client-classic-e
5
5
  const getMonitoredEntityDetails = async (dtClient, entityId) => {
6
6
  const monitoredEntitiesClient = new client_classic_environment_v2_1.MonitoredEntitiesClient(dtClient);
7
7
  const entityDetails = await monitoredEntitiesClient.getEntity({
8
- entityId: entityId
8
+ entityId: entityId,
9
9
  });
10
10
  return entityDetails;
11
11
  };
@@ -1,15 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getOwnershipInformation = void 0;
4
- const dynatrace_clients_1 = require("../dynatrace-clients");
4
+ const call_app_function_1 = require("./call-app-function");
5
5
  const getOwnershipInformation = async (dtClient, entityIds) => {
6
- const ownershipResponse = await (0, dynatrace_clients_1.callAppFunction)(dtClient, 'dynatrace.ownership', 'get-ownership-from-entity', { entityIds: entityIds });
6
+ const ownershipResponse = await (0, call_app_function_1.callAppFunction)(dtClient, 'dynatrace.ownership', 'get-ownership-from-entity', {
7
+ entityIds: entityIds,
8
+ });
7
9
  if (ownershipResponse.error) {
8
10
  // e.g., "Not enough parameters provided"
9
11
  return `Error: ${ownershipResponse.error}`;
10
12
  }
11
13
  if (ownershipResponse.result && ownershipResponse.result.owners && ownershipResponse.result.owners.length == 0) {
12
- return "No owners found - please check out how to setup owners on https://docs.dynatrace.com/docs/deliver/ownership";
14
+ return 'No owners found - please check out how to setup owners on https://docs.dynatrace.com/docs/deliver/ownership';
13
15
  }
14
16
  return ownershipResponse.result;
15
17
  };
@@ -7,7 +7,7 @@ const getProblemDetails = async (dtClient, problemId) => {
7
7
  const problemsClient = new client_classic_environment_v2_1.ProblemsClient(dtClient);
8
8
  const problemDetails = await problemsClient.getProblem({
9
9
  problemId: problemId,
10
- fields: 'evidenceDetails,affectedEntities'
10
+ fields: 'evidenceDetails,affectedEntities',
11
11
  });
12
12
  return problemDetails;
13
13
  };
@@ -6,7 +6,7 @@ const getVulnerabilityDetails = async (dtClient, securityProblemId) => {
6
6
  const securityProblemsClient = new client_classic_environment_v2_1.SecurityProblemsClient(dtClient);
7
7
  const response = await securityProblemsClient.getSecurityProblem({
8
8
  id: securityProblemId,
9
- fields: 'riskAssessment,codeLevelVulnerabilityDetails,globalCounts,description,remediationDescription,exposedEntities,affectedEntities,relatedAttacks,entryPoints'
9
+ fields: 'riskAssessment,codeLevelVulnerabilityDetails,globalCounts,description,remediationDescription,exposedEntities,affectedEntities,relatedAttacks,entryPoints',
10
10
  });
11
11
  return response;
12
12
  };
@@ -7,7 +7,7 @@ const listVulnerabilities = async (dtClient) => {
7
7
  const response = await securityProblemsClient.getSecurityProblems({
8
8
  sort: '-riskAssessment.riskScore',
9
9
  pageSize: 100,
10
- securityProblemSelector: `minRiskScore("8.0")`
10
+ securityProblemSelector: `minRiskScore("8.0")`,
11
11
  });
12
12
  const securityProblems = response.securityProblems?.map((secProb) => {
13
13
  return `${secProb.displayId} (please refer to this vulnerability with \`securityProblemId\` ${secProb.securityProblemId}): ${secProb.title} (Technology: ${secProb.technology}, External Vulnerability ID: ${secProb.externalVulnerabilityId}, CVE: ${secProb.cveIds?.join(', ')})`;
@@ -1,14 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sendSlackMessage = void 0;
4
- const dynatrace_clients_1 = require("../dynatrace-clients");
4
+ const call_app_function_1 = require("./call-app-function");
5
5
  const sendSlackMessage = async (dtClient, connectionId, channel, message) => {
6
- const response = await (0, dynatrace_clients_1.callAppFunction)(dtClient, 'dynatrace.slack', 'slack-send-message', {
7
- message: message, channel: channel, connection: connectionId,
6
+ const response = await (0, call_app_function_1.callAppFunction)(dtClient, 'dynatrace.slack', 'slack-send-message', {
7
+ message: message,
8
+ channel: channel,
9
+ connection: connectionId,
8
10
  workflowID: 'foobar-123',
9
11
  executionID: 'exec-123',
10
12
  executionDate: new Date().toString(),
11
- appendToThread: false
13
+ appendToThread: false,
12
14
  });
13
15
  if (response.error) {
14
16
  // e.g., "Not enough parameters provided"
@@ -6,7 +6,7 @@ const updateWorkflow = async (dtClient, workflowId, body) => {
6
6
  const workflowsclient = new client_automation_1.WorkflowsClient(dtClient);
7
7
  return await workflowsclient.updateWorkflow({
8
8
  id: workflowId,
9
- body: body
9
+ body: body,
10
10
  });
11
11
  };
12
12
  exports.updateWorkflow = updateWorkflow;
@@ -1,105 +1 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.callAppFunction = exports.createOAuthClient = exports.ExtendedOauthClient = void 0;
4
- const http_client_1 = require("@dynatrace-sdk/http-client");
5
- const dt_app_1 = require("dt-app");
6
- const package_json_1 = require("../package.json");
7
- /**
8
- * Uses the provided oauth Client ID and Secret and requests a token
9
- * @param clientId - OAuth Client ID for Dynatrace
10
- * @param clientSecret - Oauth Client Secret for Dynatrace
11
- * @param authUrl - SSO Authentication URL
12
- * @param scopes - List of requested scopes
13
- * @returns
14
- */
15
- const requestToken = async (clientId, clientSecret, authUrl, scopes) => {
16
- const res = await fetch(authUrl, {
17
- method: 'POST',
18
- headers: {
19
- 'Content-Type': 'application/x-www-form-urlencoded',
20
- },
21
- body: new URLSearchParams({
22
- grant_type: 'client_credentials',
23
- client_id: clientId,
24
- client_secret: clientSecret,
25
- scope: scopes.join(' '),
26
- }),
27
- });
28
- // check if the response was okay (HTTP 2xx) or not (HTTP 4xx or 5xx)
29
- if (!res.ok) {
30
- // log the error
31
- console.error(`Failed to fetch token: ${res.status} ${res.statusText}`);
32
- }
33
- // and return the JSON result, as it contains additional information
34
- return await res.json();
35
- };
36
- /**
37
- * ExtendedOAuthClient that takes parameters for clientId, secret, scopes, environmentUrl, authUrl, and the version of the dynatrace-mcp-server
38
- */
39
- class ExtendedOauthClient extends http_client_1._OAuthHttpClient {
40
- userAgent;
41
- constructor(config, userAgent) {
42
- super(config);
43
- this.userAgent = userAgent;
44
- }
45
- send(options) {
46
- // add the user-agent header to the request
47
- options.headers = {
48
- ...options.headers,
49
- 'User-Agent': this.userAgent,
50
- };
51
- // call the parent send method
52
- return super.send(options);
53
- }
54
- }
55
- exports.ExtendedOauthClient = ExtendedOauthClient;
56
- /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes */
57
- const createOAuthClient = async (clientId, clientSecret, environmentUrl, scopes) => {
58
- if (!clientId) {
59
- throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"');
60
- }
61
- if (!clientSecret) {
62
- throw new Error('Failed to retrieve OAuth client secret from env "DT_APP_OAUTH_CLIENT_SECRET"');
63
- }
64
- if (!environmentUrl) {
65
- throw new Error('Failed to retrieve environment URL from env "DT_ENVIRONMENT"');
66
- }
67
- console.error(`Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId}`);
68
- const ssoBaseUrl = await (0, dt_app_1.getSSOUrl)(environmentUrl);
69
- const ssoAuthUrl = new URL('/sso/oauth2/token', ssoBaseUrl).toString();
70
- console.error(`Using SSO auth URL: ${ssoAuthUrl}`);
71
- // try to request a token, just to verify that everything is set up correctly
72
- const tokenResponse = await requestToken(clientId, clientSecret, ssoAuthUrl, scopes);
73
- // in case we didn't get a token, or error / error_description / issueId is set, we throw an error
74
- if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) {
75
- throw new Error(`Failed to retrieve OAuth token (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}. Note: Your OAuth client is most likely not configured correctly.`);
76
- }
77
- console.error(`Successfully retrieved token from SSO!`);
78
- const userAgent = `dynatrace-mcp-server/v${package_json_1.version} (${process.platform}-${process.arch})`;
79
- return new ExtendedOauthClient({
80
- scopes,
81
- clientId,
82
- secret: clientSecret,
83
- environmentUrl,
84
- authUrl: ssoAuthUrl,
85
- }, userAgent);
86
- };
87
- exports.createOAuthClient = createOAuthClient;
88
- /** Helper function to call an app-function via platform-api */
89
- const callAppFunction = async (dtClient, appId, functionName, payload) => {
90
- console.error(`Sending payload ${JSON.stringify(payload)}`);
91
- const response = await dtClient.send({
92
- url: `/platform/app-engine/app-functions/v1/apps/${appId}/api/${functionName}`,
93
- method: 'POST',
94
- headers: {
95
- 'Accept': 'application/json',
96
- 'Content-Type': 'application/json',
97
- },
98
- body: payload,
99
- statusValidator: (status) => {
100
- return [200].includes(status);
101
- },
102
- });
103
- return await response.body('json');
104
- };
105
- exports.callAppFunction = callAppFunction;
@@ -9,15 +9,15 @@ function getDynatraceEnv(env = process.env) {
9
9
  const oauthClient = env.OAUTH_CLIENT_ID;
10
10
  const oauthClientSecret = env.OAUTH_CLIENT_SECRET;
11
11
  const dtEnvironment = env.DT_ENVIRONMENT;
12
- const slackConnectionId = env.SLACK_CONNECTION_ID || "fake-slack-connection-id";
12
+ const slackConnectionId = env.SLACK_CONNECTION_ID || 'fake-slack-connection-id';
13
13
  if (!oauthClient || !oauthClientSecret || !dtEnvironment) {
14
- throw new Error("Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables");
14
+ throw new Error('Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables');
15
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)");
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
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)");
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
21
  }
22
22
  return { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId };
23
23
  }
@@ -1,58 +1,70 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const getDynatraceEnv_1 = require("./getDynatraceEnv");
4
- describe("getDynatraceEnv", () => {
4
+ describe('getDynatraceEnv', () => {
5
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"
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
10
  };
11
- it("returns all required values when environment is valid", () => {
11
+ it('returns all required values when environment is valid', () => {
12
12
  const env = { ...baseEnv };
13
13
  const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
14
14
  expect(result).toEqual({
15
15
  oauthClient: env.OAUTH_CLIENT_ID,
16
16
  oauthClientSecret: env.OAUTH_CLIENT_SECRET,
17
17
  dtEnvironment: env.DT_ENVIRONMENT,
18
- slackConnectionId: env.SLACK_CONNECTION_ID
18
+ slackConnectionId: env.SLACK_CONNECTION_ID,
19
19
  });
20
20
  });
21
- it("uses default slackConnectionId if not set", () => {
21
+ it('uses default slackConnectionId if not set', () => {
22
22
  const env = { ...baseEnv, SLACK_CONNECTION_ID: undefined };
23
23
  const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
24
- expect(result.slackConnectionId).toBe("fake-slack-connection-id");
24
+ expect(result.slackConnectionId).toBe('fake-slack-connection-id');
25
25
  });
26
- it("throws if OAUTH_CLIENT_ID is missing", () => {
26
+ it('throws if OAUTH_CLIENT_ID is missing', () => {
27
27
  const env = { ...baseEnv, OAUTH_CLIENT_ID: undefined };
28
28
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_ID/);
29
29
  });
30
- it("throws if OAUTH_CLIENT_SECRET is missing", () => {
30
+ it('throws if OAUTH_CLIENT_SECRET is missing', () => {
31
31
  const env = { ...baseEnv, OAUTH_CLIENT_SECRET: undefined };
32
32
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_SECRET/);
33
33
  });
34
- it("throws if DT_ENVIRONMENT is missing", () => {
34
+ it('throws if DT_ENVIRONMENT is missing', () => {
35
35
  const env = { ...baseEnv, DT_ENVIRONMENT: undefined };
36
36
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/DT_ENVIRONMENT/);
37
37
  });
38
- it("throws if DT_ENVIRONMENT does not start with https://", () => {
39
- const env = { ...baseEnv, DT_ENVIRONMENT: "http://abc123.apps.dynatrace.com" };
38
+ it('throws if DT_ENVIRONMENT does not start with https://', () => {
39
+ const env = {
40
+ ...baseEnv,
41
+ DT_ENVIRONMENT: 'http://abc123.apps.dynatrace.com',
42
+ };
40
43
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/https:\/\//);
41
44
  });
42
- it("throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (any URL)", () => {
43
- const env = { ...baseEnv, DT_ENVIRONMENT: "https://abc123.example.com" };
45
+ it('throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (any URL)', () => {
46
+ const env = { ...baseEnv, DT_ENVIRONMENT: 'https://abc123.example.com' };
44
47
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/Dynatrace Platform Environment URL/);
45
48
  });
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" };
49
+ it('throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (contains live)', () => {
50
+ const env = {
51
+ ...baseEnv,
52
+ DT_ENVIRONMENT: 'https://abc123.live.dynatrace.com',
53
+ };
48
54
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/Dynatrace Platform Environment URL/);
49
55
  });
50
- it("accepts DT_ENVIRONMENT with apps.dynatracelabs.com", () => {
51
- const env = { ...baseEnv, DT_ENVIRONMENT: "https://xyz789.apps.dynatracelabs.com" };
56
+ it('accepts DT_ENVIRONMENT with apps.dynatracelabs.com', () => {
57
+ const env = {
58
+ ...baseEnv,
59
+ DT_ENVIRONMENT: 'https://xyz789.apps.dynatracelabs.com',
60
+ };
52
61
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
53
62
  });
54
- it("accepts DT_ENVIRONMENT with apps.dynatrace.com", () => {
55
- const env = { ...baseEnv, DT_ENVIRONMENT: "https://env123.apps.dynatrace.com" };
63
+ it('accepts DT_ENVIRONMENT with apps.dynatrace.com', () => {
64
+ const env = {
65
+ ...baseEnv,
66
+ DT_ENVIRONMENT: 'https://env123.apps.dynatrace.com',
67
+ };
56
68
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
57
69
  });
58
70
  });
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
8
  const dotenv_1 = require("dotenv");
9
9
  const zod_1 = require("zod");
10
10
  const package_json_1 = require("../package.json");
11
- const dynatrace_clients_1 = require("./dynatrace-clients");
11
+ const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
12
12
  const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
13
13
  const list_problems_1 = require("./capabilities/list-problems");
14
14
  const get_problem_details_1 = require("./capabilities/get-problem-details");
@@ -24,38 +24,10 @@ const send_slack_message_1 = require("./capabilities/send-slack-message");
24
24
  const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-entity-by-name");
25
25
  const getDynatraceEnv_1 = require("./getDynatraceEnv");
26
26
  (0, dotenv_1.config)();
27
- let scopes = [
27
+ let scopesBase = [
28
28
  'app-engine:apps:run', // needed for environmentInformationClient
29
29
  'app-engine:functions:run', // needed for environmentInformationClient
30
- 'hub:catalog:read', // get details about installed Apps on Dynatrace Environment
31
- 'environment-api:security-problems:read', // needed for reading security problems
32
- 'environment-api:entities:read', // read monitored entities
33
- 'environment-api:problems:read', // get problems
34
- 'environment-api:metrics:read', // read metrics
35
- 'environment-api:slo:read', // read SLOs
36
- 'settings:objects:read', // needed for reading settings objects, like ownership information and Guardians (SRG) from settings
37
- // 'settings:objects:write', // [OPTIONAL] not used right now
38
- // Grail related permissions: https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model/assign-permissions-in-grail
39
- 'storage:buckets:read', // Read all system data stored on Grail
40
- 'storage:logs:read', // Read logs for reliability guardian validations
41
- 'storage:metrics:read', // Read metrics for reliability guardian validations
42
- 'storage:bizevents:read', // Read bizevents for reliability guardian validations
43
- 'storage:spans:read', // Read spans from Grail
44
- 'storage:entities:read', // Read Entities from Grail
45
- 'storage:events:read', // Read events from Grail
46
- 'storage:system:read', // Read System Data from Grail
47
- 'storage:user.events:read', // Read User events from Grail
48
- 'storage:user.sessions:read', // Read User sessions from Grail
49
30
  ];
50
- // configurable call for app settings scope (not available on all environments)
51
- if (process.env.USE_APP_SETTINGS) {
52
- scopes.push('app-settings:objects:read'); // needed when using app settings in Workflows, see below
53
- }
54
- if (process.env.USE_WORKFLOWS) {
55
- scopes.push('automation:workflows:read'); // read workflows
56
- scopes.push('automation:workflows:write'); // write workflows
57
- scopes.push('automation:workflows:run'); // execute workflows
58
- }
59
31
  const main = async () => {
60
32
  // read Environment variables
61
33
  let dynatraceEnv;
@@ -67,11 +39,9 @@ const main = async () => {
67
39
  process.exit(1);
68
40
  }
69
41
  const { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId } = dynatraceEnv;
70
- // create an oauth-client
71
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopes);
72
42
  console.error(`Starting Dynatrace MCP Server v${package_json_1.version}...`);
73
43
  const server = new mcp_js_1.McpServer({
74
- name: "Dynatrace MCP Server",
44
+ name: 'Dynatrace MCP Server',
75
45
  version: package_json_1.version,
76
46
  }, {
77
47
  capabilities: {
@@ -84,7 +54,7 @@ const main = async () => {
84
54
  try {
85
55
  const response = await cb(args);
86
56
  return {
87
- content: [{ type: "text", text: response }],
57
+ content: [{ type: 'text', text: response }],
88
58
  };
89
59
  }
90
60
  catch (error) {
@@ -93,13 +63,15 @@ const main = async () => {
93
63
  const e = error;
94
64
  let additionalErrorInformation = '';
95
65
  if (e.response.status == 403) {
96
- additionalErrorInformation = 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
66
+ additionalErrorInformation =
67
+ 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
97
68
  }
98
69
  return {
99
- content: [{
100
- type: "text",
101
- text: `Client Request Error: ${e.message} with HTTP status: ${e.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(e.body)})`
102
- }
70
+ content: [
71
+ {
72
+ type: 'text',
73
+ text: `Client Request Error: ${e.message} with HTTP status: ${e.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(e.body)})`,
74
+ },
103
75
  ],
104
76
  isError: true,
105
77
  };
@@ -107,14 +79,16 @@ const main = async () => {
107
79
  // else: We don't know what kind of error happened - best-case we can provide error.message
108
80
  console.log(error);
109
81
  return {
110
- content: [{ type: "text", text: `Error: ${error.message}` }],
82
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
111
83
  isError: true,
112
84
  };
113
85
  }
114
86
  };
115
- server.tool(name, description, paramsSchema, args => wrappedCb(args));
87
+ server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
116
88
  };
117
- tool("get_environment_info", "Get information about the connected Dynatrace Environment (Tenant)", {}, async ({}) => {
89
+ tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant)', {}, async ({}) => {
90
+ // create an oauth-client
91
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase);
118
92
  const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
119
93
  const environmentInfo = await environmentInformationClient.getEnvironmentInformation();
120
94
  let resp = `Environment Information (also referred to as tenant):
@@ -122,10 +96,11 @@ const main = async () => {
122
96
  resp += `You can reach it via ${dtEnvironment}\n`;
123
97
  return resp;
124
98
  });
125
- tool("list_vulnerabilities", "List all vulnerabilities from Dynatrace", {}, async ({}) => {
99
+ tool('list_vulnerabilities', 'List all vulnerabilities from Dynatrace', {}, async ({}) => {
100
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:security-problems:read'));
126
101
  const result = await (0, list_vulnerabilities_1.listVulnerabilities)(dtClient);
127
102
  if (!result || result.length === 0) {
128
- return "No vulnerabilities found";
103
+ return 'No vulnerabilities found';
129
104
  }
130
105
  let resp = `Found the following vulnerabilities:`;
131
106
  result.forEach((vulnerability) => {
@@ -134,12 +109,13 @@ const main = async () => {
134
109
  resp += `\nWe recommend to take a look at ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities to get a better overview of vulnerabilities.\n`;
135
110
  return resp;
136
111
  });
137
- tool("get_vulnerabilty_details", "Get details of a vulnerability by `securityProblemId` on Dynatrace", {
138
- securityProblemId: zod_1.z.string().optional()
112
+ tool('get_vulnerabilty_details', 'Get details of a vulnerability by `securityProblemId` on Dynatrace', {
113
+ securityProblemId: zod_1.z.string().optional(),
139
114
  }, async ({ securityProblemId }) => {
115
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:security-problems:read'));
140
116
  const result = await (0, get_vulnerability_details_1.getVulnerabilityDetails)(dtClient, securityProblemId);
141
117
  let resp = `The Security Problem (Vulnerability) ${result.displayId} with securityProblemId ${result.securityProblemId} has the title ${result.title}.\n`;
142
- resp += `The related CVEs are ${result.cveIds?.join(",") || "unknown"}.\n`;
118
+ resp += `The related CVEs are ${result.cveIds?.join(',') || 'unknown'}.\n`;
143
119
  resp += `The description is: ${result.description}.\n`;
144
120
  resp += `The remediation description is: ${result.remediationDescription}.\n`;
145
121
  if (result.affectedEntities && result.affectedEntities.length > 0) {
@@ -181,16 +157,18 @@ const main = async () => {
181
157
  resp += `Tell the user to access the link ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities/vulnerabilities/${result.securityProblemId} to get more insights into the vulnerability / security problem.\n`;
182
158
  return resp;
183
159
  });
184
- tool("list_problems", "List all problems known on Dynatrace", {}, async ({}) => {
160
+ tool('list_problems', 'List all problems known on Dynatrace', {}, async ({}) => {
161
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:problems:read'));
185
162
  const result = await (0, list_problems_1.listProblems)(dtClient);
186
163
  if (!result || result.length === 0) {
187
- return "No problems found";
164
+ return 'No problems found';
188
165
  }
189
- return `Found these problems: ${result.join(",")}`;
166
+ return `Found these problems: ${result.join(',')}`;
190
167
  });
191
- tool("get_problem_details", "Get details of a problem on Dynatrace", {
192
- problemId: zod_1.z.string().optional()
168
+ tool('get_problem_details', 'Get details of a problem on Dynatrace', {
169
+ problemId: zod_1.z.string().optional(),
193
170
  }, async ({ problemId }) => {
171
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:problems:read'));
194
172
  const result = await (0, get_problem_details_1.getProblemDetails)(dtClient, problemId);
195
173
  let resp = `The problem ${result.displayId} with the title ${result.title} (ID: ${result.problemId}).` +
196
174
  `The severity is ${result.severityLevel}, and it affects ${result.affectedEntities.length} entities:`;
@@ -211,48 +189,53 @@ const main = async () => {
211
189
  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`;
212
190
  return resp;
213
191
  });
214
- tool("find_entity_by_name", "Get the entityId of a monitored entity based on the name of the entity on Dynatrace", {
215
- entityName: zod_1.z.string()
192
+ tool('find_entity_by_name', 'Get the entityId of a monitored entity based on the name of the entity on Dynatrace', {
193
+ entityName: zod_1.z.string(),
216
194
  }, async ({ entityName }) => {
195
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read', 'storage:entities:read'));
217
196
  const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
218
197
  return entityResponse;
219
198
  });
220
- tool("get_entity_details", "Get details of a monitored entity based on the entityId on Dynatrace", {
221
- entityId: zod_1.z.string().optional()
199
+ tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
200
+ entityId: zod_1.z.string().optional(),
222
201
  }, async ({ entityId }) => {
202
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read'));
223
203
  const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
224
204
  let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
225
205
  `Properties: ${JSON.stringify(entityDetails.properties)}\n`;
226
- if (entityDetails.type == "SERVICE") {
206
+ if (entityDetails.type == 'SERVICE') {
227
207
  resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`;
228
208
  }
229
- else if (entityDetails.type == "HOST") {
209
+ else if (entityDetails.type == 'HOST') {
230
210
  resp += `You can find more information about the host at ${dtEnvironment}/ui/apps/dynatrace.infraops/hosts/${entityDetails.entityId}`;
231
211
  }
232
- else if (entityDetails.type == "KUBERNETES_CLUSTER") {
212
+ else if (entityDetails.type == 'KUBERNETES_CLUSTER') {
233
213
  resp += `You can find more information about the cluster at ${dtEnvironment}/ui/apps/dynatrace.infraops/kubernetes/${entityDetails.entityId}`;
234
214
  }
235
- else if (entityDetails.type == "CLOUD_APPLICATION") {
215
+ else if (entityDetails.type == 'CLOUD_APPLICATION') {
236
216
  resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}`;
237
217
  }
238
218
  return resp;
239
219
  });
240
- tool("send_slack_message", "Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace", {
220
+ tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
241
221
  channel: zod_1.z.string().optional(),
242
- message: zod_1.z.string().optional()
222
+ message: zod_1.z.string().optional(),
243
223
  }, async ({ channel, message }) => {
224
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('app-settings:objects:read'));
244
225
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
245
226
  return `Message sent to Slack channel: ${JSON.stringify(response)}`;
246
227
  });
247
- tool("get_logs_for_entity", "Get Logs for a monitored entity based on name of the entity on Dynatrace", {
248
- entityName: zod_1.z.string().optional()
228
+ tool('get_logs_for_entity', 'Get Logs for a monitored entity based on name of the entity on Dynatrace', {
229
+ entityName: zod_1.z.string().optional(),
249
230
  }, async ({ entityName }) => {
231
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:logs:read'));
250
232
  const logs = await (0, get_logs_for_entity_1.getLogsForEntity)(dtClient, entityName);
251
- return `Logs:\n${JSON.stringify(logs?.map(logLine => logLine ? logLine.content : 'Empty log'))}`;
233
+ return `Logs:\n${JSON.stringify(logs?.map((logLine) => (logLine ? logLine.content : 'Empty log')))}`;
252
234
  });
253
- 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.", {
254
- dqlStatement: zod_1.z.string()
235
+ 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.', {
236
+ dqlStatement: zod_1.z.string(),
255
237
  }, async ({ dqlStatement }) => {
238
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase);
256
239
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
257
240
  let resp = 'DQL Statement Verification:\n';
258
241
  if (response.notifications && response.notifications.length > 0) {
@@ -269,24 +252,36 @@ const main = async () => {
269
252
  }
270
253
  return resp;
271
254
  });
272
- 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`.', {
273
- dqlStatement: zod_1.z.string()
255
+ 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`.', {
256
+ dqlStatement: zod_1.z.string(),
274
257
  }, async ({ dqlStatement }) => {
258
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
259
+ 'storage:logs:read', // Read logs for reliability guardian validations
260
+ 'storage:metrics:read', // Read metrics for reliability guardian validations
261
+ 'storage:bizevents:read', // Read bizevents for reliability guardian validations
262
+ 'storage:spans:read', // Read spans from Grail
263
+ 'storage:entities:read', // Read Entities from Grail
264
+ 'storage:events:read', // Read events from Grail
265
+ 'storage:system:read', // Read System Data from Grail
266
+ 'storage:user.events:read', // Read User events from Grail
267
+ 'storage:user.sessions:read', // Read User sessions from Grail
268
+ 'storage:security.events:read'));
275
269
  const response = await (0, execute_dql_1.executeDql)(dtClient, dqlStatement);
276
270
  return `DQL Response: ${JSON.stringify(response)}`;
277
271
  });
278
- tool("create_workflow_for_notification", "Create a notification for a team based on a problem type within Workflows in Dynatrace", {
272
+ tool('create_workflow_for_notification', 'Create a notification for a team based on a problem type within Workflows in Dynatrace', {
279
273
  problemType: zod_1.z.string().optional(),
280
274
  teamName: zod_1.z.string().optional(),
281
275
  channel: zod_1.z.string().optional(),
282
- isPrivate: zod_1.z.boolean().optional().default(false)
276
+ isPrivate: zod_1.z.boolean().optional().default(false),
283
277
  }, async ({ problemType, teamName, channel, isPrivate }) => {
278
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'));
284
279
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
285
280
  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`;
286
- if (response.type == "SIMPLE") {
281
+ if (response.type == 'SIMPLE') {
287
282
  resp += `Note: This is a simple workflow. Workflow-hours will not be billed.\n`;
288
283
  }
289
- else if (response.type == "STANDARD") {
284
+ else if (response.type == 'STANDARD') {
290
285
  resp += `Note: This is a standard workflow. Workflow-hours will be billed.\n`;
291
286
  }
292
287
  if (isPrivate) {
@@ -294,23 +289,29 @@ const main = async () => {
294
289
  }
295
290
  return resp;
296
291
  });
297
- tool("make_workflow_public", "Modify a workflow and make it publicly available to everyone on the Dynatrace Environment", {
298
- workflowId: zod_1.z.string().optional()
292
+ tool('make_workflow_public', 'Modify a workflow and make it publicly available to everyone on the Dynatrace Environment', {
293
+ workflowId: zod_1.z.string().optional(),
299
294
  }, async ({ workflowId }) => {
295
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'));
300
296
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
301
297
  isPrivate: false,
302
298
  });
303
299
  return `Workflow ${response.id} is now public!\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`;
304
300
  });
305
- tool("get_kubernetes_events", "Get all events from a specific Kubernetes (K8s) cluster", {
306
- clusterId: zod_1.z.string().optional().describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`)
301
+ tool('get_kubernetes_events', 'Get all events from a specific Kubernetes (K8s) cluster', {
302
+ clusterId: zod_1.z
303
+ .string()
304
+ .optional()
305
+ .describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`),
307
306
  }, async ({ clusterId }) => {
307
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:events:read'));
308
308
  const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
309
309
  return `Kubernetes Events:\n${JSON.stringify(events)}`;
310
310
  });
311
- tool("get_ownership", "Get detailed Ownership information for one or multiple entities on Dynatrace", {
312
- entityIds: zod_1.z.string().optional().describe("Comma separated list of entityIds"),
311
+ tool('get_ownership', 'Get detailed Ownership information for one or multiple entities on Dynatrace', {
312
+ entityIds: zod_1.z.string().optional().describe('Comma separated list of entityIds'),
313
313
  }, async ({ entityIds }) => {
314
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'));
314
315
  console.error(`Fetching ownership for ${entityIds}`);
315
316
  const ownershipInformation = await (0, get_ownership_information_1.getOwnershipInformation)(dtClient, entityIds);
316
317
  console.error(`Done!`);
@@ -319,11 +320,11 @@ const main = async () => {
319
320
  return resp;
320
321
  });
321
322
  const transport = new stdio_js_1.StdioServerTransport();
322
- console.error("Connecting server to transport...");
323
+ console.error('Connecting server to transport...');
323
324
  await server.connect(transport);
324
- console.error("Dynatrace MCP Server running on stdio");
325
+ console.error('Dynatrace MCP Server running on stdio');
325
326
  };
326
327
  main().catch((error) => {
327
- console.error("Fatal error in main():", error);
328
+ console.error('Fatal error in main():', error);
328
329
  process.exit(1);
329
330
  });
@@ -0,0 +1 @@
1
+ "use strict";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Model Context Protocol (MCP) server for Dynatrace",
5
5
  "keywords": [
6
6
  "Dynatrace",
@@ -37,7 +37,9 @@
37
37
  "build": "tsc --build",
38
38
  "prepare": "npm run build",
39
39
  "watch": "tsc --watch",
40
- "test": "jest"
40
+ "test": "jest",
41
+ "prettier": "prettier --check .",
42
+ "prettier:fix": "prettier --write ."
41
43
  },
42
44
  "author": "Dynatrace",
43
45
  "license": "MIT",
@@ -56,6 +58,7 @@
56
58
  "@types/jest": "^30.0.0",
57
59
  "@types/node": "^22",
58
60
  "jest": "^30.0.0",
61
+ "prettier": "^3.6.2",
59
62
  "ts-jest": "^29.4.0",
60
63
  "ts-node": "^10.9.2",
61
64
  "typescript": "^5.6.2"