@dynatrace-oss/dynatrace-mcp-server 0.6.0-rc.2 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +71 -54
  2. package/dist/index.js +67 -5
  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
 
@@ -261,6 +277,43 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
261
277
 
262
278
  This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
263
279
 
280
+ **Google Gemini CLI**
281
+
282
+ The [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) is Google's official command-line AI assistant that supports MCP server integration. You can add the Dynatrace MCP server using either the built-in management commands or manual configuration.
283
+
284
+ Using `gemini` CLI directly (recommended):
285
+
286
+ ```bash
287
+ gemini extensions install https://github.com/dynatrace-oss/dynatrace-mcp
288
+ export DT_PLATFORM_TOKEN=...
289
+ export DT_ENVIRONMENT=https://...
290
+ ```
291
+
292
+ and verify that the server is running via
293
+
294
+ ```bash
295
+ gemini mcp list
296
+ ```
297
+
298
+ Or manually in your `~/.gemini/settings.json` or `.gemini/settings.json`:
299
+
300
+ ```json
301
+ {
302
+ "mcpServers": {
303
+ "dynatrace": {
304
+ "command": "npx",
305
+ "args": ["@dynatrace-oss/dynatrace-mcp-server@latest"],
306
+ "env": {
307
+ "DT_PLATFORM_TOKEN": "",
308
+ "DT_ENVIRONMENT": ""
309
+ },
310
+ "timeout": 30000,
311
+ "trust": false
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
264
317
  ### HTTP Server Mode (Alternative)
265
318
 
266
319
  For scenarios where you need to run the MCP server as an HTTP service instead of using stdio (e.g., for stateful sessions, load balancing, or integration with web clients), you can use the HTTP server mode:
@@ -328,11 +381,11 @@ For fetching just error-logs, add `| filter loglevel == "ERROR"`.
328
381
 
329
382
  You can set up authentication via **Platform Tokens** (recommended) or **OAuth Client** via the following environment variables:
330
383
 
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`)
384
+ - `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
385
  - `DT_PLATFORM_TOKEN` (string, e.g., `dt0s16.SAMPLE.abcd1234`) - **Recommended**: Dynatrace Platform Token
333
386
  - `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Alternative: Dynatrace OAuth Client ID (for advanced use cases)
334
387
  - `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.
388
+ - `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
389
 
337
390
  **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
391
 
@@ -384,6 +437,18 @@ and extend them as needed. They're here to help you imagine how real-time observ
384
437
 
385
438
  ### **Basic Queries & AI Assistance**
386
439
 
440
+ **Find a monitored entity**
441
+
442
+ ```
443
+ Get all details of the entity 'my-service'
444
+ ```
445
+
446
+ **Find error logs**
447
+
448
+ ```
449
+ Show me error logs
450
+ ```
451
+
387
452
  **Write a DQL query from natural language:**
388
453
 
389
454
  ```
@@ -608,51 +673,3 @@ To disable usage tracking, add this to your environment:
608
673
  ```bash
609
674
  DT_MCP_DISABLE_TELEMETRY=true
610
675
  ```
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
@@ -27,7 +27,20 @@ const davis_copilot_1 = require("./capabilities/davis-copilot");
27
27
  const getDynatraceEnv_1 = require("./getDynatraceEnv");
28
28
  const telemetry_openkit_1 = require("./utils/telemetry-openkit");
29
29
  const grail_budget_tracker_1 = require("./utils/grail-budget-tracker");
30
- (0, dotenv_1.config)();
30
+ // Load environment variables from .env file if available, and suppress warnings/logging to stdio
31
+ // as it breaks MCP communication when using stdio transport
32
+ const dotEnvOutput = (0, dotenv_1.config)({ quiet: true });
33
+ if (dotEnvOutput.error) {
34
+ // Only log error if it's not about missing .env file
35
+ if (dotEnvOutput.error.code !== 'ENOENT') {
36
+ console.error('Error loading .env file:', dotEnvOutput.error);
37
+ process.exit(1);
38
+ }
39
+ }
40
+ else {
41
+ // Successfully loaded .env file
42
+ console.error(`.env file loaded successfully - loaded ${dotEnvOutput.parsed ? Object.keys(dotEnvOutput.parsed).length : 0} environment variables: ${Object.keys(dotEnvOutput.parsed || {}).join(', ')}`);
43
+ }
31
44
  let scopesBase = [
32
45
  'app-engine:apps:run', // needed for environmentInformationClient
33
46
  'app-engine:functions:run', // needed for environmentInformationClient
@@ -129,7 +142,7 @@ const main = async () => {
129
142
  },
130
143
  });
131
144
  // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
132
- const tool = (name, description, paramsSchema, cb) => {
145
+ const tool = (name, description, paramsSchema, annotations, cb) => {
133
146
  const wrappedCb = async (args) => {
134
147
  // track starttime for telemetry
135
148
  const startTime = Date.now();
@@ -168,10 +181,12 @@ const main = async () => {
168
181
  .catch((e) => console.warn('Failed to track tool usage:', e));
169
182
  }
170
183
  };
171
- server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
184
+ server.tool(name, description, paramsSchema, annotations, (args) => wrappedCb(args));
172
185
  };
173
186
  /** Tool Definitions below */
174
- tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, async ({}) => {
187
+ tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, {
188
+ readOnlyHint: true,
189
+ }, async ({}) => {
175
190
  // create an oauth-client
176
191
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
177
192
  const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
@@ -195,6 +210,8 @@ const main = async () => {
195
210
  .number()
196
211
  .default(25)
197
212
  .describe('Maximum number of vulnerabilities to display in the response.'),
213
+ }, {
214
+ readOnlyHint: true,
198
215
  }, async ({ riskScore, additionalFilter, maxVulnerabilitiesToDisplay }) => {
199
216
  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
217
  const result = await (0, list_vulnerabilities_1.listVulnerabilities)(dtClient, additionalFilter, riskScore);
@@ -236,6 +253,8 @@ const main = async () => {
236
253
  .optional()
237
254
  .describe('Additional filter for DQL statement for dt.davis.problems, e.g., \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\''),
238
255
  maxProblemsToDisplay: zod_1.z.number().default(10).describe('Maximum number of problems to display in the response.'),
256
+ }, {
257
+ readOnlyHint: true,
239
258
  }, async ({ additionalFilter, maxProblemsToDisplay }) => {
240
259
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read', 'storage:buckets:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
241
260
  // get problems (uses fetch)
@@ -270,6 +289,8 @@ const main = async () => {
270
289
  });
271
290
  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
291
  entityName: zod_1.z.string().describe('Name of the entity to search for, e.g., "my-service" or "my-host"'),
292
+ }, {
293
+ readOnlyHint: true,
273
294
  }, async ({ entityName }) => {
274
295
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
275
296
  const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
@@ -277,6 +298,8 @@ const main = async () => {
277
298
  });
278
299
  tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
279
300
  entityId: zod_1.z.string().optional(),
301
+ }, {
302
+ readOnlyHint: true,
280
303
  }, async ({ entityId }) => {
281
304
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
282
305
  const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
@@ -314,6 +337,9 @@ const main = async () => {
314
337
  tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
315
338
  channel: zod_1.z.string().optional(),
316
339
  message: zod_1.z.string().optional(),
340
+ }, {
341
+ // not read-only, not open-world, not destructive
342
+ readOnlyHint: false,
317
343
  }, async ({ channel, message }) => {
318
344
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
319
345
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
@@ -321,6 +347,9 @@ const main = async () => {
321
347
  });
322
348
  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
349
  dqlStatement: zod_1.z.string(),
350
+ }, {
351
+ readOnlyHint: true,
352
+ idempotentHint: true, // same input always yields same output
324
353
  }, async ({ dqlStatement }) => {
325
354
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
326
355
  const response = await (0, execute_dql_1.verifyDqlStatement)(dtClient, dqlStatement);
@@ -345,6 +374,12 @@ const main = async () => {
345
374
  dqlStatement: zod_1.z
346
375
  .string()
347
376
  .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) }")'),
377
+ }, {
378
+ // not readonly (DQL statements may modify things), not idempotent (may change over time)
379
+ readOnlyHint: false,
380
+ idempotentHint: false,
381
+ // 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
382
+ openWorldHint: true,
348
383
  }, async ({ dqlStatement }) => {
349
384
  // Create a HTTP Client that has all storage:*:read scopes
350
385
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
@@ -404,6 +439,9 @@ const main = async () => {
404
439
  text: zod_1.z
405
440
  .string()
406
441
  .describe('Natural language description of what you want to query. Be specific and include time ranges, entities, and metrics of interest.'),
442
+ }, {
443
+ readOnlyHint: true,
444
+ idempotentHint: true,
407
445
  }, async ({ text }) => {
408
446
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:nl2dql:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
409
447
  const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
@@ -430,6 +468,9 @@ const main = async () => {
430
468
  });
431
469
  tool('explain_dql_in_natural_language', 'Explain Dynatrace Query Language (DQL) statements in natural language using Davis CoPilot AI.', {
432
470
  dql: zod_1.z.string().describe('The DQL statement to explain'),
471
+ }, {
472
+ readOnlyHint: true,
473
+ idempotentHint: true,
433
474
  }, async ({ dql }) => {
434
475
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:dql2nl:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
435
476
  const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
@@ -451,6 +492,10 @@ const main = async () => {
451
492
  text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
452
493
  context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
453
494
  instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
495
+ }, {
496
+ readOnlyHint: true,
497
+ idempotentHint: true,
498
+ openWorldHint: true, // web-search like characteristics
454
499
  }, async ({ text, context, instruction }) => {
455
500
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:conversations:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
456
501
  const conversationContext = [];
@@ -500,6 +545,10 @@ const main = async () => {
500
545
  teamName: zod_1.z.string().optional(),
501
546
  channel: zod_1.z.string().optional(),
502
547
  isPrivate: zod_1.z.boolean().optional().default(false),
548
+ }, {
549
+ // not read only, not idempotent
550
+ readOnlyHint: false,
551
+ idempotentHint: false, // creating the same workflow multiple times is possible
503
552
  }, async ({ problemType, teamName, channel, isPrivate }) => {
504
553
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
505
554
  const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
@@ -517,6 +566,10 @@ const main = async () => {
517
566
  });
518
567
  tool('make_workflow_public', 'Modify a workflow and make it publicly available to everyone on the Dynatrace Environment', {
519
568
  workflowId: zod_1.z.string().optional(),
569
+ }, {
570
+ // not read only, but idempotent
571
+ readOnlyHint: false,
572
+ idempotentHint: true, // making the same workflow public multiple times yields the same result
520
573
  }, async ({ workflowId }) => {
521
574
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
522
575
  const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
@@ -529,6 +582,8 @@ const main = async () => {
529
582
  .string()
530
583
  .optional()
531
584
  .describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`),
585
+ }, {
586
+ readOnlyHint: true,
532
587
  }, async ({ clusterId }) => {
533
588
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:events:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
534
589
  const events = await (0, get_events_for_cluster_1.getEventsForCluster)(dtClient, clusterId);
@@ -536,6 +591,8 @@ const main = async () => {
536
591
  });
537
592
  tool('get_ownership', 'Get detailed Ownership information for one or multiple entities on Dynatrace', {
538
593
  entityIds: zod_1.z.string().optional().describe('Comma separated list of entityIds'),
594
+ }, {
595
+ readOnlyHint: true,
539
596
  }, async ({ entityIds }) => {
540
597
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
541
598
  console.error(`Fetching ownership for ${entityIds}`);
@@ -545,7 +602,10 @@ const main = async () => {
545
602
  resp += JSON.stringify(ownershipInformation);
546
603
  return resp;
547
604
  });
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 ({}) => {
605
+ 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.', {}, {
606
+ readonlyHint: false, // modifies state
607
+ idempotentHint: true, // multiple resets yield the same result
608
+ }, async ({}) => {
549
609
  // Reset the global tracker
550
610
  (0, grail_budget_tracker_1.resetGrailBudgetTracker)();
551
611
  // Get a fresh tracker to show the reset state
@@ -572,6 +632,8 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
572
632
  bccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for BCC recipients'),
573
633
  subject: zod_1.z.string().describe('Subject line of the email'),
574
634
  body: zod_1.z.string().describe('Body content of the email (plain text only)'),
635
+ }, {
636
+ openWorldHint: true, // email is as close to the open-world as we can get with our system
575
637
  }, async ({ toRecipients, ccRecipients, bccRecipients, subject, body }) => {
576
638
  // Validate total recipients limit (10 max across TO, CC, and BCC)
577
639
  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.1",
4
4
  "mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Dynatrace",
6
6
  "keywords": [