@dynatrace-oss/dynatrace-mcp-server 0.4.0 → 0.5.0-rc.1
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 +70 -31
- package/dist/authentication/dynatrace-clients.js +30 -27
- package/dist/authentication/dynatrace-clients.test.js +189 -0
- package/dist/capabilities/davis-copilot.js +63 -0
- package/dist/getDynatraceEnv.js +8 -4
- package/dist/getDynatraceEnv.test.js +10 -7
- package/dist/index.js +104 -17
- package/package.json +3 -1
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
|
|
11
|
-
- Fix issues
|
|
12
|
-
-
|
|
13
|
-
- Natural language
|
|
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
|
-
|
|
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
|
-
**
|
|
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,77 @@ This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
|
|
|
106
113
|
|
|
107
114
|
## Environment Variables
|
|
108
115
|
|
|
109
|
-
|
|
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
|
-
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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:problems:read` - get problems (_currently not available for Platform Tokens_)
|
|
140
|
+
- `environment-api:metrics:read` - read metrics (_currently not available for Platform Tokens_)
|
|
141
|
+
- `environment-api:slo:read` - read SLOs (_currently not available for Platform Tokens_)
|
|
142
|
+
- `automation:workflows:read` - read Workflows
|
|
143
|
+
- `automation:workflows:write` - create and update Workflows
|
|
144
|
+
- `automation:workflows:run` - run Workflows
|
|
145
|
+
- `storage:buckets:read` - needed for `execute_dql` tool to read all system data stored on Grail
|
|
146
|
+
- `storage:logs:read` - needed for `execute_dql` tool to read logs for reliability guardian validations
|
|
147
|
+
- `storage:metrics:read` - needed for `execute_dql` tool to read metrics for reliability guardian validations
|
|
148
|
+
- `storage:bizevents:read` - needed for `execute_dql` tool to read bizevents for reliability guardian validations
|
|
149
|
+
- `storage:spans:read` - needed for `execute_dql` tool to read spans from Grail
|
|
150
|
+
- `storage:entities:read` - needed for `execute_dql` tool to read Entities from Grail
|
|
151
|
+
- `storage:events:read` - needed for `execute_dql` tool to read Events from Grail
|
|
152
|
+
- `storage:security.events:read`- needed for `execute_dql` tool to read Security Events from Grail
|
|
153
|
+
- `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
|
|
154
|
+
- `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
|
|
155
|
+
- `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
|
|
156
|
+
- `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
|
|
157
|
+
- `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
|
|
158
|
+
- `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
|
|
159
|
+
- `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings
|
|
160
|
+
|
|
161
|
+
**Note**: Please ensure that `settings:objects:read` is used, and _not_ the similarly named scope `app-settings:objects:read`.
|
|
162
|
+
|
|
143
163
|
## ✨ Example prompts ✨
|
|
144
164
|
|
|
145
165
|
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
166
|
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
167
|
|
|
168
|
+
**Write a DQL query from natural language:**
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Show me error rates for the payment service in the last hour
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Explain a DQL query:**
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
What does this DQL do?
|
|
178
|
+
fetch logs | filter dt.source_entity == 'SERVICE-123' | summarize count(), by:{severity} | sort count() desc
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Chat with Davis CoPilot:**
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
How can I investigate slow database queries in Dynatrace?
|
|
185
|
+
```
|
|
186
|
+
|
|
148
187
|
**Find open vulnerabilities on production, setup alert.**
|
|
149
188
|
|
|
150
189
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
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
|
-
*
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/getDynatraceEnv.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
14
|
-
throw new Error('Please set
|
|
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 {
|
|
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
|
-
|
|
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
|
|
27
|
-
const env = {
|
|
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
|
@@ -22,6 +22,7 @@ const get_vulnerability_details_1 = require("./capabilities/get-vulnerability-de
|
|
|
22
22
|
const execute_dql_1 = require("./capabilities/execute-dql");
|
|
23
23
|
const send_slack_message_1 = require("./capabilities/send-slack-message");
|
|
24
24
|
const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-entity-by-name");
|
|
25
|
+
const davis_copilot_1 = require("./capabilities/davis-copilot");
|
|
25
26
|
const getDynatraceEnv_1 = require("./getDynatraceEnv");
|
|
26
27
|
(0, dotenv_1.config)();
|
|
27
28
|
let scopesBase = [
|
|
@@ -38,7 +39,8 @@ const main = async () => {
|
|
|
38
39
|
console.error(err.message);
|
|
39
40
|
process.exit(1);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
console.error(`Initializing Dynatrace MCP Server v${package_json_1.version}...`);
|
|
43
|
+
const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv;
|
|
42
44
|
console.error(`Starting Dynatrace MCP Server v${package_json_1.version}...`);
|
|
43
45
|
const server = new mcp_js_1.McpServer({
|
|
44
46
|
name: 'Dynatrace MCP Server',
|
|
@@ -88,7 +90,7 @@ const main = async () => {
|
|
|
88
90
|
};
|
|
89
91
|
tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant)', {}, async ({}) => {
|
|
90
92
|
// create an oauth-client
|
|
91
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
93
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
92
94
|
const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
|
|
93
95
|
const environmentInfo = await environmentInformationClient.getEnvironmentInformation();
|
|
94
96
|
let resp = `Environment Information (also referred to as tenant):
|
|
@@ -97,7 +99,7 @@ const main = async () => {
|
|
|
97
99
|
return resp;
|
|
98
100
|
});
|
|
99
101
|
tool('list_vulnerabilities', 'List all vulnerabilities from Dynatrace', {}, async ({}) => {
|
|
100
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
102
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:security-problems:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
101
103
|
const result = await (0, list_vulnerabilities_1.listVulnerabilities)(dtClient);
|
|
102
104
|
if (!result || result.length === 0) {
|
|
103
105
|
return 'No vulnerabilities found';
|
|
@@ -112,7 +114,7 @@ const main = async () => {
|
|
|
112
114
|
tool('get_vulnerabilty_details', 'Get details of a vulnerability by `securityProblemId` on Dynatrace', {
|
|
113
115
|
securityProblemId: zod_1.z.string().optional(),
|
|
114
116
|
}, async ({ securityProblemId }) => {
|
|
115
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
117
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:security-problems:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
116
118
|
const result = await (0, get_vulnerability_details_1.getVulnerabilityDetails)(dtClient, securityProblemId);
|
|
117
119
|
let resp = `The Security Problem (Vulnerability) ${result.displayId} with securityProblemId ${result.securityProblemId} has the title ${result.title}.\n`;
|
|
118
120
|
resp += `The related CVEs are ${result.cveIds?.join(',') || 'unknown'}.\n`;
|
|
@@ -158,7 +160,7 @@ const main = async () => {
|
|
|
158
160
|
return resp;
|
|
159
161
|
});
|
|
160
162
|
tool('list_problems', 'List all problems known on Dynatrace', {}, async ({}) => {
|
|
161
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
163
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:problems:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
162
164
|
const result = await (0, list_problems_1.listProblems)(dtClient);
|
|
163
165
|
if (!result || result.length === 0) {
|
|
164
166
|
return 'No problems found';
|
|
@@ -168,7 +170,7 @@ const main = async () => {
|
|
|
168
170
|
tool('get_problem_details', 'Get details of a problem on Dynatrace', {
|
|
169
171
|
problemId: zod_1.z.string().optional(),
|
|
170
172
|
}, async ({ problemId }) => {
|
|
171
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
173
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:problems:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
172
174
|
const result = await (0, get_problem_details_1.getProblemDetails)(dtClient, problemId);
|
|
173
175
|
let resp = `The problem ${result.displayId} with the title ${result.title} (ID: ${result.problemId}).` +
|
|
174
176
|
`The severity is ${result.severityLevel}, and it affects ${result.affectedEntities.length} entities:`;
|
|
@@ -192,14 +194,14 @@ const main = async () => {
|
|
|
192
194
|
tool('find_entity_by_name', 'Get the entityId of a monitored entity based on the name of the entity on Dynatrace', {
|
|
193
195
|
entityName: zod_1.z.string(),
|
|
194
196
|
}, async ({ entityName }) => {
|
|
195
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
197
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
196
198
|
const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
|
|
197
199
|
return entityResponse;
|
|
198
200
|
});
|
|
199
201
|
tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
|
|
200
202
|
entityId: zod_1.z.string().optional(),
|
|
201
203
|
}, async ({ entityId }) => {
|
|
202
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
204
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
203
205
|
const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
|
|
204
206
|
let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
|
|
205
207
|
`Properties: ${JSON.stringify(entityDetails.properties)}\n`;
|
|
@@ -221,21 +223,21 @@ const main = async () => {
|
|
|
221
223
|
channel: zod_1.z.string().optional(),
|
|
222
224
|
message: zod_1.z.string().optional(),
|
|
223
225
|
}, async ({ channel, message }) => {
|
|
224
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
226
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
225
227
|
const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
|
|
226
228
|
return `Message sent to Slack channel: ${JSON.stringify(response)}`;
|
|
227
229
|
});
|
|
228
230
|
tool('get_logs_for_entity', 'Get Logs for a monitored entity based on name of the entity on Dynatrace', {
|
|
229
231
|
entityName: zod_1.z.string().optional(),
|
|
230
232
|
}, async ({ entityName }) => {
|
|
231
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
233
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:logs:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
232
234
|
const logs = await (0, get_logs_for_entity_1.getLogsForEntity)(dtClient, entityName);
|
|
233
235
|
return `Logs:\n${JSON.stringify(logs?.map((logLine) => (logLine ? logLine.content : 'Empty log')))}`;
|
|
234
236
|
});
|
|
235
237
|
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
238
|
dqlStatement: zod_1.z.string(),
|
|
237
239
|
}, async ({ dqlStatement }) => {
|
|
238
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
240
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
239
241
|
const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
|
|
240
242
|
let resp = 'DQL Statement Verification:\n';
|
|
241
243
|
if (response.notifications && response.notifications.length > 0) {
|
|
@@ -255,7 +257,7 @@ const main = async () => {
|
|
|
255
257
|
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
258
|
dqlStatement: zod_1.z.string(),
|
|
257
259
|
}, async ({ dqlStatement }) => {
|
|
258
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
260
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
|
|
259
261
|
'storage:logs:read', // Read logs for reliability guardian validations
|
|
260
262
|
'storage:metrics:read', // Read metrics for reliability guardian validations
|
|
261
263
|
'storage:bizevents:read', // Read bizevents for reliability guardian validations
|
|
@@ -265,17 +267,102 @@ const main = async () => {
|
|
|
265
267
|
'storage:system:read', // Read System Data from Grail
|
|
266
268
|
'storage:user.events:read', // Read User events from Grail
|
|
267
269
|
'storage:user.sessions:read', // Read User sessions from Grail
|
|
268
|
-
'storage:security.events:read'));
|
|
270
|
+
'storage:security.events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
269
271
|
const response = await (0, execute_dql_1.executeDql)(dtClient, dqlStatement);
|
|
270
272
|
return `DQL Response: ${JSON.stringify(response)}`;
|
|
271
273
|
});
|
|
274
|
+
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.", {
|
|
275
|
+
text: zod_1.z
|
|
276
|
+
.string()
|
|
277
|
+
.describe('Natural language description of what you want to query. Be specific and include time ranges, entities, and metrics of interest.'),
|
|
278
|
+
}, async ({ text }) => {
|
|
279
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:nl2dql:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
280
|
+
const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
|
|
281
|
+
let resp = `🔤 Natural Language to DQL:\n\n`;
|
|
282
|
+
resp += `**Query:** "${text}"\n\n`;
|
|
283
|
+
resp += `**Generated DQL:**\n\`\`\`\n${response.dql}\n\`\`\`\n\n`;
|
|
284
|
+
resp += `**Status:** ${response.status}\n`;
|
|
285
|
+
resp += `**Message Token:** ${response.messageToken}\n`;
|
|
286
|
+
if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
|
|
287
|
+
resp += `\n**Notifications:**\n`;
|
|
288
|
+
response.metadata.notifications.forEach((notification) => {
|
|
289
|
+
resp += `- ${notification.severity}: ${notification.message}\n`;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
resp += `\n💡 **Next Steps:**\n`;
|
|
293
|
+
resp += `1. Use "verify_dql" tool to validate this query\n`;
|
|
294
|
+
resp += `2. Use "execute_dql" tool to run the query\n`;
|
|
295
|
+
resp += `3. If results don't match expectations, refine your natural language description and try again\n`;
|
|
296
|
+
return resp;
|
|
297
|
+
});
|
|
298
|
+
tool('explain_dql_in_natural_language', 'Explain Dynatrace Query Language (DQL) statements in natural language using Davis CoPilot AI.', {
|
|
299
|
+
dql: zod_1.z.string().describe('The DQL statement to explain'),
|
|
300
|
+
}, async ({ dql }) => {
|
|
301
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:dql2nl:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
302
|
+
const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
|
|
303
|
+
let resp = `📝 DQL to Natural Language:\n\n`;
|
|
304
|
+
resp += `**DQL Query:**\n\`\`\`\n${dql}\n\`\`\`\n\n`;
|
|
305
|
+
resp += `**Summary:** ${response.summary}\n\n`;
|
|
306
|
+
resp += `**Detailed Explanation:**\n${response.explanation}\n\n`;
|
|
307
|
+
resp += `**Status:** ${response.status}\n`;
|
|
308
|
+
resp += `**Message Token:** ${response.messageToken}\n`;
|
|
309
|
+
if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
|
|
310
|
+
resp += `\n**Notifications:**\n`;
|
|
311
|
+
response.metadata.notifications.forEach((notification) => {
|
|
312
|
+
resp += `- ${notification.severity}: ${notification.message}\n`;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return resp;
|
|
316
|
+
});
|
|
317
|
+
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)*', {
|
|
318
|
+
text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
|
|
319
|
+
context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
|
|
320
|
+
instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
|
|
321
|
+
}, async ({ text, context, instruction }) => {
|
|
322
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:conversations:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
323
|
+
const conversationContext = [];
|
|
324
|
+
if (context) {
|
|
325
|
+
conversationContext.push({
|
|
326
|
+
type: 'supplementary',
|
|
327
|
+
value: context,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (instruction) {
|
|
331
|
+
conversationContext.push({
|
|
332
|
+
type: 'instruction',
|
|
333
|
+
value: instruction,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const response = await (0, davis_copilot_1.chatWithDavisCopilot)(dtClient, text, conversationContext);
|
|
337
|
+
let resp = `🤖 Davis CoPilot Response:\n\n`;
|
|
338
|
+
resp += `**Your Question:** "${text}"\n\n`;
|
|
339
|
+
resp += `**Answer:**\n${response.text}\n\n`;
|
|
340
|
+
resp += `**Status:** ${response.status}\n`;
|
|
341
|
+
resp += `**Message Token:** ${response.messageToken}\n`;
|
|
342
|
+
if (response.metadata?.sources && response.metadata.sources.length > 0) {
|
|
343
|
+
resp += `\n**Sources:**\n`;
|
|
344
|
+
response.metadata.sources.forEach((source) => {
|
|
345
|
+
resp += `- ${source.title || 'Untitled'}: ${source.url || 'No URL'}\n`;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (response.metadata?.notifications && response.metadata.notifications.length > 0) {
|
|
349
|
+
resp += `\n**Notifications:**\n`;
|
|
350
|
+
response.metadata.notifications.forEach((notification) => {
|
|
351
|
+
resp += `- ${notification.severity}: ${notification.message}\n`;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (response.state?.conversationId) {
|
|
355
|
+
resp += `\n**Conversation ID:** ${response.state.conversationId}`;
|
|
356
|
+
}
|
|
357
|
+
return resp;
|
|
358
|
+
});
|
|
272
359
|
tool('create_workflow_for_notification', 'Create a notification for a team based on a problem type within Workflows in Dynatrace', {
|
|
273
360
|
problemType: zod_1.z.string().optional(),
|
|
274
361
|
teamName: zod_1.z.string().optional(),
|
|
275
362
|
channel: zod_1.z.string().optional(),
|
|
276
363
|
isPrivate: zod_1.z.boolean().optional().default(false),
|
|
277
364
|
}, async ({ problemType, teamName, channel, isPrivate }) => {
|
|
278
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
365
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
279
366
|
const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
|
|
280
367
|
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
368
|
if (response.type == 'SIMPLE') {
|
|
@@ -292,7 +379,7 @@ const main = async () => {
|
|
|
292
379
|
tool('make_workflow_public', 'Modify a workflow and make it publicly available to everyone on the Dynatrace Environment', {
|
|
293
380
|
workflowId: zod_1.z.string().optional(),
|
|
294
381
|
}, async ({ workflowId }) => {
|
|
295
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
382
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
296
383
|
const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
|
|
297
384
|
isPrivate: false,
|
|
298
385
|
});
|
|
@@ -304,14 +391,14 @@ const main = async () => {
|
|
|
304
391
|
.optional()
|
|
305
392
|
.describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`),
|
|
306
393
|
}, async ({ clusterId }) => {
|
|
307
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
394
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
308
395
|
const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
|
|
309
396
|
return `Kubernetes Events:\n${JSON.stringify(events)}`;
|
|
310
397
|
});
|
|
311
398
|
tool('get_ownership', 'Get detailed Ownership information for one or multiple entities on Dynatrace', {
|
|
312
399
|
entityIds: zod_1.z.string().optional().describe('Comma separated list of entityIds'),
|
|
313
400
|
}, async ({ entityIds }) => {
|
|
314
|
-
const dtClient = await (0, dynatrace_clients_1.
|
|
401
|
+
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
315
402
|
console.error(`Fetching ownership for ${entityIds}`);
|
|
316
403
|
const ownershipInformation = await (0, get_ownership_information_1.getOwnershipInformation)(dtClient, entityIds);
|
|
317
404
|
console.error(`Done!`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynatrace-oss/dynatrace-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-rc.1+build1",
|
|
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
|
},
|