@dynatrace-oss/dynatrace-mcp-server 0.1.0 → 0.1.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
|
@@ -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
|
|
4
|
+
const execute_dql_1 = require("./execute-dql");
|
|
5
5
|
const getEventsForCluster = async (dtClient, clusterId) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
4
|
+
const execute_dql_1 = require("./execute-dql");
|
|
5
5
|
const getLogsForEntity = async (dtClient, entityId) => {
|
|
6
|
-
const
|
|
7
|
-
|
|
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
|
+
throw new 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
|
-
|
|
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:
|
|
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()
|
|
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()
|
|
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,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynatrace-oss/dynatrace-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Dynatrace",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Dynatrace",
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"server"
|
|
10
|
+
],
|
|
5
11
|
"main": "./dist/index.js",
|
|
6
12
|
"type": "commonjs",
|
|
7
13
|
"bin": {
|
|
@@ -17,6 +23,13 @@
|
|
|
17
23
|
"default": "./index.js"
|
|
18
24
|
}
|
|
19
25
|
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/dynatrace-oss/dynatrace-mcp/issues"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/dynatrace-oss/dynatrace-mcp.git"
|
|
32
|
+
},
|
|
20
33
|
"directories": {
|
|
21
34
|
"dist": "dist"
|
|
22
35
|
},
|