@dynatrace-oss/dynatrace-mcp-server 0.1.1 → 0.1.3

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
@@ -41,6 +41,25 @@ You can add this MCP server (using STDIO) to your MCP Client like VS Code, Claud
41
41
  }
42
42
  ```
43
43
 
44
+ Please note: In this config, [the `${workspaceFolder}` variable](https://code.visualstudio.com/docs/reference/variables-reference#_predefined-variables) is used.
45
+ This only works if the config is stored in the current workspaces, e.g., `<your-repo>/.vscode/mcp.json`. Alternatively, this can also be stored in user-settings, and you can define `env` as follows:
46
+
47
+ ```json
48
+ {
49
+ "servers": {
50
+ "npx-dynatrace-mcp-server": {
51
+ "command": "npx",
52
+ "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
53
+ "env": {
54
+ "OAUTH_CLIENT_ID": "",
55
+ "OAUTH_CLIENT_SECRET": "",
56
+ "DT_ENVIRONMENT": ""
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
44
63
  **Claude Desktop**
45
64
  ```json
46
65
  {
@@ -58,6 +77,25 @@ You can add this MCP server (using STDIO) to your MCP Client like VS Code, Claud
58
77
  }
59
78
  ```
60
79
 
80
+ **Amazon Q Developer CLI**
81
+
82
+ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-chat.html) provides an interactive chat experience directly in your terminal. You can ask questions, get help with AWS services, troubleshoot issues, and generate code snippets without leaving your command line environment.
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "mobile-mcp": {
87
+ "command": "npx",
88
+ "args": ["-y", "@dynatrace-oss/dynatrace-mcp-server@latest"],
89
+ "env": {
90
+ "OAUTH_CLIENT_ID": "",
91
+ "OAUTH_CLIENT_SECRET": "",
92
+ "DT_ENVIRONMENT": ""
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
61
99
  ## Environment Variables
62
100
 
63
101
  A **Dynatrace OAuth Client** is needed to communicate with your Dynatrace Environment. Please follow the documentation about
@@ -83,6 +121,7 @@ and set up the following environment variables in order for this MCP to work:
83
121
  * `storage:bizevents:read` - Read bizevents for reliability guardian validations
84
122
  * `storage:spans:read` - Read spans from Grail
85
123
  * `storage:entities:read` - Read Entities from Grail
124
+ * `storage:events:read` - Read Events from Grail
86
125
  * `storage:system:read` - Read System Data from Grail
87
126
  * `storage:user.events:read` - Read User events from Grail
88
127
  * `storage:user.sessions:read` - Read User sessions from Grail
@@ -1,35 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getEventsForCluster = void 0;
4
- const client_query_1 = require("@dynatrace-sdk/client-query");
4
+ const execute_dql_1 = require("./execute-dql");
5
5
  const getEventsForCluster = async (dtClient, clusterId) => {
6
- const queryExecutionClient = new client_query_1.QueryExecutionClient(dtClient);
7
- const response = await queryExecutionClient.queryExecute({
8
- body: {
9
- query: `fetch events | filter k8s.cluster.id == "${clusterId}"`,
10
- }
11
- });
12
- if (response.result) {
13
- // return response result immediately
14
- return response.result.records;
6
+ let dql = `fetch events | filter k8s.cluster.uid == "${clusterId}"`;
7
+ if (!clusterId) {
8
+ // if no clusterId is provided, we need to fetch all events
9
+ dql = `fetch events | filter isNotNull(k8s.cluster.uid)`;
15
10
  }
16
- // else: We might have to poll
17
- if (response.requestToken) {
18
- // poll for the result
19
- let pollResponse;
20
- do {
21
- // sleep for 2 seconds
22
- await new Promise(resolve => setTimeout(resolve, 2000));
23
- pollResponse = await queryExecutionClient.queryPoll({
24
- requestToken: response.requestToken,
25
- });
26
- // done - let's return it
27
- if (pollResponse.result) {
28
- return pollResponse.result.records;
29
- }
30
- } while (pollResponse.state === 'RUNNING' || pollResponse.state === 'NOT_STARTED');
31
- }
32
- // else: whatever happened - we have an error
33
- return undefined;
11
+ return (0, execute_dql_1.executeDql)(dtClient, dql);
34
12
  };
35
13
  exports.getEventsForCluster = getEventsForCluster;
@@ -1,35 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getLogsForEntity = void 0;
4
- const client_query_1 = require("@dynatrace-sdk/client-query");
4
+ const execute_dql_1 = require("./execute-dql");
5
5
  const getLogsForEntity = async (dtClient, entityId) => {
6
- const queryExecutionClient = new client_query_1.QueryExecutionClient(dtClient);
7
- const response = await queryExecutionClient.queryExecute({
8
- body: {
9
- query: `fetch logs | filter dt.source_entity == "${entityId}"`,
10
- }
11
- });
12
- if (response.result) {
13
- // return response result immediately
14
- return response.result.records;
15
- }
16
- // else: We might have to poll
17
- if (response.requestToken) {
18
- // poll for the result
19
- let pollResponse;
20
- do {
21
- // sleep for 2 seconds
22
- await new Promise(resolve => setTimeout(resolve, 2000));
23
- pollResponse = await queryExecutionClient.queryPoll({
24
- requestToken: response.requestToken,
25
- });
26
- // done - let's return it
27
- if (pollResponse.result) {
28
- return pollResponse.result.records;
29
- }
30
- } while (pollResponse.state === 'RUNNING' || pollResponse.state === 'NOT_STARTED');
31
- }
32
- // else: whatever happened - we have an error
33
- return undefined;
6
+ const dql = `fetch logs | filter dt.source_entity == "${entityId}"`;
7
+ return (0, execute_dql_1.executeDql)(dtClient, dql);
34
8
  };
35
9
  exports.getLogsForEntity = getLogsForEntity;
@@ -3,22 +3,52 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.callAppFunction = exports.createOAuthClient = void 0;
4
4
  const http_client_1 = require("@dynatrace-sdk/http-client");
5
5
  const dt_app_1 = require("dt-app");
6
+ /** Uses the provided oauth Client ID and Secret and requests a token */
7
+ const requestToken = async (clientId, clientSecret, authUrl, scopes) => {
8
+ const res = await fetch(authUrl, {
9
+ method: 'POST',
10
+ headers: {
11
+ 'Content-Type': 'application/x-www-form-urlencoded',
12
+ },
13
+ body: new URLSearchParams({
14
+ grant_type: 'client_credentials',
15
+ client_id: clientId,
16
+ client_secret: clientSecret,
17
+ scope: scopes.join(' '),
18
+ }),
19
+ });
20
+ if (!res.ok) {
21
+ console.error(`Failed to fetch token: ${res.status} ${res.statusText}`);
22
+ }
23
+ return await res.json();
24
+ };
6
25
  /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes */
7
26
  const createOAuthClient = async (clientId, clientSecret, environmentUrl, scopes) => {
8
- const baseUrl = await (0, dt_app_1.getSSOUrl)(environmentUrl);
9
27
  if (!clientId) {
10
28
  throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"');
11
29
  }
12
30
  if (!clientSecret) {
13
31
  throw new Error('Failed to retrieve OAuth client secret from env "DT_APP_OAUTH_CLIENT_SECRET"');
14
32
  }
15
- console.error(`baseUrl=${baseUrl}`);
33
+ if (!environmentUrl) {
34
+ throw new Error('Failed to retrieve environment URL from env "DT_ENVIRONMENT"');
35
+ }
36
+ console.error(`Trying to authenticate API Calls to ${environmentUrl} via OAuthClientId ${clientId}`);
37
+ const ssoBaseUrl = await (0, dt_app_1.getSSOUrl)(environmentUrl);
38
+ const ssoAuthUrl = new URL('/sso/oauth2/token', ssoBaseUrl).toString();
39
+ console.error(`Using SSO auth URL: ${ssoAuthUrl}`);
40
+ // try to request a token, just to verify that everything is set up correctly
41
+ const tokenResponse = await requestToken(clientId, clientSecret, ssoAuthUrl, scopes);
42
+ if (tokenResponse.error && tokenResponse.error_description) {
43
+ throw new Error(`Failed to retrieve OAuth token: ${tokenResponse.error} - ${tokenResponse.error_description}`);
44
+ }
45
+ console.error(`Successfully retrieved token from SSO!`);
16
46
  return new http_client_1._OAuthHttpClient({
17
47
  scopes,
18
48
  clientId,
19
49
  secret: clientSecret,
20
50
  environmentUrl,
21
- authUrl: new URL('/sso/oauth2/token', baseUrl).toString(),
51
+ authUrl: ssoAuthUrl,
22
52
  });
23
53
  };
24
54
  exports.createOAuthClient = createOAuthClient;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthHttpClient = void 0;
4
+ class OAuthHttpClient {
5
+ config;
6
+ oauthToken;
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ async send(options) {
11
+ const { statusValidator = defaultStatusValidator } = options;
12
+ if (!this.oauthToken || this.oauthToken.isExpired()) {
13
+ this.oauthToken = await this.requestToken();
14
+ }
15
+ const requestBodyType = options.requestBodyType ?? 'json';
16
+ const url = new URL(options.url, this.config.environmentUrl);
17
+ const body = options.body !== undefined
18
+ ? await encodeRequestBody(options.body, requestBodyType)
19
+ : undefined;
20
+ const headers = {
21
+ ...applyContentTypeHeader(options.headers, requestBodyType),
22
+ Authorization: `${this.oauthToken.tokenType} ${this.oauthToken.accessToken}`,
23
+ };
24
+ const response = await send(url.toString(), {
25
+ method: options.method,
26
+ signal: options.abortSignal,
27
+ body,
28
+ headers,
29
+ });
30
+ const ok = statusValidator(response.status);
31
+ if (ok) {
32
+ return new HttpClientResponse(response);
33
+ }
34
+ else {
35
+ throw new HttpClientResponseError(response, { requestMethod: options.method });
36
+ }
37
+ }
38
+ async requestToken() {
39
+ const res = await send(this.config.authUrl, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/x-www-form-urlencoded',
43
+ },
44
+ body: new URLSearchParams({
45
+ grant_type: 'client_credentials',
46
+ client_id: this.config.clientId,
47
+ client_secret: this.config.secret,
48
+ scope: this.config.scopes.join(' '),
49
+ }),
50
+ });
51
+ const data = await res.json();
52
+ return new OAuthToken(data);
53
+ }
54
+ }
55
+ exports.OAuthHttpClient = OAuthHttpClient;
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ let scopes = [
38
38
  'storage:bizevents:read', // Read bizevents for reliability guardian validations
39
39
  'storage:spans:read', // Read spans from Grail
40
40
  'storage:entities:read', // Read Entities from Grail
41
+ 'storage:events:read', // Read events from Grail
41
42
  'storage:system:read', // Read System Data from Grail
42
43
  'storage:user.events:read', // Read User events from Grail
43
44
  'storage:user.sessions:read', // Read User sessions from Grail
@@ -222,13 +223,13 @@ const main = async () => {
222
223
  return `Logs:\n${JSON.stringify(logs?.map(logLine => logLine ? logLine.content : 'Empty log'))}`;
223
224
  });
224
225
  tool("verify_dql", "Verify a DQL statement on Dynatrace", {
225
- dqlStatement: zod_1.z.string().optional()
226
+ dqlStatement: zod_1.z.string()
226
227
  }, async ({ dqlStatement }) => {
227
228
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
228
229
  return `Parsing DQL Statement resulted in: ${JSON.stringify(response)}`;
229
230
  });
230
231
  tool("execute_dql", "Get Logs, Metrics, Spans, Events from Dynatrace by executing a DQL statement. Please use verify_dql tool before you execute a DQL statement.", {
231
- dqlStatement: zod_1.z.string().optional()
232
+ dqlStatement: zod_1.z.string()
232
233
  }, async ({ dqlStatement }) => {
233
234
  const response = await (0, execute_dql_1.executeDql)(dtClient, dqlStatement);
234
235
  return `DQL Response: ${JSON.stringify(response)}`;
@@ -260,11 +261,11 @@ const main = async () => {
260
261
  });
261
262
  return `Workflow ${response.id} is now public!\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`;
262
263
  });
263
- tool("get_kubernetes_events", "Get all events from a specific Kubernetes cluster", {
264
- clusterId: zod_1.z.string().optional()
264
+ tool("get_kubernetes_events", "Get all events from a specific Kubernetes (K8s) cluster", {
265
+ clusterId: zod_1.z.string().optional().describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`)
265
266
  }, async ({ clusterId }) => {
266
267
  const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
267
- return `Events:\n${JSON.stringify(events)}`;
268
+ return `Kubernetes Events:\n${JSON.stringify(events)}`;
268
269
  });
269
270
  tool("get_ownership", "Get detailed Ownership information for one or multiple entities on Dynatrace", {
270
271
  entityIds: zod_1.z.string().optional().describe("Comma separated list of entityIds"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Model Context Protocol (MCP) server for Dynatrace",
5
5
  "keywords": [
6
6
  "Dynatrace",