@dynatrace-oss/dynatrace-mcp-server 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,17 +25,15 @@ bringing real-time observability data directly into your development workflow.
25
25
 
26
26
  If you need help, please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
27
27
 
28
- ## Quickstart
28
+ https://github.com/user-attachments/assets/25c05db1-8e09-4a7f-add2-ed486ffd4b5a
29
29
 
30
- You can add this MCP server to your MCP Client like VSCode, Claude, Cursor, Amazon Q, Windsurf, ChatGPT, or Github Copilot via the npmjs package `@dynatrace-oss/dynatrace-mcp-server`, and type `stdio`.
31
- You can find more details about the configuration for different AI Assistants, Agents and MCP Clients in the [Configuration section below](#configuration).
30
+ ## Quickstart
32
31
 
33
- Furthermore, you need your Dynatrace environment URL, e.g., `https://abc12345.apps.dynatrace.com`, as well as a [Platform Token](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/platform-tokens), e.g., `dt0s16.SAMPLE.abcd1234`, with [required scopes](#scopes-for-authentication).
32
+ You can add this MCP server to your MCP Client like VSCode, Claude, Cursor, Amazon Q, Windsurf, ChatGPT, or Github Copilot via the command is `npx -y @dynatrace-oss/dynatrace-mcp-server` (type: `stdio`). For more details, please refer to the [configuration section below](#configuration).
34
33
 
35
- Depending on your MCP Client, you need to configure these as environment variables or as settings in the UI:
34
+ Furthermore, you need to configure the URL to a Dynatrace environment:
36
35
 
37
36
  - `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`)
38
- - `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
39
37
 
40
38
  Once you are done, we recommend looking into [example prompts](#-example-prompts-), like `Get all details of the entity 'my-service'` or `Show me error logs`. Please mind that these prompts lead to executing DQL statements which may incur [costs](#costs) in accordance to your licence.
41
39
 
@@ -109,97 +107,6 @@ fetch dt.system.events
109
107
 
110
108
  > **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).
111
109
 
112
- ## 🎯 AI-Powered Observability Workshop Rules
113
-
114
- Enhance your AI assistant with comprehensive Dynatrace observability analysis capabilities through our streamlined workshop rules. These rules provide hierarchical workflows for security, compliance, incident response, and distributed systems investigation.
115
-
116
- ### **🚀 Quick Setup for AI Assistants**
117
-
118
- Copy the comprehensive rule files from the [`dynatrace-agent-rules/rules/`](./dynatrace-agent-rules/rules/) directory to your AI assistant's rules directory:
119
-
120
- **IDE-Specific Locations:**
121
-
122
- - **Amazon Q**: `.amazonq/rules/` (project) or `~/.aws/amazonq/rules/` (global)
123
- - **Cursor**: `.cursor/rules/` (project) or via Settings → Rules (global)
124
- - **Windsurf**: `.windsurfrules/` (project) or via Customizations → Rules (global)
125
- - **Cline**: `.clinerules/` (project) or `~/Documents/Cline/Rules/` (global)
126
- - **GitHub Copilot**: `.github/copilot-instructions.md` (project only)
127
-
128
- Then initialize the agent in your AI chat:
129
-
130
- ```
131
- load dynatrace mcp
132
- ```
133
-
134
- ### **🏗️ Enhanced Analysis Capabilities**
135
-
136
- The workshop rules unlock advanced observability analysis modes:
137
-
138
- #### **🚨 Incident Response & Problem Investigation**
139
-
140
- - **4-phase structured investigation** workflow (Detection → Impact → Root Cause → Resolution)
141
- - **Cross-data source correlation** (problems → logs → spans → metrics)
142
- - **Kubernetes-aware incident analysis** with namespace and pod context
143
- - **User impact assessment** with Davis AI integration
144
-
145
- #### **📊 Comprehensive Data Investigation**
146
-
147
- - **Unified log-service-process analysis** in single workflow
148
- - **Business logic error detection** patterns
149
- - **Deployment correlation analysis** with ArgoCD/GitOps integration
150
- - **Golden signals monitoring** (Rate, Errors, Duration, Saturation)
151
-
152
- #### **🔗 Advanced Transaction Analysis**
153
-
154
- - **Precise root cause identification** with file/line numbers
155
- - **Exception stack trace analysis** with business context
156
- - **Multi-service cascade failure analysis**
157
- - **Performance impact correlation** across distributed systems
158
-
159
- #### **🛡️ Enhanced Security & Compliance**
160
-
161
- - **Latest-scan analysis** prevents outdated data aggregation
162
- - **Multi-cloud compliance** (AWS, Azure, GCP, Kubernetes)
163
- - **Evidence-based investigation** with detailed remediation paths
164
- - **Risk-based scoring** with team-specific guidance
165
-
166
- #### **⚡ DevOps Automation & SRE**
167
-
168
- - **Deployment health gates** with automated promotion/rollback
169
- - **SLO/SLI automation** with error budget calculations
170
- - **Infrastructure as Code remediation** with auto-generated templates
171
- - **Alert optimization workflows** with pattern recognition
172
-
173
- ### **📁 Hierarchical Rule Architecture**
174
-
175
- The rules are organized in a context-window optimized structure:
176
-
177
- ```
178
- rules/
179
- ├── DynatraceMcpIntegration.md # 🎯 MAIN ORCHESTRATOR
180
- ├── workflows/ # 🔧 ANALYSIS WORKFLOWS
181
- │ ├── incidentResponse.md # Core incident investigation
182
- │ ├── DynatraceSecurityCompliance.md # Security & compliance analysis
183
- │ ├── DynatraceDevOpsIntegration.md # CI/CD automation
184
- │ └── dataSourceGuides/ # 📊 DATA ANALYSIS GUIDES
185
- │ ├── dataInvestigation.md # Logs, services, processes
186
- │ └── DynatraceSpanAnalysis.md # Transaction tracing
187
- └── reference/ # 📚 TECHNICAL DOCUMENTATION
188
- ├── DynatraceQueryLanguage.md # DQL syntax foundation
189
- ├── DynatraceExplore.md # Field discovery patterns
190
- ├── DynatraceSecurityEvents.md # Security events schema
191
- └── DynatraceProblemsSpec.md # Problems schema reference
192
- ```
193
-
194
- **Key Architectural Benefits:**
195
-
196
- - **All files under 6,500 tokens** - Compatible with most LLM context limits
197
- - **Hierarchical organization** - Clear entry points and specialized guides
198
- - **Eliminated circular references** - No more confusing cross-referencing webs
199
- - **DQL-first approach** - Prefer flexible queries over rigid MCP calls
200
-
201
- For detailed information about the workshop rules, see the [Rules README](./dynatrace-agent-rules/rules/README.md).
202
-
203
110
  ## Configuration
204
111
 
205
112
  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`.
@@ -231,7 +138,6 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
231
138
  "command": "npx",
232
139
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
233
140
  "env": {
234
- "DT_PLATFORM_TOKEN": "",
235
141
  "DT_ENVIRONMENT": ""
236
142
  }
237
143
  }
@@ -248,7 +154,6 @@ This only works if the config is stored in the current workspaces, e.g., `<your-
248
154
  "command": "npx",
249
155
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
250
156
  "env": {
251
- "DT_PLATFORM_TOKEN": "",
252
157
  "DT_ENVIRONMENT": ""
253
158
  }
254
159
  }
@@ -267,7 +172,6 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
267
172
  "command": "npx",
268
173
  "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
269
174
  "env": {
270
- "DT_PLATFORM_TOKEN": "",
271
175
  "DT_ENVIRONMENT": ""
272
176
  }
273
177
  }
@@ -285,7 +189,7 @@ Using `gemini` CLI directly (recommended):
285
189
 
286
190
  ```bash
287
191
  gemini extensions install https://github.com/dynatrace-oss/dynatrace-mcp
288
- export DT_PLATFORM_TOKEN=...
192
+ export DT_PLATFORM_TOKEN=... # optional
289
193
  export DT_ENVIRONMENT=https://...
290
194
  ```
291
195
 
@@ -304,7 +208,6 @@ Or manually in your `~/.gemini/settings.json` or `.gemini/settings.json`:
304
208
  "command": "npx",
305
209
  "args": ["@dynatrace-oss/dynatrace-mcp-server@latest"],
306
210
  "env": {
307
- "DT_PLATFORM_TOKEN": "",
308
211
  "DT_ENVIRONMENT": ""
309
212
  },
310
213
  "timeout": 30000,
@@ -379,17 +282,15 @@ For fetching just error-logs, add `| filter loglevel == "ERROR"`.
379
282
 
380
283
  ## Environment Variables
381
284
 
382
- You can set up authentication via **Platform Tokens** (recommended) or **OAuth Client** via the following environment variables:
383
-
384
- - `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`)
385
- - `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
386
- - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Alternative: Dynatrace OAuth Client ID (for advanced use cases)
387
- - `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Alternative: Dynatrace OAuth Client Secret (for advanced use cases)
388
- - `DT_GRAIL_QUERY_BUDGET_GB` (number, default: `1000`) - Budget limit in GB (base 1000) for Grail query bytes scanned per session. The MCP server tracks your Grail usage and warns when approaching or exceeding this limit.
285
+ - `DT_ENVIRONMENT` (**required**, string, e.g., `https://abc12345.apps.dynatrace.com`) - URL to your Dynatrace Platform (do not use Dynatrace classic URLs like `abc12345.live.dynatrace.com`)
286
+ - `DT_PLATFORM_TOKEN` (optional, string, e.g., `dt0s16.SAMPLE.abcd1234`) - Dynatrace Platform Token
287
+ - `OAUTH_CLIENT_ID` (optional, string, e.g., `dt0s02.SAMPLE`) - Alternative: Dynatrace OAuth Client ID (for advanced use cases)
288
+ - `OAUTH_CLIENT_SECRET` (optional, string, e.g., `dt0s02.SAMPLE.abcd1234`) - Alternative: Dynatrace OAuth Client Secret (for advanced use cases)
289
+ - `DT_GRAIL_QUERY_BUDGET_GB` (optional, number, default: `1000`) - Budget limit in GB (base 1000) for Grail query bytes scanned per session. The MCP server tracks your Grail usage and warns when approaching or exceeding this limit.
389
290
 
390
- **Platform Tokens are recommended** for most use cases as they provide a simpler authentication flow. OAuth Clients should only be used when specific OAuth features are required.
291
+ When just providing `DT_ENVIRONMENT`, the local MCP server will try to open a browser window to authenticate against the Dynatrace SSO.
391
292
 
392
- For more information, please have a look at the documentation about
293
+ For more information about the other authentication methods, please have a look at the documentation about
393
294
  [creating a Platform Token in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/platform-tokens), as well as
394
295
  [creating an OAuth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients) for advanced scenarios.
395
296
 
@@ -4,37 +4,13 @@ exports.createDtHttpClient = void 0;
4
4
  const http_client_1 = require("@dynatrace-sdk/http-client");
5
5
  const dt_app_1 = require("dt-app");
6
6
  const user_agent_1 = require("../utils/user-agent");
7
+ const dynatrace_oauth_auth_code_flow_1 = require("./dynatrace-oauth-auth-code-flow");
8
+ const token_cache_1 = require("./token-cache");
9
+ const utils_1 = require("./utils");
10
+ const dynatrace_oauth_client_credentials_1 = require("./dynatrace-oauth-client-credentials");
7
11
  /**
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 Response of the OAuth Endpoint (which, in the best case includes a token)
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
12
+ * Create a Dynatrace Http Client (from the http-client SDK) based on the provided authentication credentials
13
+ * Supports Platform Token, OAuth Client Credentials Flow, and OAuth Authorization Code Flow (interactive)
38
14
  * @param environmentUrl
39
15
  * @param scopes
40
16
  * @param clientId
@@ -43,50 +19,128 @@ const requestToken = async (clientId, clientSecret, ssoAuthUrl, scopes) => {
43
19
  * @returns an authenticated HttpClient
44
20
  */
45
21
  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
- }
22
+ /** Logic:
23
+ * * if a platform token is provided, use it
24
+ * * If no platform token is provided, but clientId and clientSecret are provided, use client credentials flow
25
+ * * If no platform token is provided, and no clientSecret is provided, but a clientId is provided, use OAuth authorization code flow (interactive)
26
+ * * If neither platform token nor OAuth credentials are provided, throw an error
27
+ */
50
28
  if (dtPlatformToken) {
51
29
  // create a simple HTTP client if only the platform token is provided
52
- return createBearerTokenHttpClient(environmentUrl, dtPlatformToken);
30
+ return createPlatformTokenHttpClient(environmentUrl, dtPlatformToken);
53
31
  }
54
- throw new Error('Failed to create Dynatrace HTTP Client: Please provide either clientId and clientSecret or dtPlatformToken');
32
+ else if (clientId && clientSecret) {
33
+ // create an Oauth client using client credentials flow (non-interactive)
34
+ return createOAuthClientCredentialsHttpClient(environmentUrl, scopes, clientId, clientSecret);
35
+ }
36
+ else if (clientId) {
37
+ // create an OAuth client using authorization code flow (interactive)
38
+ return createOAuthAuthCodeFlowHttpClient(environmentUrl, scopes, clientId);
39
+ }
40
+ throw new Error('Failed to create Dynatrace HTTP Client: Please provide either clientId and clientSecret for client credentials flow, clientId only for interactive OAuth flow, or just a platform token.');
55
41
  };
56
42
  exports.createDtHttpClient = createDtHttpClient;
57
- /** Creates an HTTP Client based on environmentUrl and a platform token */
58
- const createBearerTokenHttpClient = async (environmentUrl, dtPlatformToken) => {
43
+ /**
44
+ * Creates an HTTP Client based on environmentUrl and a bearer token, and also sets the user agent
45
+ */
46
+ const createBearerTokenHttpClient = async (environmentUrl, bearerToken) => {
59
47
  return new http_client_1.PlatformHttpClient({
60
48
  baseUrl: environmentUrl,
61
49
  defaultHeaders: {
62
- 'Authorization': `Bearer ${dtPlatformToken}`,
50
+ 'Authorization': `Bearer ${bearerToken}`,
63
51
  'User-Agent': (0, user_agent_1.getUserAgent)(),
64
52
  },
65
53
  });
66
54
  };
67
- /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes
55
+ /**
56
+ * Creates an HTTP Client based on environmentUrl and a platform token (as bearer token)
57
+ */
58
+ const createPlatformTokenHttpClient = async (environmentUrl, dtPlatformToken) => {
59
+ console.error(`🔒 Using Platform Token to authenticate API Calls to ${environmentUrl}`);
60
+ return createBearerTokenHttpClient(environmentUrl, dtPlatformToken);
61
+ };
62
+ /**
63
+ * Create an OAuth Client based on clientId, clientSecret, environmentUrl and scopes
68
64
  * This uses a client-credentials flow to request a token from the SSO endpoint.
65
+ * Note: We do not refresh the token here, we always request a new one on each client creation.
69
66
  */
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}`);
67
+ const createOAuthClientCredentialsHttpClient = async (environmentUrl, scopes, clientId, clientSecret) => {
68
+ console.error(`🔒 Client-Creds-Flow: Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`);
69
+ // Get SSO Base URL
70
+ const ssoBaseURL = await (0, dt_app_1.getSSOUrl)(environmentUrl);
84
71
  // try to request a token, just to verify that everything is set up correctly
85
- const tokenResponse = await requestToken(clientId, clientSecret, ssoAuthUrl, scopes);
72
+ const tokenResponse = await (0, dynatrace_oauth_client_credentials_1.requestTokenForClientCredentials)(clientId, clientSecret, ssoBaseURL, scopes);
86
73
  // in case we didn't get a token, or error / error_description / issueId is set, we throw an error
87
74
  if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) {
88
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 and/or is missing scopes.`);
89
76
  }
90
- console.error(`Successfully retrieved token from SSO!`);
77
+ console.error(`Successfully retrieved token from SSO! Token valid for ${tokenResponse.expires_in}s with scopes: ${tokenResponse.scope}`);
78
+ // now that we have the access token, we can just use a plain bearer token client
79
+ return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token);
80
+ };
81
+ /** Create an OAuth Client using authorization code flow (interactive authentication)
82
+ * This starts a local HTTP server to handle the OAuth redirect and requires user interaction.
83
+ * Implements token caching (via .dt-mcp/token.json) to avoid repeated OAuth flows.
84
+ * Note: Always requests a complete set of scopes for maximum token reusability. Else the user will end up having to approve multiple requests.
85
+ */
86
+ const createOAuthAuthCodeFlowHttpClient = async (environmentUrl, scopes, clientId) => {
87
+ // Get SSO Base URL
88
+ const ssoBaseURL = await (0, dt_app_1.getSSOUrl)(environmentUrl);
89
+ // Fast Track: Fetch cached token and check if it is still valid
90
+ const cachedToken = token_cache_1.globalTokenCache.getToken(scopes);
91
+ const isValid = token_cache_1.globalTokenCache.isTokenValid(scopes);
92
+ // If we have a valid cached token, we can use it
93
+ if (isValid && cachedToken) {
94
+ const expiresIn = cachedToken.expires_at ? Math.round((cachedToken.expires_at - Date.now()) / 1000) : 'never';
95
+ console.error(`✅ Auth-Code-Flow: Using cached access token (expires in ${expiresIn}s)`);
96
+ // just use the cached token as a bearer token
97
+ return createBearerTokenHttpClient(environmentUrl, cachedToken.access_token);
98
+ }
99
+ // If we have an expired token that can be refreshed, refresh it
100
+ if (cachedToken && cachedToken.refresh_token && !isValid) {
101
+ const expiresIn = cachedToken.expires_at ? Math.round((cachedToken.expires_at - Date.now()) / 1000) : 'never';
102
+ console.error(`🔍 Auth-Code-Flow: Found expired cached token (expires in ${expiresIn}s), attempting refresh...`);
103
+ try {
104
+ console.error(`🔄 Attempting to refresh expired access token...`);
105
+ const tokenResponse = await (0, dynatrace_oauth_auth_code_flow_1.refreshAccessToken)(ssoBaseURL, clientId, cachedToken.refresh_token, scopes);
106
+ if (tokenResponse.access_token && !tokenResponse.error) {
107
+ console.error(`✅ Successfully refreshed access token!`);
108
+ // Update the cache with the new token
109
+ token_cache_1.globalTokenCache.setToken(scopes, tokenResponse);
110
+ // now use the updated token as a bearer token
111
+ return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token);
112
+ }
113
+ else {
114
+ console.error(`❌ Token refresh failed: ${tokenResponse.error} - ${tokenResponse.error_description}`);
115
+ // Clear the invalid token from cache
116
+ token_cache_1.globalTokenCache.clearToken();
117
+ }
118
+ }
119
+ catch (error) {
120
+ console.error(`❌ Token refresh failed with error: ${error instanceof Error ? error.message : String(error)}`);
121
+ // Clear the invalid token from cache
122
+ token_cache_1.globalTokenCache.clearToken();
123
+ }
124
+ }
125
+ // If we get here, we are currently not authenticated, and need to perform a full OAuth Authorization Code Flow
126
+ console.error(`🚀 Auth-Code-Flow: No valid cached token found, initiating OAuth Authorization Code Flow...`);
127
+ console.error(`Using SSO base URL ${ssoBaseURL}`);
128
+ // Randomly select a port for the OAuth redirect URL (e.g., 5344)
129
+ const port = (0, utils_1.getRandomPort)();
130
+ // Perform the OAuth authorization code flow with all scopes
131
+ const tokenResponse = await (0, dynatrace_oauth_auth_code_flow_1.performOAuthAuthorizationCodeFlow)(ssoBaseURL, {
132
+ clientId,
133
+ // redirectUri will be used as a redirect/callback from the authorization code flow
134
+ redirectUri: `http://localhost:${port}/auth/login`,
135
+ scopes: scopes, // Request all scopes upfront
136
+ }, port);
137
+ // Check if we got a valid token
138
+ if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) {
139
+ throw new Error(`Failed to retrieve OAuth token via authorization code flow (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}`);
140
+ }
141
+ // Cache the new token with all scopes
142
+ token_cache_1.globalTokenCache.setToken(scopes, tokenResponse);
143
+ console.error(`✅ Successfully retrieved token from SSO! Token cached for future use with scopes: ${scopes.join(', ')}`);
144
+ // now that we have the access token, we can just use a plain bearer token client
91
145
  return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token);
92
146
  };
@@ -3,9 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const dynatrace_clients_1 = require("./dynatrace-clients");
4
4
  const http_client_1 = require("@dynatrace-sdk/http-client");
5
5
  const dt_app_1 = require("dt-app");
6
+ const dynatrace_oauth_auth_code_flow_1 = require("./dynatrace-oauth-auth-code-flow");
7
+ const token_cache_1 = require("./token-cache");
6
8
  // Mock external dependencies
7
9
  jest.mock('@dynatrace-sdk/http-client');
8
10
  jest.mock('dt-app');
11
+ jest.mock('./dynatrace-oauth-auth-code-flow');
12
+ jest.mock('./token-cache');
9
13
  jest.mock('../../package.json', () => ({
10
14
  version: '1.0.0-test',
11
15
  }));
@@ -13,12 +17,21 @@ jest.mock('../../package.json', () => ({
13
17
  global.fetch = jest.fn();
14
18
  const mockPlatformHttpClient = http_client_1.PlatformHttpClient;
15
19
  const mockGetSSOUrl = dt_app_1.getSSOUrl;
20
+ const mockPerformOAuthAuthorizationCodeFlow = dynatrace_oauth_auth_code_flow_1.performOAuthAuthorizationCodeFlow;
21
+ const mockGlobalTokenCache = token_cache_1.globalTokenCache;
16
22
  const mockFetch = global.fetch;
17
23
  describe('dynatrace-clients', () => {
18
24
  beforeEach(() => {
19
25
  jest.clearAllMocks();
20
26
  // Reset console.error mock
21
27
  jest.spyOn(console, 'error').mockImplementation(() => { });
28
+ // Mock token cache methods
29
+ mockGlobalTokenCache.getToken.mockReturnValue(null);
30
+ mockGlobalTokenCache.isTokenValid.mockReturnValue(false);
31
+ mockGlobalTokenCache.setToken.mockImplementation(() => { });
32
+ mockGlobalTokenCache.clearToken.mockImplementation(() => { });
33
+ // Mock getSSOUrl
34
+ mockGetSSOUrl.mockResolvedValue('https://sso.dynatrace.com');
22
35
  });
23
36
  afterEach(() => {
24
37
  jest.restoreAllMocks();
@@ -67,12 +80,6 @@ describe('dynatrace-clients', () => {
67
80
  });
68
81
  expect(result).toBeInstanceOf(http_client_1.PlatformHttpClient);
69
82
  });
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
83
  it('should throw error when token request fails with HTTP error', async () => {
77
84
  mockFetch.mockResolvedValueOnce({
78
85
  ok: false,
@@ -120,7 +127,7 @@ describe('dynatrace-clients', () => {
120
127
  json: async () => mockTokenResponse,
121
128
  });
122
129
  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(', ')}`);
130
+ expect(console.error).toHaveBeenCalledWith(`🔒 Client-Creds-Flow: Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId} with the following scopes: ${scopes.join(', ')}`);
124
131
  });
125
132
  });
126
133
  describe('with Bearer token', () => {
@@ -137,11 +144,6 @@ describe('dynatrace-clients', () => {
137
144
  expect(result).toBeInstanceOf(http_client_1.PlatformHttpClient);
138
145
  });
139
146
  });
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
147
  });
146
148
  describe('requestToken function (indirectly tested)', () => {
147
149
  it('should handle fetch errors gracefully', async () => {