@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 +42 -37
- package/dist/authentication/dynatrace-clients.js +89 -0
- package/dist/authentication/types.js +2 -0
- package/dist/capabilities/call-app-function.js +21 -0
- package/dist/capabilities/create-workflow-for-problem-notification.js +15 -15
- package/dist/capabilities/execute-dql.js +3 -3
- package/dist/capabilities/get-monitored-entity-details.js +1 -1
- package/dist/capabilities/get-ownership-information.js +5 -3
- package/dist/capabilities/get-problem-details.js +1 -1
- package/dist/capabilities/get-vulnerability-details.js +1 -1
- package/dist/capabilities/list-vulnerabilities.js +1 -1
- package/dist/capabilities/send-slack-message.js +6 -4
- package/dist/capabilities/update-workflow.js +1 -1
- package/dist/dynatrace-clients.js +0 -104
- package/dist/getDynatraceEnv.js +6 -6
- package/dist/getDynatraceEnv.test.js +34 -22
- package/dist/index.js +83 -82
- package/dist/types/http-client-types.js +1 -0
- package/package.json +5 -2
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
**Note**: Please ensure that `settings:objects:read` is used, and
|
|
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
|
-
|
|
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,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(
|
|
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
|
-
|
|
80
|
-
name:
|
|
81
|
-
action:
|
|
82
|
-
description:
|
|
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:
|
|
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
|
|
4
|
+
const call_app_function_1 = require("./call-app-function");
|
|
5
5
|
const getOwnershipInformation = async (dtClient, entityIds) => {
|
|
6
|
-
const ownershipResponse = await (0,
|
|
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
|
|
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
|
|
4
|
+
const call_app_function_1 = require("./call-app-function");
|
|
5
5
|
const sendSlackMessage = async (dtClient, connectionId, channel, message) => {
|
|
6
|
-
const response = await (0,
|
|
7
|
-
message: message,
|
|
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;
|
package/dist/getDynatraceEnv.js
CHANGED
|
@@ -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 ||
|
|
12
|
+
const slackConnectionId = env.SLACK_CONNECTION_ID || 'fake-slack-connection-id';
|
|
13
13
|
if (!oauthClient || !oauthClientSecret || !dtEnvironment) {
|
|
14
|
-
throw new Error(
|
|
14
|
+
throw new Error('Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables');
|
|
15
15
|
}
|
|
16
|
-
if (!dtEnvironment.startsWith(
|
|
17
|
-
throw new Error(
|
|
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(
|
|
20
|
-
throw new Error(
|
|
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(
|
|
4
|
+
describe('getDynatraceEnv', () => {
|
|
5
5
|
const baseEnv = {
|
|
6
|
-
OAUTH_CLIENT_ID:
|
|
7
|
-
OAUTH_CLIENT_SECRET:
|
|
8
|
-
DT_ENVIRONMENT:
|
|
9
|
-
SLACK_CONNECTION_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(
|
|
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(
|
|
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(
|
|
24
|
+
expect(result.slackConnectionId).toBe('fake-slack-connection-id');
|
|
25
25
|
});
|
|
26
|
-
it(
|
|
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(
|
|
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(
|
|
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(
|
|
39
|
-
const env = {
|
|
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(
|
|
43
|
-
const env = { ...baseEnv, DT_ENVIRONMENT:
|
|
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(
|
|
47
|
-
const env = {
|
|
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(
|
|
51
|
-
const env = {
|
|
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(
|
|
55
|
-
const env = {
|
|
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
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
101
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ==
|
|
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 ==
|
|
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(
|
|
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(
|
|
306
|
-
clusterId: zod_1.z
|
|
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(
|
|
312
|
-
entityIds: zod_1.z.string().optional().describe(
|
|
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(
|
|
323
|
+
console.error('Connecting server to transport...');
|
|
323
324
|
await server.connect(transport);
|
|
324
|
-
console.error(
|
|
325
|
+
console.error('Dynatrace MCP Server running on stdio');
|
|
325
326
|
};
|
|
326
327
|
main().catch((error) => {
|
|
327
|
-
console.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
|
+
"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"
|