@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 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, fetch production-level data for early detection.
11
- - Fix issues in the context from monitored exceptions, logs, and anomalies.
12
- - More context on security level issues
13
- - Natural language to query log data
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
- ## Quickstart
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
- **Work in progress**
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
- A **Dynatrace OAuth Client** is needed to communicate with your Dynatrace Environment. Please follow the documentation about
110
- [creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients),
111
- and set up the following environment variables in order for this MCP to work:
112
-
113
- * `DT_ENVIRONMENT` (string, e.g., https://abc12345.apps.dynatrace.com) - URL to your Dynatrace Platform (do not use Dynatrace classic URLs like `abc12345.live.dynatrace.com`)
114
- * `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
115
- * `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret
116
- * OAuth Client Scopes:
117
- * `app-engine:apps:run` - needed for environmentInformationClient
118
- * `app-engine:functions:run` - needed for environmentInformationClient
119
- * `hub:catalog:read` - get details about installed Apps on Dynatrace Environment
120
- * `environment-api:security-problems:read` - needed for reading security problems
121
- * `environment-api:entities:read` - read monitored entities
122
- * `environment-api:problems:read` - get problems
123
- * `environment-api:metrics:read` - read metrics
124
- * `environment-api:slo:read` - read SLOs
125
- * `storage:buckets:read` - Read all system data stored on Grail
126
- * `storage:logs:read` - Read logs for reliability guardian validations
127
- * `storage:metrics:read` - Read metrics for reliability guardian validations
128
- * `storage:bizevents:read` - Read bizevents for reliability guardian validations
129
- * `storage:spans:read` - Read spans from Grail
130
- * `storage:entities:read` - Read Entities from Grail
131
- * `storage:events:read` - Read Events from Grail
132
- * `storage:system:read` - Read System Data from Grail
133
- * `storage:user.events:read` - Read User events from Grail
134
- * `storage:user.sessions:read` - Read User sessions from Grail
135
- * `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
136
-
137
- **Note**: Please ensure that `settings:objects:read` is used, and *not* the similarly named scope `app-settings:objects:read`.
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
- * `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
142
- * `USE_APP_SETTINGS` (boolean, `true` or `false`; default: `false`)
143
- * Requires scope `app-settings:objects:read` to read settings-objects from app settings
144
- * `USE_WORKFLOWS` (boolean, `true` or `false`; default: `false`)
145
- * Requires scopes `automation:workflows:read`, `automation:workflows:write` and `automation:workflows:run` to read, write and execute Workflows
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,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;