@dynatrace-oss/dynatrace-mcp-server 0.2.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
@@ -16,9 +16,10 @@ Bring real-time observability data directly into your development workflow.
16
16
 
17
17
  - List and get [problem](https://www.dynatrace.com/hub/detail/problems/) details from your services (for example Kubernetes)
18
18
  - List and get security problems / [vulnerability](https://www.dynatrace.com/hub/detail/vulnerabilities/) details
19
- - Execute DQL(Dynatrace Query Language) like getting events or logs
19
+ - Execute DQL (Dynatrace Query Language) and retrieve logs, events, spans and metrics
20
20
  - Send Slack messages (via Slack Connector)
21
21
  - Set up notification Workflow (via Dynatrace [AutomationEngine](https://docs.dynatrace.com/docs/discover-dynatrace/platform/automationengine))
22
+ - Get more information about a monitored entity
22
23
  - Get Ownership of an entity
23
24
 
24
25
  ## Quickstart
@@ -27,6 +28,8 @@ Bring real-time observability data directly into your development workflow.
27
28
 
28
29
  You can add this MCP server (using STDIO) to your MCP Client like VS Code, Claude, Cursor, Amazon Q Developer CLI, Windsurf Github Copilot via the package `@dynatrace-oss/dynatrace-mcp-server`.
29
30
 
31
+ We recommend to always set it up for your current workspace instead of using it globally.
32
+
30
33
  **VS Code**
31
34
 
32
35
  ```json
@@ -34,6 +37,7 @@ You can add this MCP server (using STDIO) to your MCP Client like VS Code, Claud
34
37
  "servers": {
35
38
  "npx-dynatrace-mcp-server": {
36
39
  "command": "npx",
40
+ "cwd": "${workspaceFolder}",
37
41
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
38
42
  "envFile": "${workspaceFolder}/.env"
39
43
  }
@@ -61,6 +65,7 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
61
65
  ```
62
66
 
63
67
  **Claude Desktop**
68
+
64
69
  ```json
65
70
  {
66
71
  "mcpServers": {
@@ -80,6 +85,7 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
80
85
  **Amazon Q Developer CLI**
81
86
 
82
87
  The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html) provides an interactive chat experience directly in your terminal. You can ask questions, get help with AWS services, troubleshoot issues, and generate code snippets without leaving your command line environment.
88
+
83
89
  ```json
84
90
  {
85
91
  "mcpServers": {
@@ -96,45 +102,43 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
96
102
  }
97
103
  ```
98
104
 
105
+ This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
106
+
99
107
  ## Environment Variables
100
108
 
101
109
  A **Dynatrace OAuth Client** is needed to communicate with your Dynatrace Environment. Please follow the documentation about
102
110
  [creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients),
103
111
  and set up the following environment variables in order for this MCP to work:
104
112
 
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
- * `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
107
- * `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
108
- * OAuth Client Scopes:
109
- * `app-engine:apps:run` - needed for environmentInformationClient
110
- * `app-engine:functions:run` - needed for environmentInformationClient
111
- * `hub:catalog:read` - get details about installed Apps on Dynatrace Environment
112
- * `environment-api:security-problems:read` - needed for reading security problems
113
- * `environment-api:entities:read` - read monitored entities
114
- * `environment-api:problems:read` - get problems
115
- * `environment-api:metrics:read` - read metrics
116
- * `environment-api:slo:read` - read SLOs
117
- * `storage:buckets:read` - Read all system data stored on Grail
118
- * `storage:logs:read` - Read logs for reliability guardian validations
119
- * `storage:metrics:read` - Read metrics for reliability guardian validations
120
- * `storage:bizevents:read` - Read bizevents for reliability guardian validations
121
- * `storage:spans:read` - Read spans from Grail
122
- * `storage:entities:read` - Read Entities from Grail
123
- * `storage:events:read` - Read Events from Grail
124
- * `storage:system:read` - Read System Data from Grail
125
- * `storage:user.events:read` - Read User events from Grail
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`.
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`.
130
138
 
131
139
  In addition, depending on the features you use, the following variables can be configured:
132
140
 
133
- * `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
134
- * `USE_APP_SETTINGS` (boolean, `true` or `false`; default: `false`)
135
- * Requires scope `app-settings:objects:read` to read settings-objects from app settings
136
- * `USE_WORKFLOWS` (boolean, `true` or `false`; default: `false`)
137
- * 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)
138
142
 
139
143
  ## ✨ Example prompts ✨
140
144
 
@@ -142,32 +146,41 @@ Use these example prompts as a starting point. Just copy them into your IDE or a
142
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.
143
147
 
144
148
  **Find open vulnerabilities on production, setup alert.**
149
+
145
150
  ```
146
151
  I have this code snippet here in my IDE, where I get a dependency vulnerability warning for my code.
147
152
  Check if I see any open vulnerability/cve on production.
148
153
  Analyze a specific production problem.
149
154
  Setup a workflow that sends Slack alerts to the #devops-alerts channel when availability problems occur.
150
155
  ```
156
+
151
157
  **Debug intermittent 503 errors.**
158
+
152
159
  ```
153
160
  Our load balancer is intermittently returning 503 errors during peak traffic.
154
161
  Pull all recent problems detected for our front-end services and
155
162
  run a query to correlate error rates with service instance health indicators.
156
163
  I suspect we have circuit breakers triggering, but need confirmation from the telemetry data.
157
164
  ```
165
+
158
166
  **Correlate memory issue with logs.**
167
+
159
168
  ```
160
169
  There's a problem with high memory usage on one of our hosts.
161
170
  Get the problem details and then fetch related logs to help understand
162
171
  what's causing the memory spike? Which file in this repo is this related to?
163
172
  ```
173
+
164
174
  **Trace request flow analysis.**
175
+
165
176
  ```
166
177
  Our users are experiencing slow checkout processes.
167
178
  Can you execute a DQL query to show me the full request trace for our checkout flow,
168
179
  so I can identify which service is causing the bottleneck?
169
180
  ```
181
+
170
182
  **Analyze Kubernetes cluster events.**
183
+
171
184
  ```
172
185
  Our application deployments seem to be failing intermittently.
173
186
  Can you fetch recent events from our "production-cluster"
@@ -186,6 +199,7 @@ In case of any problems, you can troubleshoot SSO/OAuth issues based on our [Dyn
186
199
  It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
187
200
 
188
201
  1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes):
202
+
189
203
  ```bash
190
204
  curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
191
205
  --header 'Content-Type: application/x-www-form-urlencoded' \
@@ -196,6 +210,7 @@ curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
196
210
  ```
197
211
 
198
212
  2. Use `access_token` from the response of the above call as the bearer-token in the next call:
213
+
199
214
  ```bash
200
215
  curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environment \
201
216
  -H 'accept: application/json' \
@@ -203,6 +218,7 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
203
218
  ```
204
219
 
205
220
  3. You should retrieve a result like this:
221
+
206
222
  ```json
207
223
  {
208
224
  "environmentId": "abc12345",
@@ -212,35 +228,32 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
212
228
  }
213
229
  ```
214
230
 
215
-
216
231
  ### Problem accessing data on Grail
217
232
 
218
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.
219
234
 
220
-
221
235
  ## Development
222
236
 
223
237
  For local development purposes, you can use VSCode and GitHub Copilot.
224
238
 
225
239
  First, enable Copilot for your Workspace `.vscode/settings.json`:
240
+
226
241
  ```json
227
242
  {
228
243
  "github.copilot.enable": {
229
244
  "*": true
230
245
  }
231
246
  }
232
-
233
247
  ```
234
248
 
235
249
  Second, add the MCP to `.vscode/mcp.json`:
250
+
236
251
  ```json
237
252
  {
238
253
  "servers": {
239
254
  "my-dynatrace-mcp-server": {
240
255
  "command": "node",
241
- "args": [
242
- "${workspaceFolder}/dist/index.js"
243
- ],
256
+ "args": ["${workspaceFolder}/dist/index.js"],
244
257
  "envFile": "${workspaceFolder}/.env"
245
258
  }
246
259
  }
@@ -251,7 +264,7 @@ Third, create a `.env` file in this repository (you can copy from `.env.template
251
264
 
252
265
  Last but not least, switch to Agent Mode in CoPilot and reload tools.
253
266
 
254
-
255
267
  ## Notes
268
+
256
269
  This product is not officially supported by Dynatrace.
257
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,83 +1 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.callAppFunction = exports.createOAuthClient = void 0;
4
- const http_client_1 = require("@dynatrace-sdk/http-client");
5
- const dt_app_1 = require("dt-app");
6
- /**
7
- * Uses the provided oauth Client ID and Secret and requests a token
8
- * @param clientId - OAuth Client ID for Dynatrace
9
- * @param clientSecret - Oauth Client Secret for Dynatrace
10
- * @param authUrl - SSO Authentication URL
11
- * @param scopes - List of requested scopes
12
- * @returns
13
- */
14
- const requestToken = async (clientId, clientSecret, authUrl, scopes) => {
15
- const res = await fetch(authUrl, {
16
- method: 'POST',
17
- headers: {
18
- 'Content-Type': 'application/x-www-form-urlencoded',
19
- },
20
- body: new URLSearchParams({
21
- grant_type: 'client_credentials',
22
- client_id: clientId,
23
- client_secret: clientSecret,
24
- scope: scopes.join(' '),
25
- }),
26
- });
27
- // check if the response was okay (HTTP 2xx) or not (HTTP 4xx or 5xx)
28
- if (!res.ok) {
29
- // log the error
30
- console.error(`Failed to fetch token: ${res.status} ${res.statusText}`);
31
- }
32
- // and return the JSON result, as it contains additional information
33
- return await res.json();
34
- };
35
- /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes */
36
- const createOAuthClient = async (clientId, clientSecret, environmentUrl, scopes) => {
37
- if (!clientId) {
38
- throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"');
39
- }
40
- if (!clientSecret) {
41
- throw new Error('Failed to retrieve OAuth client secret from env "DT_APP_OAUTH_CLIENT_SECRET"');
42
- }
43
- if (!environmentUrl) {
44
- throw new Error('Failed to retrieve environment URL from env "DT_ENVIRONMENT"');
45
- }
46
- console.error(`Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId}`);
47
- const ssoBaseUrl = await (0, dt_app_1.getSSOUrl)(environmentUrl);
48
- const ssoAuthUrl = new URL('/sso/oauth2/token', ssoBaseUrl).toString();
49
- console.error(`Using SSO auth URL: ${ssoAuthUrl}`);
50
- // try to request a token, just to verify that everything is set up correctly
51
- const tokenResponse = await requestToken(clientId, clientSecret, ssoAuthUrl, scopes);
52
- // in case we didn't get a token, or error / error_description / issueId is set, we throw an error
53
- if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) {
54
- 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.`);
55
- }
56
- console.error(`Successfully retrieved token from SSO!`);
57
- return new http_client_1._OAuthHttpClient({
58
- scopes,
59
- clientId,
60
- secret: clientSecret,
61
- environmentUrl,
62
- authUrl: ssoAuthUrl,
63
- });
64
- };
65
- exports.createOAuthClient = createOAuthClient;
66
- /** Helper function to call an app-function via platform-api */
67
- const callAppFunction = async (dtClient, appId, functionName, payload) => {
68
- console.error(`Sending payload ${JSON.stringify(payload)}`);
69
- const response = await dtClient.send({
70
- url: `/platform/app-engine/app-functions/v1/apps/${appId}/api/${functionName}`,
71
- method: 'POST',
72
- headers: {
73
- 'Accept': 'application/json',
74
- 'Content-Type': 'application/json',
75
- },
76
- body: payload,
77
- statusValidator: (status) => {
78
- return [200].includes(status);
79
- },
80
- });
81
- return await response.body('json');
82
- };
83
- 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
@@ -7,7 +7,8 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
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
- const dynatrace_clients_1 = require("./dynatrace-clients");
10
+ const package_json_1 = require("../package.json");
11
+ const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
11
12
  const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
12
13
  const list_problems_1 = require("./capabilities/list-problems");
13
14
  const get_problem_details_1 = require("./capabilities/get-problem-details");
@@ -23,38 +24,10 @@ const send_slack_message_1 = require("./capabilities/send-slack-message");
23
24
  const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-entity-by-name");
24
25
  const getDynatraceEnv_1 = require("./getDynatraceEnv");
25
26
  (0, dotenv_1.config)();
26
- let scopes = [
27
+ let scopesBase = [
27
28
  'app-engine:apps:run', // needed for environmentInformationClient
28
29
  'app-engine:functions:run', // needed for environmentInformationClient
29
- 'hub:catalog:read', // get details about installed Apps on Dynatrace Environment
30
- 'environment-api:security-problems:read', // needed for reading security problems
31
- 'environment-api:entities:read', // read monitored entities
32
- 'environment-api:problems:read', // get problems
33
- 'environment-api:metrics:read', // read metrics
34
- 'environment-api:slo:read', // read SLOs
35
- 'settings:objects:read', // needed for reading settings objects, like ownership information and Guardians (SRG) from settings
36
- // 'settings:objects:write', // [OPTIONAL] not used right now
37
- // Grail related permissions: https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model/assign-permissions-in-grail
38
- 'storage:buckets:read', // Read all system data stored on Grail
39
- 'storage:logs:read', // Read logs for reliability guardian validations
40
- 'storage:metrics:read', // Read metrics for reliability guardian validations
41
- 'storage:bizevents:read', // Read bizevents for reliability guardian validations
42
- 'storage:spans:read', // Read spans from Grail
43
- 'storage:entities:read', // Read Entities from Grail
44
- 'storage:events:read', // Read events from Grail
45
- 'storage:system:read', // Read System Data from Grail
46
- 'storage:user.events:read', // Read User events from Grail
47
- 'storage:user.sessions:read', // Read User sessions from Grail
48
30
  ];
49
- // configurable call for app settings scope (not available on all environments)
50
- if (process.env.USE_APP_SETTINGS) {
51
- scopes.push('app-settings:objects:read'); // needed when using app settings in Workflows, see below
52
- }
53
- if (process.env.USE_WORKFLOWS) {
54
- scopes.push('automation:workflows:read'); // read workflows
55
- scopes.push('automation:workflows:write'); // write workflows
56
- scopes.push('automation:workflows:run'); // execute workflows
57
- }
58
31
  const main = async () => {
59
32
  // read Environment variables
60
33
  let dynatraceEnv;
@@ -66,12 +39,10 @@ const main = async () => {
66
39
  process.exit(1);
67
40
  }
68
41
  const { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId } = dynatraceEnv;
69
- // create an oauth-client
70
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopes);
71
- console.error("Starting Dynatrace MCP Server...");
42
+ console.error(`Starting Dynatrace MCP Server v${package_json_1.version}...`);
72
43
  const server = new mcp_js_1.McpServer({
73
- name: "Dynatrace MCP Server",
74
- version: "0.0.1", // ToDo: Read from package.json / hard-code?
44
+ name: 'Dynatrace MCP Server',
45
+ version: package_json_1.version,
75
46
  }, {
76
47
  capabilities: {
77
48
  tools: {},
@@ -83,7 +54,7 @@ const main = async () => {
83
54
  try {
84
55
  const response = await cb(args);
85
56
  return {
86
- content: [{ type: "text", text: response }],
57
+ content: [{ type: 'text', text: response }],
87
58
  };
88
59
  }
89
60
  catch (error) {
@@ -92,13 +63,15 @@ const main = async () => {
92
63
  const e = error;
93
64
  let additionalErrorInformation = '';
94
65
  if (e.response.status == 403) {
95
- 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.';
96
68
  }
97
69
  return {
98
- content: [{
99
- type: "text",
100
- text: `Client Request Error: ${e.message} with HTTP status: ${e.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(e.body)})`
101
- }
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
+ },
102
75
  ],
103
76
  isError: true,
104
77
  };
@@ -106,14 +79,16 @@ const main = async () => {
106
79
  // else: We don't know what kind of error happened - best-case we can provide error.message
107
80
  console.log(error);
108
81
  return {
109
- content: [{ type: "text", text: `Error: ${error.message}` }],
82
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
110
83
  isError: true,
111
84
  };
112
85
  }
113
86
  };
114
- server.tool(name, description, paramsSchema, args => wrappedCb(args));
87
+ server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
115
88
  };
116
- 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);
117
92
  const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
118
93
  const environmentInfo = await environmentInformationClient.getEnvironmentInformation();
119
94
  let resp = `Environment Information (also referred to as tenant):
@@ -121,10 +96,11 @@ const main = async () => {
121
96
  resp += `You can reach it via ${dtEnvironment}\n`;
122
97
  return resp;
123
98
  });
124
- 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'));
125
101
  const result = await (0, list_vulnerabilities_1.listVulnerabilities)(dtClient);
126
102
  if (!result || result.length === 0) {
127
- return "No vulnerabilities found";
103
+ return 'No vulnerabilities found';
128
104
  }
129
105
  let resp = `Found the following vulnerabilities:`;
130
106
  result.forEach((vulnerability) => {
@@ -133,12 +109,13 @@ const main = async () => {
133
109
  resp += `\nWe recommend to take a look at ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities to get a better overview of vulnerabilities.\n`;
134
110
  return resp;
135
111
  });
136
- tool("get_vulnerabilty_details", "Get details of a vulnerability by `securityProblemId` on Dynatrace", {
137
- 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(),
138
114
  }, async ({ securityProblemId }) => {
115
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:security-problems:read'));
139
116
  const result = await (0, get_vulnerability_details_1.getVulnerabilityDetails)(dtClient, securityProblemId);
140
117
  let resp = `The Security Problem (Vulnerability) ${result.displayId} with securityProblemId ${result.securityProblemId} has the title ${result.title}.\n`;
141
- resp += `The related CVEs are ${result.cveIds?.join(",") || "unknown"}.\n`;
118
+ resp += `The related CVEs are ${result.cveIds?.join(',') || 'unknown'}.\n`;
142
119
  resp += `The description is: ${result.description}.\n`;
143
120
  resp += `The remediation description is: ${result.remediationDescription}.\n`;
144
121
  if (result.affectedEntities && result.affectedEntities.length > 0) {
@@ -180,16 +157,18 @@ const main = async () => {
180
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`;
181
158
  return resp;
182
159
  });
183
- 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'));
184
162
  const result = await (0, list_problems_1.listProblems)(dtClient);
185
163
  if (!result || result.length === 0) {
186
- return "No problems found";
164
+ return 'No problems found';
187
165
  }
188
- return `Found these problems: ${result.join(",")}`;
166
+ return `Found these problems: ${result.join(',')}`;
189
167
  });
190
- tool("get_problem_details", "Get details of a problem on Dynatrace", {
191
- 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(),
192
170
  }, async ({ problemId }) => {
171
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:problems:read'));
193
172
  const result = await (0, get_problem_details_1.getProblemDetails)(dtClient, problemId);
194
173
  let resp = `The problem ${result.displayId} with the title ${result.title} (ID: ${result.problemId}).` +
195
174
  `The severity is ${result.severityLevel}, and it affects ${result.affectedEntities.length} entities:`;
@@ -210,48 +189,53 @@ const main = async () => {
210
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`;
211
190
  return resp;
212
191
  });
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()
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(),
215
194
  }, async ({ entityName }) => {
195
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read', 'storage:entities:read'));
216
196
  const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
217
197
  return entityResponse;
218
198
  });
219
- tool("get_entity_details", "Get details of a monitored entity based on the entityId on Dynatrace", {
220
- 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(),
221
201
  }, async ({ entityId }) => {
202
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read'));
222
203
  const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
223
204
  let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
224
205
  `Properties: ${JSON.stringify(entityDetails.properties)}\n`;
225
- if (entityDetails.type == "SERVICE") {
206
+ if (entityDetails.type == 'SERVICE') {
226
207
  resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`;
227
208
  }
228
- else if (entityDetails.type == "HOST") {
209
+ else if (entityDetails.type == 'HOST') {
229
210
  resp += `You can find more information about the host at ${dtEnvironment}/ui/apps/dynatrace.infraops/hosts/${entityDetails.entityId}`;
230
211
  }
231
- else if (entityDetails.type == "KUBERNETES_CLUSTER") {
212
+ else if (entityDetails.type == 'KUBERNETES_CLUSTER') {
232
213
  resp += `You can find more information about the cluster at ${dtEnvironment}/ui/apps/dynatrace.infraops/kubernetes/${entityDetails.entityId}`;
233
214
  }
234
- else if (entityDetails.type == "CLOUD_APPLICATION") {
215
+ else if (entityDetails.type == 'CLOUD_APPLICATION') {
235
216
  resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}`;
236
217
  }
237
218
  return resp;
238
219
  });
239
- 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', {
240
221
  channel: zod_1.z.string().optional(),
241
- message: zod_1.z.string().optional()
222
+ message: zod_1.z.string().optional(),
242
223
  }, async ({ channel, message }) => {
224
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('app-settings:objects:read'));
243
225
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
244
226
  return `Message sent to Slack channel: ${JSON.stringify(response)}`;
245
227
  });
246
- tool("get_logs_for_entity", "Get Logs for a monitored entity based on name of the entity on Dynatrace", {
247
- 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(),
248
230
  }, async ({ entityName }) => {
231
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:logs:read'));
249
232
  const logs = await (0, get_logs_for_entity_1.getLogsForEntity)(dtClient, entityName);
250
- 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')))}`;
251
234
  });
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.", {
253
- 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(),
254
237
  }, async ({ dqlStatement }) => {
238
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase);
255
239
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
256
240
  let resp = 'DQL Statement Verification:\n';
257
241
  if (response.notifications && response.notifications.length > 0) {
@@ -268,24 +252,36 @@ const main = async () => {
268
252
  }
269
253
  return resp;
270
254
  });
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`.', {
272
- 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(),
273
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'));
274
269
  const response = await (0, execute_dql_1.executeDql)(dtClient, dqlStatement);
275
270
  return `DQL Response: ${JSON.stringify(response)}`;
276
271
  });
277
- 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', {
278
273
  problemType: zod_1.z.string().optional(),
279
274
  teamName: zod_1.z.string().optional(),
280
275
  channel: zod_1.z.string().optional(),
281
- isPrivate: zod_1.z.boolean().optional().default(false)
276
+ isPrivate: zod_1.z.boolean().optional().default(false),
282
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'));
283
279
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
284
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`;
285
- if (response.type == "SIMPLE") {
281
+ if (response.type == 'SIMPLE') {
286
282
  resp += `Note: This is a simple workflow. Workflow-hours will not be billed.\n`;
287
283
  }
288
- else if (response.type == "STANDARD") {
284
+ else if (response.type == 'STANDARD') {
289
285
  resp += `Note: This is a standard workflow. Workflow-hours will be billed.\n`;
290
286
  }
291
287
  if (isPrivate) {
@@ -293,23 +289,29 @@ const main = async () => {
293
289
  }
294
290
  return resp;
295
291
  });
296
- tool("make_workflow_public", "Modify a workflow and make it publicly available to everyone on the Dynatrace Environment", {
297
- 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(),
298
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'));
299
296
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
300
297
  isPrivate: false,
301
298
  });
302
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`;
303
300
  });
304
- tool("get_kubernetes_events", "Get all events from a specific Kubernetes (K8s) cluster", {
305
- 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)`),
306
306
  }, async ({ clusterId }) => {
307
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:events:read'));
307
308
  const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
308
309
  return `Kubernetes Events:\n${JSON.stringify(events)}`;
309
310
  });
310
- tool("get_ownership", "Get detailed Ownership information for one or multiple entities on Dynatrace", {
311
- 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'),
312
313
  }, async ({ entityIds }) => {
314
+ const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'));
313
315
  console.error(`Fetching ownership for ${entityIds}`);
314
316
  const ownershipInformation = await (0, get_ownership_information_1.getOwnershipInformation)(dtClient, entityIds);
315
317
  console.error(`Done!`);
@@ -318,11 +320,11 @@ const main = async () => {
318
320
  return resp;
319
321
  });
320
322
  const transport = new stdio_js_1.StdioServerTransport();
321
- console.error("Connecting server to transport...");
323
+ console.error('Connecting server to transport...');
322
324
  await server.connect(transport);
323
- console.error("Dynatrace MCP Server running on stdio");
325
+ console.error('Dynatrace MCP Server running on stdio');
324
326
  };
325
327
  main().catch((error) => {
326
- console.error("Fatal error in main():", error);
328
+ console.error('Fatal error in main():', error);
327
329
  process.exit(1);
328
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.2.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"
@@ -1,15 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getMonitoredEntityOwner = void 0;
4
- const get_monitored_entity_details_1 = require("./get-monitored-entity-details");
5
- const getMonitoredEntityOwner = async (dtClient, entityId) => {
6
- const monitoredEntity = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
7
- const owner = monitoredEntity.tags?.find((tag) => {
8
- return tag?.key === "owner";
9
- });
10
- return {
11
- entity: monitoredEntity,
12
- owner: owner?.value
13
- };
14
- };
15
- exports.getMonitoredEntityOwner = getMonitoredEntityOwner;
@@ -1,27 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.executeWorkflow = exports.deleteWorkflow = exports.getWorkflowDetails = exports.listWorkflows = void 0;
4
- const client_automation_1 = require("@dynatrace-sdk/client-automation");
5
- const listWorkflows = async (dtClient) => {
6
- const workflowsClient = new client_automation_1.WorkflowsClient(dtClient);
7
- return await workflowsClient.getWorkflows({});
8
- };
9
- exports.listWorkflows = listWorkflows;
10
- const getWorkflowDetails = async (dtClient, workflowId) => {
11
- const workflowsClient = new client_automation_1.WorkflowsClient(dtClient);
12
- return await workflowsClient.getWorkflow({ id: workflowId });
13
- };
14
- exports.getWorkflowDetails = getWorkflowDetails;
15
- const deleteWorkflow = async (dtClient, workflowId) => {
16
- const workflowsClient = new client_automation_1.WorkflowsClient(dtClient);
17
- return await workflowsClient.deleteWorkflow({ id: workflowId });
18
- };
19
- exports.deleteWorkflow = deleteWorkflow;
20
- const executeWorkflow = async (dtClient, workflowId, input) => {
21
- const workflowsClient = new client_automation_1.WorkflowsClient(dtClient);
22
- return await workflowsClient.runWorkflow({
23
- id: workflowId,
24
- body: input || {}
25
- });
26
- };
27
- exports.executeWorkflow = executeWorkflow;
@@ -1,55 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OAuthHttpClient = void 0;
4
- class OAuthHttpClient {
5
- config;
6
- oauthToken;
7
- constructor(config) {
8
- this.config = config;
9
- }
10
- async send(options) {
11
- const { statusValidator = defaultStatusValidator } = options;
12
- if (!this.oauthToken || this.oauthToken.isExpired()) {
13
- this.oauthToken = await this.requestToken();
14
- }
15
- const requestBodyType = options.requestBodyType ?? 'json';
16
- const url = new URL(options.url, this.config.environmentUrl);
17
- const body = options.body !== undefined
18
- ? await encodeRequestBody(options.body, requestBodyType)
19
- : undefined;
20
- const headers = {
21
- ...applyContentTypeHeader(options.headers, requestBodyType),
22
- Authorization: `${this.oauthToken.tokenType} ${this.oauthToken.accessToken}`,
23
- };
24
- const response = await send(url.toString(), {
25
- method: options.method,
26
- signal: options.abortSignal,
27
- body,
28
- headers,
29
- });
30
- const ok = statusValidator(response.status);
31
- if (ok) {
32
- return new HttpClientResponse(response);
33
- }
34
- else {
35
- throw new HttpClientResponseError(response, { requestMethod: options.method });
36
- }
37
- }
38
- async requestToken() {
39
- const res = await send(this.config.authUrl, {
40
- method: 'POST',
41
- headers: {
42
- 'Content-Type': 'application/x-www-form-urlencoded',
43
- },
44
- body: new URLSearchParams({
45
- grant_type: 'client_credentials',
46
- client_id: this.config.clientId,
47
- client_secret: this.config.secret,
48
- scope: this.config.scopes.join(' '),
49
- }),
50
- });
51
- const data = await res.json();
52
- return new OAuthToken(data);
53
- }
54
- }
55
- exports.OAuthHttpClient = OAuthHttpClient;
@@ -1 +0,0 @@
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"}