@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 +12 -111
- package/dist/authentication/dynatrace-clients.js +110 -56
- package/dist/authentication/dynatrace-clients.test.js +14 -12
- package/dist/authentication/dynatrace-oauth-auth-code-flow.js +220 -0
- package/dist/authentication/dynatrace-oauth-auth-code-flow.test.js +50 -0
- package/dist/authentication/dynatrace-oauth-base.js +28 -0
- package/dist/authentication/dynatrace-oauth-client-credentials.js +21 -0
- package/dist/authentication/token-cache.js +149 -0
- package/dist/authentication/utils.js +26 -0
- package/dist/capabilities/get-events-for-cluster.js +15 -5
- package/dist/capabilities/update-workflow.js +2 -2
- package/dist/getDynatraceEnv.js +2 -3
- package/dist/getDynatraceEnv.test.js +6 -2
- package/dist/index.js +110 -25
- package/package.json +2 -1
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
|
-
|
|
28
|
+
https://github.com/user-attachments/assets/25c05db1-8e09-4a7f-add2-ed486ffd4b5a
|
|
29
29
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
- `
|
|
385
|
-
- `
|
|
386
|
-
- `
|
|
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
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
30
|
+
return createPlatformTokenHttpClient(environmentUrl, dtPlatformToken);
|
|
53
31
|
}
|
|
54
|
-
|
|
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
|
-
/**
|
|
58
|
-
|
|
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 ${
|
|
50
|
+
'Authorization': `Bearer ${bearerToken}`,
|
|
63
51
|
'User-Agent': (0, user_agent_1.getUserAgent)(),
|
|
64
52
|
},
|
|
65
53
|
});
|
|
66
54
|
};
|
|
67
|
-
/**
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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(
|
|
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 () => {
|