@dynatrace-oss/dynatrace-mcp-server 0.4.0 → 0.5.0-rc.2

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,45 +113,76 @@ 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:
116
+ You can set up authentication via **OAuth Client** or **Platform Tokens** (v0.5.0 and newer) via the following environment variables:
112
117
 
113
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`)
114
119
  - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID
115
120
  - `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`.
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
129
  - `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
142
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:metrics:read` - read metrics (_currently not available for Platform Tokens_)
140
+ - `environment-api:slo:read` - read SLOs (_currently not available for Platform Tokens_)
141
+ - `automation:workflows:read` - read Workflows
142
+ - `automation:workflows:write` - create and update Workflows
143
+ - `automation:workflows:run` - run Workflows
144
+ - `storage:buckets:read` - needed for `execute_dql` tool to read all system data stored on Grail
145
+ - `storage:logs:read` - needed for `execute_dql` tool to read logs for reliability guardian validations
146
+ - `storage:metrics:read` - needed for `execute_dql` tool to read metrics for reliability guardian validations
147
+ - `storage:bizevents:read` - needed for `execute_dql` tool to read bizevents for reliability guardian validations
148
+ - `storage:spans:read` - needed for `execute_dql` tool to read spans from Grail
149
+ - `storage:entities:read` - needed for `execute_dql` tool to read Entities from Grail
150
+ - `storage:events:read` - needed for `execute_dql` tool to read Events from Grail
151
+ - `storage:security.events:read`- needed for `execute_dql` tool to read Security Events from Grail
152
+ - `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
153
+ - `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
154
+ - `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
155
+ - `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
156
+ - `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
157
+ - `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
158
+ - `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
159
+
160
+ **Note**: Please ensure that `settings:objects:read` is used, and _not_ the similarly named scope `app-settings:objects:read`.
161
+
143
162
  ## ✨ Example prompts ✨
144
163
 
145
164
  Use these example prompts as a starting point. Just copy them into your IDE or agent setup, adapt them to your services/stack/architecture,
146
165
  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.
147
166
 
167
+ **Write a DQL query from natural language:**
168
+
169
+ ```
170
+ Show me error rates for the payment service in the last hour
171
+ ```
172
+
173
+ **Explain a DQL query:**
174
+
175
+ ```
176
+ What does this DQL do?
177
+ fetch logs | filter dt.source_entity == 'SERVICE-123' | summarize count(), by:{severity} | sort count() desc
178
+ ```
179
+
180
+ **Chat with Davis CoPilot:**
181
+
182
+ ```
183
+ How can I investigate slow database queries in Dynatrace?
184
+ ```
185
+
148
186
  **Find open vulnerabilities on production, setup alert.**
149
187
 
150
188
  ```
@@ -246,6 +284,8 @@ First, enable Copilot for your Workspace `.vscode/settings.json`:
246
284
  }
247
285
  ```
248
286
 
287
+ and make sure that you are using Agent Mode in CoPilot.
288
+
249
289
  Second, add the MCP to `.vscode/mcp.json`:
250
290
 
251
291
  ```json
@@ -253,7 +293,7 @@ Second, add the MCP to `.vscode/mcp.json`:
253
293
  "servers": {
254
294
  "my-dynatrace-mcp-server": {
255
295
  "command": "node",
256
- "args": ["${workspaceFolder}/dist/index.js"],
296
+ "args": ["--watch", "${workspaceFolder}/dist/index.js"],
257
297
  "envFile": "${workspaceFolder}/.env"
258
298
  }
259
299
  }
@@ -262,7 +302,7 @@ Second, add the MCP to `.vscode/mcp.json`:
262
302
 
263
303
  Third, create a `.env` file in this repository (you can copy from `.env.template`) and configure environment variables as [described above](#environment-variables).
264
304
 
265
- Last but not least, switch to Agent Mode in CoPilot and reload tools.
305
+ Finally, make changes to your code and compile it with `npm run build` or just run `npm run watch` and it auto-compiles.
266
306
 
267
307
  ## Notes
268
308
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createOAuthClient = exports.ExtendedOauthClient = void 0;
3
+ 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 package_json_1 = require("../../package.json");
@@ -34,29 +34,40 @@ const requestToken = async (clientId, clientSecret, ssoAuthUrl, scopes) => {
34
34
  return await res.json();
35
35
  };
36
36
  /**
37
- * ExtendedOAuthClient that takes parameters for clientId, secret, scopes, environmentUrl, authUrl, and the version of the dynatrace-mcp-server
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
38
44
  */
39
- class ExtendedOauthClient extends http_client_1._OAuthHttpClient {
40
- userAgent;
41
- constructor(config, userAgent) {
42
- super(config);
43
- this.userAgent = userAgent;
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);
44
49
  }
45
- send(options) {
46
- // add the user-agent header to the request
47
- options.headers = {
48
- ...options.headers,
49
- 'User-Agent': this.userAgent,
50
- };
51
- // call the parent send method
52
- return super.send(options);
50
+ if (dtPlatformToken) {
51
+ // create a simple HTTP client if only the platform token is provided
52
+ return createBearerTokenHttpClient(environmentUrl, dtPlatformToken);
53
53
  }
54
- }
55
- exports.ExtendedOauthClient = ExtendedOauthClient;
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
+ };
56
67
  /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes
57
68
  * This uses a client-credentials flow to request a token from the SSO endpoint.
58
69
  */
59
- const createOAuthClient = async (clientId, clientSecret, environmentUrl, scopes) => {
70
+ const createOAuthHttpClient = async (environmentUrl, scopes, clientId, clientSecret) => {
60
71
  if (!clientId) {
61
72
  throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"');
62
73
  }
@@ -77,13 +88,5 @@ const createOAuthClient = async (clientId, clientSecret, environmentUrl, scopes)
77
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.`);
78
89
  }
79
90
  console.error(`Successfully retrieved token from SSO!`);
80
- const userAgent = `dynatrace-mcp-server/v${package_json_1.version} (${process.platform}-${process.arch})`;
81
- return new ExtendedOauthClient({
82
- scopes,
83
- clientId,
84
- secret: clientSecret,
85
- environmentUrl,
86
- authUrl: ssoAuthUrl,
87
- }, userAgent);
91
+ return createBearerTokenHttpClient(environmentUrl, tokenResponse.access_token);
88
92
  };
89
- exports.createOAuthClient = createOAuthClient;
@@ -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,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.generateDqlFromNaturalLanguage = void 0;
4
+ // API Functions
5
+ /**
6
+ * Generate DQL from natural language
7
+ * Converts plain English descriptions into powerful Dynatrace Query Language (DQL) statements.
8
+ * DQL is the most powerful way to query any data in Dynatrace, including problem events,
9
+ * security issues, logs, metrics, spans, and custom data.
10
+ */
11
+ const generateDqlFromNaturalLanguage = async (dtClient, text) => {
12
+ const request = { text };
13
+ const response = await dtClient.send({
14
+ method: 'POST',
15
+ url: '/platform/davis/copilot/v0.2/skills/nl2dql:generate',
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ 'Accept': 'application/json',
19
+ },
20
+ body: JSON.stringify(request),
21
+ });
22
+ return await response.body('json');
23
+ };
24
+ exports.generateDqlFromNaturalLanguage = generateDqlFromNaturalLanguage;
25
+ /**
26
+ * Explain DQL in natural language
27
+ * Provides plain English explanations of complex DQL queries.
28
+ * Helps users understand what powerful DQL statements do, including
29
+ * queries for problem events, security issues, and performance metrics.
30
+ */
31
+ const explainDqlInNaturalLanguage = async (dtClient, dql) => {
32
+ const request = { dql };
33
+ const response = await dtClient.send({
34
+ method: 'POST',
35
+ url: '/platform/davis/copilot/v0.2/skills/dql2nl:explain',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'Accept': 'application/json',
39
+ },
40
+ body: request, // Not sure why this does not need JSON.stringify, but it only works like this; once we have the SDK, this will be consistent
41
+ });
42
+ return await response.body('json');
43
+ };
44
+ exports.explainDqlInNaturalLanguage = explainDqlInNaturalLanguage;
45
+ const chatWithDavisCopilot = async (dtClient, text, context, annotations, state) => {
46
+ const request = {
47
+ text,
48
+ context,
49
+ annotations,
50
+ state,
51
+ };
52
+ const response = await dtClient.send({
53
+ method: 'POST',
54
+ url: '/platform/davis/copilot/v0.2/skills/conversations:message',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'Accept': 'application/json',
58
+ },
59
+ body: JSON.stringify(request),
60
+ });
61
+ return await response.body('json');
62
+ };
63
+ exports.chatWithDavisCopilot = chatWithDavisCopilot;
@@ -12,13 +12,17 @@ const verifyDqlStatement = async (dtClient, dqlStatement) => {
12
12
  return response;
13
13
  };
14
14
  exports.verifyDqlStatement = verifyDqlStatement;
15
- const executeDql = async (dtClient, dqlStatement) => {
15
+ /**
16
+ * Execute a DQL statement against the Dynatrace API.
17
+ * If the result is immediately available, it will be returned.
18
+ * If the result is not immediately available, it will poll for the result until it is available.
19
+ * @param dtClient
20
+ * @param body - Contains the DQL statement to execute, and optional parameters like maxResultRecords and maxResultBytes
21
+ * @returns the result without metadata and without notifications, or undefined if the query failed or no result was returned.
22
+ */
23
+ const executeDql = async (dtClient, body) => {
16
24
  const queryExecutionClient = new client_query_1.QueryExecutionClient(dtClient);
17
- const response = await queryExecutionClient.queryExecute({
18
- body: {
19
- query: dqlStatement,
20
- },
21
- });
25
+ const response = await queryExecutionClient.queryExecute({ body });
22
26
  if (response.result) {
23
27
  // return response result immediately
24
28
  return response.result.records;
@@ -8,7 +8,7 @@ const findMonitoredEntityByName = async (dtClient, entityName) => {
8
8
  | append [fetch dt.entity.host | search "*${entityName}*" | fieldsAdd entity.type]
9
9
  | append [fetch dt.entity.process_group | search "*${entityName}*" | fieldsAdd entity.type]
10
10
  | append [fetch dt.entity.cloud_application | search "*${entityName}*" | fieldsAdd entity.type]`;
11
- const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, dql);
11
+ const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
12
12
  if (dqlResponse && dqlResponse.length > 0) {
13
13
  let resp = 'The following monitored entities were found:\n';
14
14
  // iterate over dqlResponse and create a string with the entity names
@@ -8,6 +8,6 @@ const getEventsForCluster = async (dtClient, clusterId) => {
8
8
  // if no clusterId is provided, we need to fetch all events
9
9
  dql = `fetch events | filter isNotNull(k8s.cluster.uid)`;
10
10
  }
11
- return (0, execute_dql_1.executeDql)(dtClient, dql);
11
+ return (0, execute_dql_1.executeDql)(dtClient, { query: dql });
12
12
  };
13
13
  exports.getEventsForCluster = getEventsForCluster;
@@ -4,6 +4,6 @@ exports.getLogsForEntity = void 0;
4
4
  const execute_dql_1 = require("./execute-dql");
5
5
  const getLogsForEntity = async (dtClient, entityId) => {
6
6
  const dql = `fetch logs | filter dt.source_entity == "${entityId}"`;
7
- return (0, execute_dql_1.executeDql)(dtClient, dql);
7
+ return (0, execute_dql_1.executeDql)(dtClient, { query: dql });
8
8
  };
9
9
  exports.getLogsForEntity = getLogsForEntity;
@@ -1,15 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.listProblems = void 0;
4
- const client_classic_environment_v2_1 = require("@dynatrace-sdk/client-classic-environment-v2");
5
- const listProblems = async (dtClient) => {
6
- const problemsClient = new client_classic_environment_v2_1.ProblemsClient(dtClient);
7
- const securityProblems = await problemsClient.getProblems({
8
- pageSize: 100,
9
- });
10
- const problems = securityProblems.problems?.map((problem) => {
11
- return `${problem.displayId} (please refer to this problem with \`problemId\` ${problem.problemId}): ${problem.title}`;
12
- });
13
- return problems;
4
+ const execute_dql_1 = require("./execute-dql");
5
+ const listProblems = async (dtClient, additionalFilter) => {
6
+ // DQL Statement from Problems App to fetch all Davis Problems for the last 12 hours to now
7
+ const dql = `fetch dt.davis.problems, from: now()-12h, to: now()
8
+ | filter isNull(dt.davis.is_duplicate) OR not(dt.davis.is_duplicate)
9
+ ${additionalFilter ? `| filter ${additionalFilter}` : ''}
10
+ | fieldsAdd
11
+ duration = coalesce(event.end, now()) - event.start,
12
+ affected_entities_count = arraySize(affected_entity_ids),
13
+ event_count = arraySize(dt.davis.event_ids),
14
+ affected_users_count = dt.davis.affected_users_count,
15
+ problem_id = event.id
16
+ | fields display_id, event.name, event.description, event.status, event.category, event.start, event.end,
17
+ root_cause_entity_id, root_cause_entity_name, duration, affected_entities_count,
18
+ event_count, affected_users_count, problem_id, dt.davis.mute.status, dt.davis.mute.user,
19
+ entity_tags, labels.alerting_profile, maintenance.is_under_maintenance,
20
+ aws.account.id, azure.resource.group, azure.subscription, cloud.provider, cloud.region,
21
+ dt.cost.costcenter, dt.cost.product, dt.host_group.id, dt.security_context, gcp.project.id,
22
+ host.name,
23
+ k8s.cluster.name, k8s.cluster.uid, k8s.container.name, k8s.namespace.name, k8s.node.name, k8s.pod.name, k8s.service.name, k8s.workload.kind, k8s.workload.name
24
+ | sort event.status asc, event.start desc
25
+ `;
26
+ return await (0, execute_dql_1.executeDql)(dtClient, { query: dql, maxResultRecords: 5000, maxResultBytes: /* 5 MB */ 5000000 });
14
27
  };
15
28
  exports.listProblems = listProblems;
@@ -6,12 +6,16 @@ exports.getDynatraceEnv = getDynatraceEnv;
6
6
  * Throws an Error if validation fails.
7
7
  */
8
8
  function getDynatraceEnv(env = process.env) {
9
- const oauthClient = env.OAUTH_CLIENT_ID;
9
+ const oauthClientId = env.OAUTH_CLIENT_ID;
10
10
  const oauthClientSecret = env.OAUTH_CLIENT_SECRET;
11
+ const dtPlatformToken = env.DT_PLATFORM_TOKEN;
11
12
  const dtEnvironment = env.DT_ENVIRONMENT;
12
13
  const slackConnectionId = env.SLACK_CONNECTION_ID || 'fake-slack-connection-id';
13
- if (!oauthClient || !oauthClientSecret || !dtEnvironment) {
14
- throw new Error('Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables');
14
+ if (!dtEnvironment) {
15
+ throw new Error('Please set DT_ENVIRONMENT environment variable to your Dynatrace Platform Environment');
16
+ }
17
+ if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
18
+ throw new Error('Please set either OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, or DT_PLATFORM_TOKEN environment variables');
15
19
  }
16
20
  if (!dtEnvironment.startsWith('https://')) {
17
21
  throw new Error('Please set DT_ENVIRONMENT to a valid Dynatrace Environment URL (e.g., https://<environment-id>.apps.dynatrace.com)');
@@ -19,5 +23,5 @@ function getDynatraceEnv(env = process.env) {
19
23
  if (!dtEnvironment.includes('apps.dynatrace.com') && !dtEnvironment.includes('apps.dynatracelabs.com')) {
20
24
  throw new Error('Please set DT_ENVIRONMENT to a valid Dynatrace Platform Environment URL (e.g., https://<environment-id>.apps.dynatrace.com)');
21
25
  }
22
- return { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId };
26
+ return { oauthClientId, oauthClientSecret, dtPlatformToken, dtEnvironment, slackConnectionId };
23
27
  }
@@ -6,15 +6,17 @@ describe('getDynatraceEnv', () => {
6
6
  OAUTH_CLIENT_ID: 'dt0s02.SAMPLE',
7
7
  OAUTH_CLIENT_SECRET: 'dt0s02.SAMPLE.abcd1234',
8
8
  DT_ENVIRONMENT: 'https://abc123.apps.dynatrace.com',
9
+ DT_PLATFORM_TOKEN: 'dt0s16.SAMPLE.abcd1234',
9
10
  SLACK_CONNECTION_ID: 'slack-conn-id',
10
11
  };
11
12
  it('returns all required values when environment is valid', () => {
12
13
  const env = { ...baseEnv };
13
14
  const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
14
15
  expect(result).toEqual({
15
- oauthClient: env.OAUTH_CLIENT_ID,
16
+ oauthClientId: env.OAUTH_CLIENT_ID,
16
17
  oauthClientSecret: env.OAUTH_CLIENT_SECRET,
17
18
  dtEnvironment: env.DT_ENVIRONMENT,
19
+ dtPlatformToken: env.DT_PLATFORM_TOKEN,
18
20
  slackConnectionId: env.SLACK_CONNECTION_ID,
19
21
  });
20
22
  });
@@ -23,14 +25,15 @@ describe('getDynatraceEnv', () => {
23
25
  const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
24
26
  expect(result.slackConnectionId).toBe('fake-slack-connection-id');
25
27
  });
26
- it('throws if OAUTH_CLIENT_ID is missing', () => {
27
- const env = { ...baseEnv, OAUTH_CLIENT_ID: undefined };
28
+ it('throws if environment variables for auth credentials are missing', () => {
29
+ const env = {
30
+ ...baseEnv,
31
+ OAUTH_CLIENT_ID: undefined,
32
+ OAUTH_CLIENT_SECRET: undefined,
33
+ DT_PLATFORM_TOKEN: undefined,
34
+ };
28
35
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_ID/);
29
36
  });
30
- it('throws if OAUTH_CLIENT_SECRET is missing', () => {
31
- const env = { ...baseEnv, OAUTH_CLIENT_SECRET: undefined };
32
- expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_SECRET/);
33
- });
34
37
  it('throws if DT_ENVIRONMENT is missing', () => {
35
38
  const env = { ...baseEnv, DT_ENVIRONMENT: undefined };
36
39
  expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/DT_ENVIRONMENT/);
package/dist/index.js CHANGED
@@ -11,7 +11,6 @@ const package_json_1 = require("../package.json");
11
11
  const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
12
12
  const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
13
13
  const list_problems_1 = require("./capabilities/list-problems");
14
- const get_problem_details_1 = require("./capabilities/get-problem-details");
15
14
  const get_monitored_entity_details_1 = require("./capabilities/get-monitored-entity-details");
16
15
  const get_ownership_information_1 = require("./capabilities/get-ownership-information");
17
16
  const get_logs_for_entity_1 = require("./capabilities/get-logs-for-entity");
@@ -22,6 +21,7 @@ const get_vulnerability_details_1 = require("./capabilities/get-vulnerability-de
22
21
  const execute_dql_1 = require("./capabilities/execute-dql");
23
22
  const send_slack_message_1 = require("./capabilities/send-slack-message");
24
23
  const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-entity-by-name");
24
+ const davis_copilot_1 = require("./capabilities/davis-copilot");
25
25
  const getDynatraceEnv_1 = require("./getDynatraceEnv");
26
26
  (0, dotenv_1.config)();
27
27
  let scopesBase = [
@@ -38,7 +38,8 @@ const main = async () => {
38
38
  console.error(err.message);
39
39
  process.exit(1);
40
40
  }
41
- const { oauthClient, oauthClientSecret, dtEnvironment, slackConnectionId } = dynatraceEnv;
41
+ console.error(`Initializing Dynatrace MCP Server v${package_json_1.version}...`);
42
+ const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv;
42
43
  console.error(`Starting Dynatrace MCP Server v${package_json_1.version}...`);
43
44
  const server = new mcp_js_1.McpServer({
44
45
  name: 'Dynatrace MCP Server',
@@ -88,7 +89,7 @@ const main = async () => {
88
89
  };
89
90
  tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant)', {}, async ({}) => {
90
91
  // create an oauth-client
91
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase);
92
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
92
93
  const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
93
94
  const environmentInfo = await environmentInformationClient.getEnvironmentInformation();
94
95
  let resp = `Environment Information (also referred to as tenant):
@@ -97,7 +98,7 @@ const main = async () => {
97
98
  return resp;
98
99
  });
99
100
  tool('list_vulnerabilities', 'List all vulnerabilities from Dynatrace', {}, async ({}) => {
100
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:security-problems:read'));
101
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:security-problems:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
101
102
  const result = await (0, list_vulnerabilities_1.listVulnerabilities)(dtClient);
102
103
  if (!result || result.length === 0) {
103
104
  return 'No vulnerabilities found';
@@ -112,7 +113,7 @@ const main = async () => {
112
113
  tool('get_vulnerabilty_details', 'Get details of a vulnerability by `securityProblemId` on Dynatrace', {
113
114
  securityProblemId: zod_1.z.string().optional(),
114
115
  }, async ({ securityProblemId }) => {
115
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:security-problems:read'));
116
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:security-problems:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
116
117
  const result = await (0, get_vulnerability_details_1.getVulnerabilityDetails)(dtClient, securityProblemId);
117
118
  let resp = `The Security Problem (Vulnerability) ${result.displayId} with securityProblemId ${result.securityProblemId} has the title ${result.title}.\n`;
118
119
  resp += `The related CVEs are ${result.cveIds?.join(',') || 'unknown'}.\n`;
@@ -157,49 +158,55 @@ const main = async () => {
157
158
  resp += `Tell the user to access the link ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities/vulnerabilities/${result.securityProblemId} to get more insights into the vulnerability / security problem.\n`;
158
159
  return resp;
159
160
  });
160
- tool('list_problems', 'List all problems known on Dynatrace', {}, async ({}) => {
161
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:problems:read'));
162
- const result = await (0, list_problems_1.listProblems)(dtClient);
163
- if (!result || result.length === 0) {
164
- return 'No problems found';
165
- }
166
- return `Found these problems: ${result.join(',')}`;
167
- });
168
- tool('get_problem_details', 'Get details of a problem on Dynatrace', {
169
- problemId: zod_1.z.string().optional(),
170
- }, async ({ problemId }) => {
171
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:problems:read'));
172
- const result = await (0, get_problem_details_1.getProblemDetails)(dtClient, problemId);
173
- let resp = `The problem ${result.displayId} with the title ${result.title} (ID: ${result.problemId}).` +
174
- `The severity is ${result.severityLevel}, and it affects ${result.affectedEntities.length} entities:`;
175
- for (const entity of result.affectedEntities) {
176
- resp += `\n- ${entity.name} (please refer to this entity with \`entityId\` ${entity.entityId?.id})`;
177
- }
178
- resp += `The problem first appeared at ${result.startTime}\n`;
179
- if (result.rootCauseEntity) {
180
- resp += `The possible root-cause could be in entity ${result.rootCauseEntity?.name} with \`entityId\` ${result.rootCauseEntity?.entityId?.id}.\n`;
181
- }
182
- if (result.impactAnalysis) {
183
- let estimatedAffectedUsers = 0;
184
- result.impactAnalysis.impacts.forEach((impact) => {
185
- estimatedAffectedUsers += impact.estimatedAffectedUsers;
161
+ tool('list_problems', 'List all problems (dt.davis.problems) known on Dynatrace, sorted by their recency, for the last 12h. An additional filter can be provided using DQL filter.', {
162
+ additionalFilter: zod_1.z
163
+ .string()
164
+ .optional()
165
+ .describe('Additional filter for DQL statement for dt.davis.problems, e.g., \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\''),
166
+ maxProblemsToDisplay: zod_1.z.number().default(10).describe('Maximum number of problems to display in the response.'),
167
+ }, async ({ additionalFilter, maxProblemsToDisplay }) => {
168
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read', 'storage:buckets:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
169
+ // get problems (uses fetch)
170
+ const result = await (0, list_problems_1.listProblems)(dtClient, additionalFilter);
171
+ if (result && result.length > 0) {
172
+ let resp = `Found ${result.length} problems! Displaying the top ${maxProblemsToDisplay} problems:\n`;
173
+ // iterate over dqlResponse and create a string with the problem details, but only show the top maxProblemsToDisplay problems
174
+ result.slice(0, maxProblemsToDisplay).forEach((problem) => {
175
+ if (problem) {
176
+ resp += `Problem ${problem['display_id']} (please refer to this problem with \`problemId\` or \`event.id\` ${problem['problem_id']}))
177
+ with event.status ${problem['event.status']}, event.category ${problem['event.category']}: ${problem['event.name']} -
178
+ affects ${problem['affected_users_count']} users and ${problem['affected_entity_count']} entities for a duration of ${problem['duration']}\n`;
179
+ }
186
180
  });
187
- resp += `The problem is estimated to affect ${estimatedAffectedUsers} users.\n`;
181
+ resp +=
182
+ `\nNext Steps:` +
183
+ `\n1. Use "execute_dql" tool with the following query to get more details about a specific problem:
184
+ "fetch dt.davis.problems, from: now()-10h, to: now() | filter event.id == \"<problem-id>\" | fields event.description, event.status, event.category, event.start, event.end,
185
+ root_cause_entity_id, root_cause_entity_name, duration, affected_entities_count,
186
+ event_count, affected_users_count, problem_id, dt.davis.mute.status, dt.davis.mute.user,
187
+ entity_tags, labels.alerting_profile, maintenance.is_under_maintenance,
188
+ aws.account.id, azure.resource.group, azure.subscription, cloud.provider, cloud.region,
189
+ dt.cost.costcenter, dt.cost.product, dt.host_group.id, dt.security_context, gcp.project.id,
190
+ host.name, k8s.cluster.name, k8s.cluster.uid, k8s.container.name, k8s.namespace.name, k8s.node.name, k8s.pod.name, k8s.service.name, k8s.workload.kind, k8s.workload.name"` +
191
+ `\n2. Use "chat_with_davis_copilot" tool and provide \`problemId\` as context, to get insights about a specific problem via Davis Copilot.` +
192
+ `\n3. Tell the user to visit ${dtEnvironment}/ui/apps/dynatrace.davis.problems/problem/<problem-id> for more details.`;
193
+ return resp;
194
+ }
195
+ else {
196
+ return 'No problems found';
188
197
  }
189
- resp += `Tell the user to access the link ${dtEnvironment}/ui/apps/dynatrace.davis.problems/problem/${result.problemId} to get more insights into the problem.\n`;
190
- return resp;
191
198
  });
192
199
  tool('find_entity_by_name', 'Get the entityId of a monitored entity based on the name of the entity on Dynatrace', {
193
200
  entityName: zod_1.z.string(),
194
201
  }, async ({ entityName }) => {
195
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read', 'storage:entities:read'));
202
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
196
203
  const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
197
204
  return entityResponse;
198
205
  });
199
206
  tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
200
207
  entityId: zod_1.z.string().optional(),
201
208
  }, async ({ entityId }) => {
202
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read'));
209
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
203
210
  const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
204
211
  let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
205
212
  `Properties: ${JSON.stringify(entityDetails.properties)}\n`;
@@ -221,21 +228,21 @@ const main = async () => {
221
228
  channel: zod_1.z.string().optional(),
222
229
  message: zod_1.z.string().optional(),
223
230
  }, async ({ channel, message }) => {
224
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('app-settings:objects:read'));
231
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
225
232
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
226
233
  return `Message sent to Slack channel: ${JSON.stringify(response)}`;
227
234
  });
228
235
  tool('get_logs_for_entity', 'Get Logs for a monitored entity based on name of the entity on Dynatrace', {
229
236
  entityName: zod_1.z.string().optional(),
230
237
  }, async ({ entityName }) => {
231
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:logs:read'));
238
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:logs:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
232
239
  const logs = await (0, get_logs_for_entity_1.getLogsForEntity)(dtClient, entityName);
233
240
  return `Logs:\n${JSON.stringify(logs?.map((logLine) => (logLine ? logLine.content : 'Empty log')))}`;
234
241
  });
235
242
  tool('verify_dql', 'Verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. This is useful to ensure that the DQL statement is valid and can be executed without errors.', {
236
243
  dqlStatement: zod_1.z.string(),
237
244
  }, async ({ dqlStatement }) => {
238
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase);
245
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
239
246
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
240
247
  let resp = 'DQL Statement Verification:\n';
241
248
  if (response.notifications && response.notifications.length > 0) {
@@ -255,7 +262,7 @@ const main = async () => {
255
262
  tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. Always use "verify_dql" tool before you execute a DQL statement. A valid statement looks like this: "fetch [logs, metrics, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}. Adapt filters for certain attributes: `traceId` could be `trace_id` or `trace.id`.', {
256
263
  dqlStatement: zod_1.z.string(),
257
264
  }, async ({ dqlStatement }) => {
258
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
265
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
259
266
  'storage:logs:read', // Read logs for reliability guardian validations
260
267
  'storage:metrics:read', // Read metrics for reliability guardian validations
261
268
  'storage:bizevents:read', // Read bizevents for reliability guardian validations
@@ -265,17 +272,102 @@ const main = async () => {
265
272
  'storage:system:read', // Read System Data from Grail
266
273
  'storage:user.events:read', // Read User events from Grail
267
274
  'storage:user.sessions:read', // Read User sessions from Grail
268
- 'storage:security.events:read'));
269
- const response = await (0, execute_dql_1.executeDql)(dtClient, dqlStatement);
275
+ 'storage:security.events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
276
+ const response = await (0, execute_dql_1.executeDql)(dtClient, { query: dqlStatement });
270
277
  return `DQL Response: ${JSON.stringify(response)}`;
271
278
  });
279
+ tool('generate_dql_from_natural_language', "Convert natural language queries to Dynatrace Query Language (DQL) using Davis CoPilot AI. You can ask for problem events, security issues, logs, metrics, spans, and custom data. Workflow: 1) Generate DQL, 2) Verify with verify_dql tool, 3) Execute with execute_dql tool, 4) Iterate if results don't match expectations.", {
280
+ text: zod_1.z
281
+ .string()
282
+ .describe('Natural language description of what you want to query. Be specific and include time ranges, entities, and metrics of interest.'),
283
+ }, async ({ text }) => {
284
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:nl2dql:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
285
+ const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
286
+ let resp = `🔤 Natural Language to DQL:\n\n`;
287
+ resp += `**Query:** "${text}"\n\n`;
288
+ resp += `**Generated DQL:**\n\`\`\`\n${response.dql}\n\`\`\`\n\n`;
289
+ resp += `**Status:** ${response.status}\n`;
290
+ resp += `**Message Token:** ${response.messageToken}\n`;
291
+ if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
292
+ resp += `\n**Notifications:**\n`;
293
+ response.metadata.notifications.forEach((notification) => {
294
+ resp += `- ${notification.severity}: ${notification.message}\n`;
295
+ });
296
+ }
297
+ resp += `\n💡 **Next Steps:**\n`;
298
+ resp += `1. Use "verify_dql" tool to validate this query\n`;
299
+ resp += `2. Use "execute_dql" tool to run the query\n`;
300
+ resp += `3. If results don't match expectations, refine your natural language description and try again\n`;
301
+ return resp;
302
+ });
303
+ tool('explain_dql_in_natural_language', 'Explain Dynatrace Query Language (DQL) statements in natural language using Davis CoPilot AI.', {
304
+ dql: zod_1.z.string().describe('The DQL statement to explain'),
305
+ }, async ({ dql }) => {
306
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:dql2nl:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
307
+ const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
308
+ let resp = `📝 DQL to Natural Language:\n\n`;
309
+ resp += `**DQL Query:**\n\`\`\`\n${dql}\n\`\`\`\n\n`;
310
+ resp += `**Summary:** ${response.summary}\n\n`;
311
+ resp += `**Detailed Explanation:**\n${response.explanation}\n\n`;
312
+ resp += `**Status:** ${response.status}\n`;
313
+ resp += `**Message Token:** ${response.messageToken}\n`;
314
+ if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
315
+ resp += `\n**Notifications:**\n`;
316
+ response.metadata.notifications.forEach((notification) => {
317
+ resp += `- ${notification.severity}: ${notification.message}\n`;
318
+ });
319
+ }
320
+ return resp;
321
+ });
322
+ tool('chat_with_davis_copilot', 'Use this tool in case no specific tool is available. Get an answer to any Dynatrace related question as well as troubleshooting, and guidance. *(Note: Davis CoPilot AI is GA, but the Davis CoPilot APIs are in preview)*', {
323
+ text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
324
+ context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
325
+ instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
326
+ }, async ({ text, context, instruction }) => {
327
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:conversations:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
328
+ const conversationContext = [];
329
+ if (context) {
330
+ conversationContext.push({
331
+ type: 'supplementary',
332
+ value: context,
333
+ });
334
+ }
335
+ if (instruction) {
336
+ conversationContext.push({
337
+ type: 'instruction',
338
+ value: instruction,
339
+ });
340
+ }
341
+ const response = await (0, davis_copilot_1.chatWithDavisCopilot)(dtClient, text, conversationContext);
342
+ let resp = `🤖 Davis CoPilot Response:\n\n`;
343
+ resp += `**Your Question:** "${text}"\n\n`;
344
+ resp += `**Answer:**\n${response.text}\n\n`;
345
+ resp += `**Status:** ${response.status}\n`;
346
+ resp += `**Message Token:** ${response.messageToken}\n`;
347
+ if (response.metadata?.sources && response.metadata.sources.length > 0) {
348
+ resp += `\n**Sources:**\n`;
349
+ response.metadata.sources.forEach((source) => {
350
+ resp += `- ${source.title || 'Untitled'}: ${source.url || 'No URL'}\n`;
351
+ });
352
+ }
353
+ if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
354
+ resp += `\n**Notifications:**\n`;
355
+ response.metadata.notifications.forEach((notification) => {
356
+ resp += `- ${notification.severity}: ${notification.message}\n`;
357
+ });
358
+ }
359
+ if (response.state?.conversationId) {
360
+ resp += `\n**Conversation ID:** ${response.state.conversationId}`;
361
+ }
362
+ return resp;
363
+ });
272
364
  tool('create_workflow_for_notification', 'Create a notification for a team based on a problem type within Workflows in Dynatrace', {
273
365
  problemType: zod_1.z.string().optional(),
274
366
  teamName: zod_1.z.string().optional(),
275
367
  channel: zod_1.z.string().optional(),
276
368
  isPrivate: zod_1.z.boolean().optional().default(false),
277
369
  }, async ({ problemType, teamName, channel, isPrivate }) => {
278
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'));
370
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
279
371
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
280
372
  let resp = `Workflow Created: ${response?.id} with name ${response?.title}.\nYou can access the Workflow via the following link: ${dtEnvironment}/ui/apps/dynatrace.automations/workflows/${response?.id}.\nTell the user to inspect the Workflow by visiting the link.\n`;
281
373
  if (response.type == 'SIMPLE') {
@@ -292,7 +384,7 @@ const main = async () => {
292
384
  tool('make_workflow_public', 'Modify a workflow and make it publicly available to everyone on the Dynatrace Environment', {
293
385
  workflowId: zod_1.z.string().optional(),
294
386
  }, async ({ workflowId }) => {
295
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'));
387
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
296
388
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
297
389
  isPrivate: false,
298
390
  });
@@ -304,14 +396,14 @@ const main = async () => {
304
396
  .optional()
305
397
  .describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`),
306
398
  }, async ({ clusterId }) => {
307
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('storage:events:read'));
399
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
308
400
  const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
309
401
  return `Kubernetes Events:\n${JSON.stringify(events)}`;
310
402
  });
311
403
  tool('get_ownership', 'Get detailed Ownership information for one or multiple entities on Dynatrace', {
312
404
  entityIds: zod_1.z.string().optional().describe('Comma separated list of entityIds'),
313
405
  }, async ({ entityIds }) => {
314
- const dtClient = await (0, dynatrace_clients_1.createOAuthClient)(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'));
406
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
315
407
  console.error(`Fetching ownership for ${entityIds}`);
316
408
  const ownershipInformation = await (0, get_ownership_information_1.getOwnershipInformation)(dtClient, entityIds);
317
409
  console.error(`Done!`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0-rc.2",
4
4
  "description": "Model Context Protocol (MCP) server for Dynatrace",
5
5
  "keywords": [
6
6
  "Dynatrace",
@@ -38,6 +38,8 @@
38
38
  "prepare": "npm run build",
39
39
  "watch": "tsc --watch",
40
40
  "test": "jest",
41
+ "test:unit": "jest --selectProjects unit",
42
+ "test:integration": "jest --selectProjects integration --runInBand",
41
43
  "prettier": "prettier --check .",
42
44
  "prettier:fix": "prettier --write ."
43
45
  },