@dynatrace-oss/dynatrace-mcp-server 0.3.0 → 0.5.0-rc.1
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 +91 -47
- package/dist/authentication/dynatrace-clients.js +92 -0
- package/dist/authentication/dynatrace-clients.test.js +189 -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/davis-copilot.js +63 -0
- 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 +13 -9
- package/dist/getDynatraceEnv.test.js +42 -27
- package/dist/index.js +171 -83
- package/dist/types/http-client-types.js +1 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@ Bring real-time observability data directly into your development workflow.
|
|
|
7
7
|
|
|
8
8
|
## Use cases
|
|
9
9
|
|
|
10
|
-
- Real-time observability
|
|
11
|
-
- Fix issues
|
|
12
|
-
-
|
|
13
|
-
- Natural language
|
|
10
|
+
- **Real-time observability** - Fetch production-level data for early detection and proactive monitoring
|
|
11
|
+
- **Contextual debugging** - Fix issues with full context from monitored exceptions, logs, and anomalies
|
|
12
|
+
- **Security insights** - Get detailed vulnerability analysis and security problem tracking
|
|
13
|
+
- **Natural language queries** - Use AI-powered DQL generation and explanation
|
|
14
14
|
|
|
15
15
|
## Capabilities
|
|
16
16
|
|
|
@@ -22,9 +22,16 @@ Bring real-time observability data directly into your development workflow.
|
|
|
22
22
|
- Get more information about a monitored entity
|
|
23
23
|
- Get Ownership of an entity
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
### AI-Powered Assistance (Preview)
|
|
26
|
+
|
|
27
|
+
- **Natural Language to DQL** - Convert plain English queries to Dynatrace Query Language
|
|
28
|
+
- **DQL Explanation** - Get plain English explanations of complex DQL queries
|
|
29
|
+
- **AI Chat Assistant** - Get contextual help and guidance for Dynatrace questions
|
|
30
|
+
- **Feedback System** - Provide feedback to improve AI responses over time
|
|
26
31
|
|
|
27
|
-
**
|
|
32
|
+
> **Note:** While Davis CoPilot AI is generally available (GA), the Davis CoPilot APIs are currently in preview. For more information, visit the [Davis CoPilot Preview Community](https://dt-url.net/copilot-community).
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
28
35
|
|
|
29
36
|
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`.
|
|
30
37
|
|
|
@@ -106,76 +113,113 @@ This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
|
|
|
106
113
|
|
|
107
114
|
## Environment Variables
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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`.
|
|
116
|
+
You can set up authentication via **OAuth Client** or **Platform Tokens** (v0.5.0 and newer) via the following environment variables:
|
|
117
|
+
|
|
118
|
+
- `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`)
|
|
119
|
+
- `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
|
|
120
|
+
- `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
|
|
121
|
+
- With v0.5.0 and newer: `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - Dynatrace Platform Token (limited support, as not all scopes are available; see below)
|
|
122
|
+
|
|
123
|
+
For more information, please have a look at the documentation about
|
|
124
|
+
[creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients), as well as
|
|
125
|
+
[creating a Platform Token in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/platform-tokens).
|
|
138
126
|
|
|
139
127
|
In addition, depending on the features you use, the following variables can be configured:
|
|
140
128
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
129
|
+
- `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
|
|
130
|
+
|
|
131
|
+
### Scopes for Authentication
|
|
132
|
+
|
|
133
|
+
Depending on the features you are using, the following scopes are needed:
|
|
134
|
+
|
|
135
|
+
- `app-engine:apps:run` - needed for almost all tools
|
|
136
|
+
- `app-engine:functions:run` - needed for for almost all tools
|
|
137
|
+
- `environment-api:security-problems:read` - needed for reading security problems (_currently not available for Platform Tokens_)
|
|
138
|
+
- `environment-api:entities:read` - read monitored entities (_currently not available for Platform Tokens_)
|
|
139
|
+
- `environment-api:problems:read` - get problems (_currently not available for Platform Tokens_)
|
|
140
|
+
- `environment-api:metrics:read` - read metrics (_currently not available for Platform Tokens_)
|
|
141
|
+
- `environment-api:slo:read` - read SLOs (_currently not available for Platform Tokens_)
|
|
142
|
+
- `automation:workflows:read` - read Workflows
|
|
143
|
+
- `automation:workflows:write` - create and update Workflows
|
|
144
|
+
- `automation:workflows:run` - run Workflows
|
|
145
|
+
- `storage:buckets:read` - needed for `execute_dql` tool to read all system data stored on Grail
|
|
146
|
+
- `storage:logs:read` - needed for `execute_dql` tool to read logs for reliability guardian validations
|
|
147
|
+
- `storage:metrics:read` - needed for `execute_dql` tool to read metrics for reliability guardian validations
|
|
148
|
+
- `storage:bizevents:read` - needed for `execute_dql` tool to read bizevents for reliability guardian validations
|
|
149
|
+
- `storage:spans:read` - needed for `execute_dql` tool to read spans from Grail
|
|
150
|
+
- `storage:entities:read` - needed for `execute_dql` tool to read Entities from Grail
|
|
151
|
+
- `storage:events:read` - needed for `execute_dql` tool to read Events from Grail
|
|
152
|
+
- `storage:security.events:read`- needed for `execute_dql` tool to read Security Events from Grail
|
|
153
|
+
- `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
|
|
154
|
+
- `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
|
|
155
|
+
- `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
|
|
156
|
+
- `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
|
|
157
|
+
- `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
|
|
158
|
+
- `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
|
|
159
|
+
- `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
|
|
160
|
+
|
|
161
|
+
**Note**: Please ensure that `settings:objects:read` is used, and _not_ the similarly named scope `app-settings:objects:read`.
|
|
146
162
|
|
|
147
163
|
## ✨ Example prompts ✨
|
|
148
164
|
|
|
149
165
|
Use these example prompts as a starting point. Just copy them into your IDE or agent setup, adapt them to your services/stack/architecture,
|
|
150
166
|
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
167
|
|
|
168
|
+
**Write a DQL query from natural language:**
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Show me error rates for the payment service in the last hour
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Explain a DQL query:**
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
What does this DQL do?
|
|
178
|
+
fetch logs | filter dt.source_entity == 'SERVICE-123' | summarize count(), by:{severity} | sort count() desc
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Chat with Davis CoPilot:**
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
How can I investigate slow database queries in Dynatrace?
|
|
185
|
+
```
|
|
186
|
+
|
|
152
187
|
**Find open vulnerabilities on production, setup alert.**
|
|
188
|
+
|
|
153
189
|
```
|
|
154
190
|
I have this code snippet here in my IDE, where I get a dependency vulnerability warning for my code.
|
|
155
191
|
Check if I see any open vulnerability/cve on production.
|
|
156
192
|
Analyze a specific production problem.
|
|
157
193
|
Setup a workflow that sends Slack alerts to the #devops-alerts channel when availability problems occur.
|
|
158
194
|
```
|
|
195
|
+
|
|
159
196
|
**Debug intermittent 503 errors.**
|
|
197
|
+
|
|
160
198
|
```
|
|
161
199
|
Our load balancer is intermittently returning 503 errors during peak traffic.
|
|
162
200
|
Pull all recent problems detected for our front-end services and
|
|
163
201
|
run a query to correlate error rates with service instance health indicators.
|
|
164
202
|
I suspect we have circuit breakers triggering, but need confirmation from the telemetry data.
|
|
165
203
|
```
|
|
204
|
+
|
|
166
205
|
**Correlate memory issue with logs.**
|
|
206
|
+
|
|
167
207
|
```
|
|
168
208
|
There's a problem with high memory usage on one of our hosts.
|
|
169
209
|
Get the problem details and then fetch related logs to help understand
|
|
170
210
|
what's causing the memory spike? Which file in this repo is this related to?
|
|
171
211
|
```
|
|
212
|
+
|
|
172
213
|
**Trace request flow analysis.**
|
|
214
|
+
|
|
173
215
|
```
|
|
174
216
|
Our users are experiencing slow checkout processes.
|
|
175
217
|
Can you execute a DQL query to show me the full request trace for our checkout flow,
|
|
176
218
|
so I can identify which service is causing the bottleneck?
|
|
177
219
|
```
|
|
220
|
+
|
|
178
221
|
**Analyze Kubernetes cluster events.**
|
|
222
|
+
|
|
179
223
|
```
|
|
180
224
|
Our application deployments seem to be failing intermittently.
|
|
181
225
|
Can you fetch recent events from our "production-cluster"
|
|
@@ -194,6 +238,7 @@ In case of any problems, you can troubleshoot SSO/OAuth issues based on our [Dyn
|
|
|
194
238
|
It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`):
|
|
195
239
|
|
|
196
240
|
1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes):
|
|
241
|
+
|
|
197
242
|
```bash
|
|
198
243
|
curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
|
|
199
244
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
|
@@ -204,6 +249,7 @@ curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \
|
|
|
204
249
|
```
|
|
205
250
|
|
|
206
251
|
2. Use `access_token` from the response of the above call as the bearer-token in the next call:
|
|
252
|
+
|
|
207
253
|
```bash
|
|
208
254
|
curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environment \
|
|
209
255
|
-H 'accept: application/json' \
|
|
@@ -211,6 +257,7 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
|
|
|
211
257
|
```
|
|
212
258
|
|
|
213
259
|
3. You should retrieve a result like this:
|
|
260
|
+
|
|
214
261
|
```json
|
|
215
262
|
{
|
|
216
263
|
"environmentId": "abc12345",
|
|
@@ -220,35 +267,32 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
|
|
|
220
267
|
}
|
|
221
268
|
```
|
|
222
269
|
|
|
223
|
-
|
|
224
270
|
### Problem accessing data on Grail
|
|
225
271
|
|
|
226
272
|
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
273
|
|
|
228
|
-
|
|
229
274
|
## Development
|
|
230
275
|
|
|
231
276
|
For local development purposes, you can use VSCode and GitHub Copilot.
|
|
232
277
|
|
|
233
278
|
First, enable Copilot for your Workspace `.vscode/settings.json`:
|
|
279
|
+
|
|
234
280
|
```json
|
|
235
281
|
{
|
|
236
282
|
"github.copilot.enable": {
|
|
237
283
|
"*": true
|
|
238
284
|
}
|
|
239
285
|
}
|
|
240
|
-
|
|
241
286
|
```
|
|
242
287
|
|
|
243
288
|
Second, add the MCP to `.vscode/mcp.json`:
|
|
289
|
+
|
|
244
290
|
```json
|
|
245
291
|
{
|
|
246
292
|
"servers": {
|
|
247
293
|
"my-dynatrace-mcp-server": {
|
|
248
294
|
"command": "node",
|
|
249
|
-
"args": [
|
|
250
|
-
"${workspaceFolder}/dist/index.js"
|
|
251
|
-
],
|
|
295
|
+
"args": ["${workspaceFolder}/dist/index.js"],
|
|
252
296
|
"envFile": "${workspaceFolder}/.env"
|
|
253
297
|
}
|
|
254
298
|
}
|
|
@@ -259,7 +303,7 @@ Third, create a `.env` file in this repository (you can copy from `.env.template
|
|
|
259
303
|
|
|
260
304
|
Last but not least, switch to Agent Mode in CoPilot and reload tools.
|
|
261
305
|
|
|
262
|
-
|
|
263
306
|
## Notes
|
|
307
|
+
|
|
264
308
|
This product is not officially supported by Dynatrace.
|
|
265
309
|
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,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDtHttpClient = 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
|
+
* Create a Dynatrace Http Client (from the http-client SDK) based on the provided authentication credentails
|
|
38
|
+
* @param environmentUrl
|
|
39
|
+
* @param scopes
|
|
40
|
+
* @param clientId
|
|
41
|
+
* @param clientSecret
|
|
42
|
+
* @param dtPlatformToken
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
45
|
+
const createDtHttpClient = async (environmentUrl, scopes, clientId, clientSecret, dtPlatformToken) => {
|
|
46
|
+
if (clientId && clientSecret) {
|
|
47
|
+
// create an OAuth client if clientId and clientSecret are provided
|
|
48
|
+
return createOAuthHttpClient(environmentUrl, scopes, clientId, clientSecret);
|
|
49
|
+
}
|
|
50
|
+
if (dtPlatformToken) {
|
|
51
|
+
// create a simple HTTP client if only the platform token is provided
|
|
52
|
+
return createBearerTokenHttpClient(environmentUrl, dtPlatformToken);
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Failed to create Dynatrace HTTP Client: Please provide either clientId and clientSecret or dtPlatformToken');
|
|
55
|
+
};
|
|
56
|
+
exports.createDtHttpClient = createDtHttpClient;
|
|
57
|
+
/** Creates an HTTP Client based on environmentUrl and a platform token */
|
|
58
|
+
const createBearerTokenHttpClient = async (environmentUrl, dtPlatformToken) => {
|
|
59
|
+
return new http_client_1.PlatformHttpClient({
|
|
60
|
+
baseUrl: environmentUrl,
|
|
61
|
+
defaultHeaders: {
|
|
62
|
+
'Authorization': `Bearer ${dtPlatformToken}`,
|
|
63
|
+
'User-Agent': `dynatrace-mcp-server/v${package_json_1.version} (${process.platform}-${process.arch})`,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
/** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes
|
|
68
|
+
* This uses a client-credentials flow to request a token from the SSO endpoint.
|
|
69
|
+
*/
|
|
70
|
+
const createOAuthHttpClient = async (environmentUrl, scopes, clientId, clientSecret) => {
|
|
71
|
+
if (!clientId) {
|
|
72
|
+
throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"');
|
|
73
|
+
}
|
|
74
|
+
if (!clientSecret) {
|
|
75
|
+
throw new Error('Failed to retrieve OAuth client secret from env "DT_APP_OAUTH_CLIENT_SECRET"');
|
|
76
|
+
}
|
|
77
|
+
if (!environmentUrl) {
|
|
78
|
+
throw new Error('Failed to retrieve environment URL from env "DT_ENVIRONMENT"');
|
|
79
|
+
}
|
|
80
|
+
console.error(`Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`);
|
|
81
|
+
const ssoBaseUrl = await (0, dt_app_1.getSSOUrl)(environmentUrl);
|
|
82
|
+
const ssoAuthUrl = new URL('/sso/oauth2/token', ssoBaseUrl).toString();
|
|
83
|
+
console.error(`Using SSO auth URL: ${ssoAuthUrl}`);
|
|
84
|
+
// try to request a token, just to verify that everything is set up correctly
|
|
85
|
+
const tokenResponse = await requestToken(clientId, clientSecret, ssoAuthUrl, scopes);
|
|
86
|
+
// in case we didn't get a token, or error / error_description / issueId is set, we throw an error
|
|
87
|
+
if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) {
|
|
88
|
+
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.`);
|
|
89
|
+
}
|
|
90
|
+
console.error(`Successfully retrieved token from SSO!`);
|
|
91
|
+
return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token);
|
|
92
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const dynatrace_clients_1 = require("./dynatrace-clients");
|
|
4
|
+
const http_client_1 = require("@dynatrace-sdk/http-client");
|
|
5
|
+
const dt_app_1 = require("dt-app");
|
|
6
|
+
// Mock external dependencies
|
|
7
|
+
jest.mock('@dynatrace-sdk/http-client');
|
|
8
|
+
jest.mock('dt-app');
|
|
9
|
+
jest.mock('../../package.json', () => ({
|
|
10
|
+
version: '1.0.0-test',
|
|
11
|
+
}));
|
|
12
|
+
// Mock fetch globally
|
|
13
|
+
global.fetch = jest.fn();
|
|
14
|
+
const mockPlatformHttpClient = http_client_1.PlatformHttpClient;
|
|
15
|
+
const mockGetSSOUrl = dt_app_1.getSSOUrl;
|
|
16
|
+
const mockFetch = global.fetch;
|
|
17
|
+
describe('dynatrace-clients', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
// Reset console.error mock
|
|
21
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
jest.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
describe('createDtHttpClient', () => {
|
|
27
|
+
const environmentUrl = 'https://test123.apps.dynatrace.com';
|
|
28
|
+
const scopes = ['scope1', 'scope2'];
|
|
29
|
+
describe('with OAuth credentials', () => {
|
|
30
|
+
const clientId = 'test-client-id';
|
|
31
|
+
const clientSecret = 'test-client-secret';
|
|
32
|
+
const platformToken = 'test-platform-token';
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com');
|
|
35
|
+
});
|
|
36
|
+
it('should create OAuth client successfully', async () => {
|
|
37
|
+
const mockTokenResponse = {
|
|
38
|
+
access_token: 'test-access-token',
|
|
39
|
+
token_type: 'Bearer',
|
|
40
|
+
expires_in: 3600,
|
|
41
|
+
scope: 'scope1 scope2',
|
|
42
|
+
};
|
|
43
|
+
mockFetch.mockResolvedValueOnce({
|
|
44
|
+
ok: true,
|
|
45
|
+
json: async () => mockTokenResponse,
|
|
46
|
+
});
|
|
47
|
+
const result = await (0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, clientId, clientSecret);
|
|
48
|
+
expect(mockGetSSOUrl).toHaveBeenCalledWith(environmentUrl);
|
|
49
|
+
expect(mockFetch).toHaveBeenCalledWith('https://sso.dynatrace.com/sso/oauth2/token', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
53
|
+
},
|
|
54
|
+
body: new URLSearchParams({
|
|
55
|
+
grant_type: 'client_credentials',
|
|
56
|
+
client_id: clientId,
|
|
57
|
+
client_secret: clientSecret,
|
|
58
|
+
scope: scopes.join(' '),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
expect(mockPlatformHttpClient).toHaveBeenCalledWith({
|
|
62
|
+
baseUrl: environmentUrl,
|
|
63
|
+
defaultHeaders: {
|
|
64
|
+
'Authorization': 'Bearer test-access-token',
|
|
65
|
+
'User-Agent': 'dynatrace-mcp-server/v1.0.0-test (linux-x64)',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
expect(result).toBeInstanceOf(http_client_1.PlatformHttpClient);
|
|
69
|
+
});
|
|
70
|
+
it('should throw error when clientId, clientSecret and platformToken are missing', async () => {
|
|
71
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, undefined, undefined, undefined)).rejects.toThrow('Failed to create Dynatrace HTTP Client: Please provide either clientId and clientSecret or dtPlatformToken');
|
|
72
|
+
});
|
|
73
|
+
it('should throw error when environmentUrl is missing', async () => {
|
|
74
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)('', scopes, clientId, clientSecret)).rejects.toThrow('Failed to retrieve environment URL from env "DT_ENVIRONMENT"');
|
|
75
|
+
});
|
|
76
|
+
it('should throw error when token request fails with HTTP error', async () => {
|
|
77
|
+
mockFetch.mockResolvedValueOnce({
|
|
78
|
+
ok: false,
|
|
79
|
+
status: 401,
|
|
80
|
+
statusText: 'Unauthorized',
|
|
81
|
+
json: async () => ({
|
|
82
|
+
error: 'invalid_client',
|
|
83
|
+
error_description: 'Invalid client credentials',
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, clientId, clientSecret)).rejects.toThrow('Failed to retrieve OAuth token');
|
|
87
|
+
expect(console.error).toHaveBeenCalledWith('Failed to fetch token: 401 Unauthorized');
|
|
88
|
+
});
|
|
89
|
+
it('should throw error when token response contains error', async () => {
|
|
90
|
+
const mockErrorResponse = {
|
|
91
|
+
error: 'invalid_scope',
|
|
92
|
+
error_description: 'The requested scope is invalid',
|
|
93
|
+
issueId: 'issue-123',
|
|
94
|
+
};
|
|
95
|
+
mockFetch.mockResolvedValueOnce({
|
|
96
|
+
ok: true,
|
|
97
|
+
json: async () => mockErrorResponse,
|
|
98
|
+
});
|
|
99
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, clientId, clientSecret)).rejects.toThrow('Failed to retrieve OAuth token (IssueId: issue-123): invalid_scope - The requested scope is invalid');
|
|
100
|
+
});
|
|
101
|
+
it('should throw error when token response is missing access_token', async () => {
|
|
102
|
+
const mockIncompleteResponse = {
|
|
103
|
+
token_type: 'Bearer',
|
|
104
|
+
expires_in: 3600,
|
|
105
|
+
};
|
|
106
|
+
mockFetch.mockResolvedValueOnce({
|
|
107
|
+
ok: true,
|
|
108
|
+
json: async () => mockIncompleteResponse,
|
|
109
|
+
});
|
|
110
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, clientId, clientSecret)).rejects.toThrow('Failed to retrieve OAuth token');
|
|
111
|
+
});
|
|
112
|
+
it('should log authentication details', async () => {
|
|
113
|
+
const mockTokenResponse = {
|
|
114
|
+
access_token: 'test-access-token',
|
|
115
|
+
token_type: 'Bearer',
|
|
116
|
+
expires_in: 3600,
|
|
117
|
+
};
|
|
118
|
+
mockFetch.mockResolvedValueOnce({
|
|
119
|
+
ok: true,
|
|
120
|
+
json: async () => mockTokenResponse,
|
|
121
|
+
});
|
|
122
|
+
await (0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, clientId, clientSecret);
|
|
123
|
+
expect(console.error).toHaveBeenCalledWith(`Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('with Bearer token', () => {
|
|
127
|
+
const dtPlatformToken = 'test-platform-token';
|
|
128
|
+
it('should create Bearer token client successfully', async () => {
|
|
129
|
+
const result = await (0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes, undefined, undefined, dtPlatformToken);
|
|
130
|
+
expect(mockPlatformHttpClient).toHaveBeenCalledWith({
|
|
131
|
+
baseUrl: environmentUrl,
|
|
132
|
+
defaultHeaders: {
|
|
133
|
+
'Authorization': `Bearer ${dtPlatformToken}`,
|
|
134
|
+
'User-Agent': 'dynatrace-mcp-server/v1.0.0-test (linux-x64)',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
expect(result).toBeInstanceOf(http_client_1.PlatformHttpClient);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('with no authentication', () => {
|
|
141
|
+
it('should throw error when no authentication method is provided', async () => {
|
|
142
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)(environmentUrl, scopes)).rejects.toThrow('Failed to create Dynatrace HTTP Client: Please provide either clientId and clientSecret or dtPlatformToken');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('requestToken function (indirectly tested)', () => {
|
|
147
|
+
it('should handle fetch errors gracefully', async () => {
|
|
148
|
+
mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com');
|
|
149
|
+
// Mock fetch to throw an error
|
|
150
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
151
|
+
await expect((0, dynatrace_clients_1.createDtHttpClient)('https://test.apps.dynatrace.com', ['scope1'], 'client-id', 'client-secret')).rejects.toThrow('Network error');
|
|
152
|
+
});
|
|
153
|
+
it('should format request body correctly', async () => {
|
|
154
|
+
mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com');
|
|
155
|
+
const mockTokenResponse = {
|
|
156
|
+
access_token: 'test-token',
|
|
157
|
+
};
|
|
158
|
+
mockFetch.mockResolvedValueOnce({
|
|
159
|
+
ok: true,
|
|
160
|
+
json: async () => mockTokenResponse,
|
|
161
|
+
});
|
|
162
|
+
await (0, dynatrace_clients_1.createDtHttpClient)('https://test.apps.dynatrace.com', ['scope1', 'scope2'], 'test-client', 'test-secret');
|
|
163
|
+
const expectedBody = new URLSearchParams({
|
|
164
|
+
grant_type: 'client_credentials',
|
|
165
|
+
client_id: 'test-client',
|
|
166
|
+
client_secret: 'test-secret',
|
|
167
|
+
scope: 'scope1 scope2',
|
|
168
|
+
});
|
|
169
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
173
|
+
},
|
|
174
|
+
body: expectedBody,
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('User-Agent header', () => {
|
|
179
|
+
it('should include correct User-Agent format', async () => {
|
|
180
|
+
const dtPlatformToken = 'test-token';
|
|
181
|
+
await (0, dynatrace_clients_1.createDtHttpClient)('https://test.apps.dynatrace.com', ['scope1'], undefined, undefined, dtPlatformToken);
|
|
182
|
+
expect(mockPlatformHttpClient).toHaveBeenCalledWith(expect.objectContaining({
|
|
183
|
+
defaultHeaders: expect.objectContaining({
|
|
184
|
+
'User-Agent': expect.stringMatching(/^dynatrace-mcp-server\/v\d+\.\d+\.\d+(-\w+)? \(\w+-\w+\)$/),
|
|
185
|
+
}),
|
|
186
|
+
}));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -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;
|