@dynatrace-oss/dynatrace-mcp-server 0.6.0-rc.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +34 -54
  2. package/dist/index.js +53 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,12 +18,28 @@
18
18
  </a>
19
19
  </h4>
20
20
 
21
- This local MCP server allows interaction with the [Dynatrace](https://www.dynatrace.com/) observability platform.
22
- Bring real-time observability data directly into your development workflow.
21
+ The local _Dynatrace MCP server_ allows AI Assistants to interact with the [Dynatrace](https://www.dynatrace.com/) observability platform,
22
+ bringing real-time observability data directly into your development workflow.
23
23
 
24
24
  > Note: This product is not officially supported by Dynatrace.
25
25
 
26
- Please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
26
+ If you need help, please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help.
27
+
28
+ ## Quickstart
29
+
30
+ You can add this MCP server to your MCP Client like VSCode, Claude, Cursor, Amazon Q, Windsurf, ChatGPT, or Github Copilot via the npmjs package `@dynatrace-oss/dynatrace-mcp-server`, and type `stdio`.
31
+ You can find more details about the configuration for different AI Assistants, Agents and MCP Clients in the [Configuration section below](#configuration).
32
+
33
+ Furthermore, you need your Dynatrace environment URL, e.g., `https://abc12345.apps.dynatrace.com`, as well as a [Platform Token](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/platform-tokens), e.g., `dt0s16.SAMPLE.abcd1234`, with [required scopes](#scopes-for-authentication).
34
+
35
+ Depending on your MCP Client, you need to configure these as environment variables or as settings in the UI:
36
+
37
+ - `DT_ENVIRONMENT` (string, e.g., `https://abc12345.apps.dynatrace.com`) - URL to your Dynatrace Platform (do not use Dynatrace classic URLs like `abc12345.live.dynatrace.com`)
38
+ - `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
39
+
40
+ Once you are done, we recommend looking into [example prompts](#-example-prompts-), like `Get all details of the entity 'my-service'` or `Show me error logs`. Please mind that these prompts lead to executing DQL statements which may incur [costs](#costs) in accordance to your licence.
41
+
42
+ ## Architecture
27
43
 
28
44
  ![Architecture](https://github.com/dynatrace-oss/dynatrace-mcp/blob/main/assets/dynatrace-mcp-arch.png?raw=true)
29
45
 
@@ -184,7 +200,7 @@ rules/
184
200
 
185
201
  For detailed information about the workshop rules, see the [Rules README](./dynatrace-agent-rules/rules/README.md).
186
202
 
187
- ## Quickstart
203
+ ## Configuration
188
204
 
189
205
  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`.
190
206
 
@@ -328,11 +344,11 @@ For fetching just error-logs, add `| filter loglevel == "ERROR"`.
328
344
 
329
345
  You can set up authentication via **Platform Tokens** (recommended) or **OAuth Client** via the following environment variables:
330
346
 
331
- - `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`)
347
+ - `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`)
332
348
  - `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
333
349
  - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Alternative: Dynatrace OAuth Client ID (for advanced use cases)
334
350
  - `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Alternative: Dynatrace OAuth Client Secret (for advanced use cases)
335
- - `DT_GRAIL_QUERY_BUDGET_GB` (number, default: 1000) - Budget limit in GB (base 1000) for Grail query bytes scanned per session. The MCP server tracks your Grail usage and warns when approaching or exceeding this limit.
351
+ - `DT_GRAIL_QUERY_BUDGET_GB` (number, default: `1000`) - Budget limit in GB (base 1000) for Grail query bytes scanned per session. The MCP server tracks your Grail usage and warns when approaching or exceeding this limit.
336
352
 
337
353
  **Platform Tokens are recommended** for most use cases as they provide a simpler authentication flow. OAuth Clients should only be used when specific OAuth features are required.
338
354
 
@@ -384,6 +400,18 @@ and extend them as needed. They're here to help you imagine how real-time observ
384
400
 
385
401
  ### **Basic Queries & AI Assistance**
386
402
 
403
+ **Find a monitored entity**
404
+
405
+ ```
406
+ Get all details of the entity 'my-service'
407
+ ```
408
+
409
+ **Find error logs**
410
+
411
+ ```
412
+ Show me error logs
413
+ ```
414
+
387
415
  **Write a DQL query from natural language:**
388
416
 
389
417
  ```
@@ -608,51 +636,3 @@ To disable usage tracking, add this to your environment:
608
636
  ```bash
609
637
  DT_MCP_DISABLE_TELEMETRY=true
610
638
  ```
611
-
612
- ## Development
613
-
614
- For local development purposes, you can use VSCode and GitHub Copilot.
615
-
616
- First, enable Copilot for your Workspace `.vscode/settings.json`:
617
-
618
- ```json
619
- {
620
- "github.copilot.enable": {
621
- "*": true
622
- }
623
- }
624
- ```
625
-
626
- and make sure that you are using Agent Mode in Copilot.
627
-
628
- Second, add the MCP to `.vscode/mcp.json`:
629
-
630
- ```json
631
- {
632
- "servers": {
633
- "my-dynatrace-mcp-server": {
634
- "command": "node",
635
- "args": ["--watch", "${workspaceFolder}/dist/index.js"],
636
- "envFile": "${workspaceFolder}/.env"
637
- }
638
- }
639
- }
640
- ```
641
-
642
- Third, create a `.env` file in this repository (you can copy from `.env.template`) and configure environment variables as [described above](#environment-variables).
643
-
644
- Finally, make changes to your code and compile it with `npm run build` or just run `npm run watch` and it auto-compiles.
645
-
646
- ## Releasing
647
-
648
- When you are preparing for a release, you can use GitHub Copilot to guide you through the preparations.
649
-
650
- In Visual Studio Code, you can use `/release` in the chat with Copilot in Agent Mode, which will execute [release.prompt.md](.github/prompts/release.prompt.md).
651
-
652
- You may include additional information such as the version number. If not specified, you will be asked.
653
-
654
- This will
655
-
656
- - prepare the [changelog](CHANGELOG.md),
657
- - update the version number in [package.json](package.json),
658
- - commit the changes.
package/dist/index.js CHANGED
@@ -129,7 +129,7 @@ const main = async () => {
129
129
  },
130
130
  });
131
131
  // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
132
- const tool = (name, description, paramsSchema, cb) => {
132
+ const tool = (name, description, paramsSchema, annotations, cb) => {
133
133
  const wrappedCb = async (args) => {
134
134
  // track starttime for telemetry
135
135
  const startTime = Date.now();
@@ -168,10 +168,12 @@ const main = async () => {
168
168
  .catch((e) => console.warn('Failed to track tool usage:', e));
169
169
  }
170
170
  };
171
- server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
171
+ server.tool(name, description, paramsSchema, annotations, (args) => wrappedCb(args));
172
172
  };
173
173
  /** Tool Definitions below */
174
- tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, async ({}) => {
174
+ tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, {
175
+ readOnlyHint: true,
176
+ }, async ({}) => {
175
177
  // create an oauth-client
176
178
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
177
179
  const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
@@ -195,6 +197,8 @@ const main = async () => {
195
197
  .number()
196
198
  .default(25)
197
199
  .describe('Maximum number of vulnerabilities to display in the response.'),
200
+ }, {
201
+ readOnlyHint: true,
198
202
  }, async ({ riskScore, additionalFilter, maxVulnerabilitiesToDisplay }) => {
199
203
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read', 'storage:buckets:read', 'storage:security.events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
200
204
  const result = await (0, list_vulnerabilities_1.listVulnerabilities)(dtClient, additionalFilter, riskScore);
@@ -236,6 +240,8 @@ const main = async () => {
236
240
  .optional()
237
241
  .describe('Additional filter for DQL statement for dt.davis.problems, e.g., \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\''),
238
242
  maxProblemsToDisplay: zod_1.z.number().default(10).describe('Maximum number of problems to display in the response.'),
243
+ }, {
244
+ readOnlyHint: true,
239
245
  }, async ({ additionalFilter, maxProblemsToDisplay }) => {
240
246
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read', 'storage:buckets:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
241
247
  // get problems (uses fetch)
@@ -270,6 +276,8 @@ const main = async () => {
270
276
  });
271
277
  tool('find_entity_by_name', 'Get the entityId of a monitored entity (service, host, process-group, application, kubernetes-node, ...) within the topology based on the name of the entity on Dynatrace', {
272
278
  entityName: zod_1.z.string().describe('Name of the entity to search for, e.g., "my-service" or "my-host"'),
279
+ }, {
280
+ readOnlyHint: true,
273
281
  }, async ({ entityName }) => {
274
282
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
275
283
  const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
@@ -277,6 +285,8 @@ const main = async () => {
277
285
  });
278
286
  tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
279
287
  entityId: zod_1.z.string().optional(),
288
+ }, {
289
+ readOnlyHint: true,
280
290
  }, async ({ entityId }) => {
281
291
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
282
292
  const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
@@ -314,6 +324,9 @@ const main = async () => {
314
324
  tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
315
325
  channel: zod_1.z.string().optional(),
316
326
  message: zod_1.z.string().optional(),
327
+ }, {
328
+ // not read-only, not open-world, not destructive
329
+ readOnlyHint: false,
317
330
  }, async ({ channel, message }) => {
318
331
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
319
332
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
@@ -321,6 +334,9 @@ const main = async () => {
321
334
  });
322
335
  tool('verify_dql', 'Verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. This step is recommended for DQL statements that have been dynamically created by non-expert tools. For statements coming from the `generate_dql_from_natural_language` tool as well as from documentation, this step can be omitted.', {
323
336
  dqlStatement: zod_1.z.string(),
337
+ }, {
338
+ readOnlyHint: true,
339
+ idempotentHint: true, // same input always yields same output
324
340
  }, async ({ dqlStatement }) => {
325
341
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
326
342
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
@@ -345,6 +361,12 @@ const main = async () => {
345
361
  dqlStatement: zod_1.z
346
362
  .string()
347
363
  .describe('DQL Statement (Ex: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}.", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }")'),
364
+ }, {
365
+ // not readonly (DQL statements may modify things), not idempotent (may change over time)
366
+ readOnlyHint: false,
367
+ idempotentHint: false,
368
+ // while we are not strictly talking to the open world here, the response from execute DQL could interpreted as a web-search, which often is referred to open-world
369
+ openWorldHint: true,
348
370
  }, async ({ dqlStatement }) => {
349
371
  // Create a HTTP Client that has all storage:*:read scopes
350
372
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
@@ -404,6 +426,9 @@ const main = async () => {
404
426
  text: zod_1.z
405
427
  .string()
406
428
  .describe('Natural language description of what you want to query. Be specific and include time ranges, entities, and metrics of interest.'),
429
+ }, {
430
+ readOnlyHint: true,
431
+ idempotentHint: true,
407
432
  }, async ({ text }) => {
408
433
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:nl2dql:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
409
434
  const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
@@ -430,6 +455,9 @@ const main = async () => {
430
455
  });
431
456
  tool('explain_dql_in_natural_language', 'Explain Dynatrace Query Language (DQL) statements in natural language using Davis CoPilot AI.', {
432
457
  dql: zod_1.z.string().describe('The DQL statement to explain'),
458
+ }, {
459
+ readOnlyHint: true,
460
+ idempotentHint: true,
433
461
  }, async ({ dql }) => {
434
462
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:dql2nl:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
435
463
  const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
@@ -451,6 +479,10 @@ const main = async () => {
451
479
  text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
452
480
  context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
453
481
  instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
482
+ }, {
483
+ readOnlyHint: true,
484
+ idempotentHint: true,
485
+ openWorldHint: true, // web-search like characteristics
454
486
  }, async ({ text, context, instruction }) => {
455
487
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:conversations:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
456
488
  const conversationContext = [];
@@ -500,6 +532,10 @@ const main = async () => {
500
532
  teamName: zod_1.z.string().optional(),
501
533
  channel: zod_1.z.string().optional(),
502
534
  isPrivate: zod_1.z.boolean().optional().default(false),
535
+ }, {
536
+ // not read only, not idempotent
537
+ readOnlyHint: false,
538
+ idempotentHint: false, // creating the same workflow multiple times is possible
503
539
  }, async ({ problemType, teamName, channel, isPrivate }) => {
504
540
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
505
541
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
@@ -517,6 +553,10 @@ const main = async () => {
517
553
  });
518
554
  tool('make_workflow_public', 'Modify a workflow and make it publicly available to everyone on the Dynatrace Environment', {
519
555
  workflowId: zod_1.z.string().optional(),
556
+ }, {
557
+ // not read only, but idempotent
558
+ readOnlyHint: false,
559
+ idempotentHint: true, // making the same workflow public multiple times yields the same result
520
560
  }, async ({ workflowId }) => {
521
561
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
522
562
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
@@ -529,6 +569,8 @@ const main = async () => {
529
569
  .string()
530
570
  .optional()
531
571
  .describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`),
572
+ }, {
573
+ readOnlyHint: true,
532
574
  }, async ({ clusterId }) => {
533
575
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
534
576
  const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
@@ -536,6 +578,8 @@ const main = async () => {
536
578
  });
537
579
  tool('get_ownership', 'Get detailed Ownership information for one or multiple entities on Dynatrace', {
538
580
  entityIds: zod_1.z.string().optional().describe('Comma separated list of entityIds'),
581
+ }, {
582
+ readOnlyHint: true,
539
583
  }, async ({ entityIds }) => {
540
584
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
541
585
  console.error(`Fetching ownership for ${entityIds}`);
@@ -545,7 +589,10 @@ const main = async () => {
545
589
  resp += JSON.stringify(ownershipInformation);
546
590
  return resp;
547
591
  });
548
- tool('reset_grail_budget', 'Reset the Grail query budget after it was exhausted, allowing new queries to be executed. This clears all tracked bytes scanned in the current session.', {}, async ({}) => {
592
+ tool('reset_grail_budget', 'Reset the Grail query budget after it was exhausted, allowing new queries to be executed. This clears all tracked bytes scanned in the current session.', {}, {
593
+ readonlyHint: false, // modifies state
594
+ idempotentHint: true, // multiple resets yield the same result
595
+ }, async ({}) => {
549
596
  // Reset the global tracker
550
597
  (0, grail_budget_tracker_1.resetGrailBudgetTracker)();
551
598
  // Get a fresh tracker to show the reset state
@@ -572,6 +619,8 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
572
619
  bccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for BCC recipients'),
573
620
  subject: zod_1.z.string().describe('Subject line of the email'),
574
621
  body: zod_1.z.string().describe('Body content of the email (plain text only)'),
622
+ }, {
623
+ openWorldHint: true, // email is as close to the open-world as we can get with our system
575
624
  }, async ({ toRecipients, ccRecipients, bccRecipients, subject, body }) => {
576
625
  // Validate total recipients limit (10 max across TO, CC, and BCC)
577
626
  const totalRecipients = toRecipients.length + (ccRecipients?.length || 0) + (bccRecipients?.length || 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.6.0-rc.2",
3
+ "version": "0.6.0",
4
4
  "mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Dynatrace",
6
6
  "keywords": [