@dynatrace-oss/dynatrace-mcp-server 0.5.0-rc.3 → 0.5.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.
package/README.md CHANGED
@@ -11,6 +11,11 @@ Bring real-time observability data directly into your development workflow.
11
11
  - **Contextual debugging** - Fix issues with full context from monitored exceptions, logs, and anomalies
12
12
  - **Security insights** - Get detailed vulnerability analysis and security problem tracking
13
13
  - **Natural language queries** - Use AI-powered DQL generation and explanation
14
+ - **Multi-phase incident investigation** - Systematic 4-phase approach with automated impact assessment
15
+ - **Advanced transaction analysis** - Precise root cause identification with file/line-level accuracy
16
+ - **Cross-data source correlation** - Connect problems → spans → logs with trace ID correlation
17
+ - **DevOps automation** - Deployment health gates with automated promotion/rollback logic
18
+ - **Security compliance monitoring** - Multi-cloud compliance assessment with evidence-based investigation
14
19
 
15
20
  ## Capabilities
16
21
 
@@ -22,6 +27,20 @@ Bring real-time observability data directly into your development workflow.
22
27
  - Get more information about a monitored entity
23
28
  - Get Ownership of an entity
24
29
 
30
+ ## Costs
31
+
32
+ **Important:** While this local MCP server is provided for free, using it to access data in Dynatrace Grail may incur additional costs based
33
+ on your Dynatrace consumption model. This affects `execute_dql` tool and other capabilities that **query** Dynatrace Grail storage, and costs
34
+ depend on the volume (GB scanned/billed).
35
+
36
+ **Before using this MCP server extensively, please:**
37
+
38
+ 1. Review your current Dynatrace consumption model and pricing
39
+ 2. Understand the cost implications of the specific data you plan to query (logs, events, metrics) - see [Dynatrace Pricing and Rate Card](https://www.dynatrace.com/pricing/)
40
+ 3. Start with smaller timeframes (e.g., 12h-24h) and make use of [buckets](https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model#built-in-grail-buckets) to reduce the cost impact
41
+
42
+ **Note**: We will be providing a way to monitor Query Usage of the dynatrace-mcp-server in the future.
43
+
25
44
  ### AI-Powered Assistance (Preview)
26
45
 
27
46
  - **Natural Language to DQL** - Convert plain English queries to Dynatrace Query Language
@@ -31,6 +50,97 @@ Bring real-time observability data directly into your development workflow.
31
50
 
32
51
  > **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
52
 
53
+ ## 🎯 AI-Powered Observability Workshop Rules
54
+
55
+ Enhance your AI assistant with comprehensive Dynatrace observability analysis capabilities through our streamlined workshop rules. These rules provide hierarchical workflows for security, compliance, incident response, and distributed systems investigation.
56
+
57
+ ### **🚀 Quick Setup for AI Assistants**
58
+
59
+ Copy the comprehensive rule files from the [`rules/`](./rules/) directory to your AI assistant's rules directory:
60
+
61
+ **IDE-Specific Locations:**
62
+
63
+ - **Amazon Q**: `.amazonq/rules/` (project) or `~/.aws/amazonq/rules/` (global)
64
+ - **Cursor**: `.cursor/rules/` (project) or via Settings → Rules (global)
65
+ - **Windsurf**: `.windsurfrules/` (project) or via Customizations → Rules (global)
66
+ - **Cline**: `.clinerules/` (project) or `~/Documents/Cline/Rules/` (global)
67
+ - **GitHub Copilot**: `.github/copilot-instructions.md` (project only)
68
+
69
+ Then initialize the agent in your AI chat:
70
+
71
+ ```
72
+ load dynatrace mcp
73
+ ```
74
+
75
+ ### **🏗️ Enhanced Analysis Capabilities**
76
+
77
+ The workshop rules unlock advanced observability analysis modes:
78
+
79
+ #### **🚨 Incident Response & Problem Investigation**
80
+
81
+ - **4-phase structured investigation** workflow (Detection → Impact → Root Cause → Resolution)
82
+ - **Cross-data source correlation** (problems → logs → spans → metrics)
83
+ - **Kubernetes-aware incident analysis** with namespace and pod context
84
+ - **User impact assessment** with Davis AI integration
85
+
86
+ #### **📊 Comprehensive Data Investigation**
87
+
88
+ - **Unified log-service-process analysis** in single workflow
89
+ - **Business logic error detection** patterns
90
+ - **Deployment correlation analysis** with ArgoCD/GitOps integration
91
+ - **Golden signals monitoring** (Rate, Errors, Duration, Saturation)
92
+
93
+ #### **🔗 Advanced Transaction Analysis**
94
+
95
+ - **Precise root cause identification** with file/line numbers
96
+ - **Exception stack trace analysis** with business context
97
+ - **Multi-service cascade failure analysis**
98
+ - **Performance impact correlation** across distributed systems
99
+
100
+ #### **🛡️ Enhanced Security & Compliance**
101
+
102
+ - **Latest-scan analysis** prevents outdated data aggregation
103
+ - **Multi-cloud compliance** (AWS, Azure, GCP, Kubernetes)
104
+ - **Evidence-based investigation** with detailed remediation paths
105
+ - **Risk-based scoring** with team-specific guidance
106
+
107
+ #### **⚡ DevOps Automation & SRE**
108
+
109
+ - **Deployment health gates** with automated promotion/rollback
110
+ - **SLO/SLI automation** with error budget calculations
111
+ - **Infrastructure as Code remediation** with auto-generated templates
112
+ - **Alert optimization workflows** with pattern recognition
113
+
114
+ ### **📁 Hierarchical Rule Architecture**
115
+
116
+ The rules are organized in a context-window optimized structure:
117
+
118
+ ```
119
+ rules/
120
+ ├── DynatraceMcpIntegration.md # 🎯 MAIN ORCHESTRATOR
121
+ ├── workflows/ # 🔧 ANALYSIS WORKFLOWS
122
+ │ ├── incidentResponse.md # Core incident investigation
123
+ │ ├── DynatraceSecurityCompliance.md # Security & compliance analysis
124
+ │ ├── DynatraceDevOpsIntegration.md # CI/CD automation
125
+ │ └── dataSourceGuides/ # 📊 DATA ANALYSIS GUIDES
126
+ │ ├── dataInvestigation.md # Logs, services, processes
127
+ │ └── DynatraceSpanAnalysis.md # Transaction tracing
128
+ └── reference/ # 📚 TECHNICAL DOCUMENTATION
129
+ ├── DynatraceQueryLanguage.md # DQL syntax foundation
130
+ ├── DynatraceExplore.md # Field discovery patterns
131
+ ├── DynatraceSecurityEvents.md # Security events schema
132
+ └── DynatraceProblemsSpec.md # Problems schema reference
133
+ ```
134
+
135
+ **Key Architectural Benefits:**
136
+
137
+ - **All files under 6,500 tokens** - Compatible with most LLM context limits
138
+ - **Hierarchical organization** - Clear entry points and specialized guides
139
+ - **Eliminated circular references** - No more confusing cross-referencing webs
140
+ - **DQL-first approach** - Prefer flexible queries over rigid MCP calls
141
+
142
+ For detailed information about the workshop rules, see the [Rules README](./rules/README.md).
143
+
34
144
  ## Quickstart
35
145
 
36
146
  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`.
@@ -111,6 +221,82 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
111
221
 
112
222
  This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
113
223
 
224
+ ### HTTP Server Mode (Alternative)
225
+
226
+ 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:
227
+
228
+ **Running as HTTP server:**
229
+
230
+ ```bash
231
+ # Get help and see all available options
232
+ npx -y @dynatrace-oss/dynatrace-mcp-server --help
233
+
234
+ # Run with HTTP server on default port 3000
235
+ npx -y @dynatrace-oss/dynatrace-mcp-server --http
236
+
237
+ # Run with custom port (using short or long flag)
238
+ npx -y @dynatrace-oss/dynatrace-mcp-server --server -p 8080
239
+ npx -y @dynatrace-oss/dynatrace-mcp-server --http --port 3001
240
+
241
+ # Run with custom host/IP (using short or long flag)
242
+ npx -y @dynatrace-oss/dynatrace-mcp-server --http --host 127.0.0.1
243
+ npx -y @dynatrace-oss/dynatrace-mcp-server --http -H 192.168.0.1
244
+
245
+ # Check version
246
+ npx -y @dynatrace-oss/dynatrace-mcp-server --version
247
+ ```
248
+
249
+ **Configuration for MCP clients that support HTTP transport:**
250
+
251
+ ```json
252
+ {
253
+ "mcpServers": {
254
+ "dynatrace-http": {
255
+ "url": "http://localhost:3000",
256
+ "transport": "http"
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ **Configuration for MCP clients that support HTTP transport:**
263
+
264
+ ```json
265
+ {
266
+ "mcpServers": {
267
+ "dynatrace-http": {
268
+ "url": "http://localhost:3000",
269
+ "transport": "http"
270
+ }
271
+ }
272
+ }
273
+ ```
274
+
275
+ ### Rule File
276
+
277
+ For efficient result retrieval from Dynatrace, please consider creating a rule file (e.g., [.github/copilot-instructions.md](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [.amazonq/rules/](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/context-project-rules.html)), instructing coding agents on how to get more details for your component/app/service. Here is an example for [easytrade](https://github.com/Dynatrace/easytrade), please adapt the names and filters to fit your use-cases and components:
278
+
279
+ ```
280
+ # Observability
281
+
282
+ We use Dynatrace as an Observability solution. This document provides instructions on how to get data for easytrade from Dynatrace using DQL.
283
+
284
+ ## How to get any data for my App
285
+
286
+ Depending on the query and tool used, the following filters can be applied to narrow down results:
287
+
288
+ * `contains(entity.name, "easytrade")`
289
+ * `contains(affected_entity.name, "easytrade")`
290
+ * `contains(container.name, "easytrade")`
291
+
292
+ For best results, you can combine these filters with an `OR` operator.
293
+
294
+ ## Logs
295
+
296
+ To fetch logs for easytrade, execute `fetch logs | filter contains(container.name, "easyatrade")`.
297
+ For fetching just error-logs, add `| filter loglevel == "ERROR"`.
298
+ ```
299
+
114
300
  ## Environment Variables
115
301
 
116
302
  You can set up authentication via **OAuth Client** or **Platform Tokens** (v0.5.0 and newer) via the following environment variables:
@@ -134,7 +320,7 @@ Depending on the features you are using, the following scopes are needed:
134
320
 
135
321
  - `app-engine:apps:run` - needed for almost all tools
136
322
  - `app-engine:functions:run` - needed for for almost all tools
137
- - `environment-api:entities:read` - read monitored entities (_currently not available for Platform Tokens_)
323
+ - `environment-api:entities:read` - for retrieving ownership details from monitored entities (_currently not available for Platform Tokens_)
138
324
  - `automation:workflows:read` - read Workflows
139
325
  - `automation:workflows:write` - create and update Workflows
140
326
  - `automation:workflows:run` - run Workflows
@@ -159,7 +345,9 @@ Depending on the features you are using, the following scopes are needed:
159
345
  ## ✨ Example prompts ✨
160
346
 
161
347
  Use these example prompts as a starting point. Just copy them into your IDE or agent setup, adapt them to your services/stack/architecture,
162
- and extend them as needed. Theyre here to help you imagine how real-time observability and automation work together in the MCP context in your IDE.
348
+ 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.
349
+
350
+ ### **Basic Queries & AI Assistance**
163
351
 
164
352
  **Write a DQL query from natural language:**
165
353
 
@@ -180,6 +368,85 @@ fetch logs | filter dt.source_entity == 'SERVICE-123' | summarize count(), by:{s
180
368
  How can I investigate slow database queries in Dynatrace?
181
369
  ```
182
370
 
371
+ ### **Advanced Incident Investigation**
372
+
373
+ **Multi-phase incident response:**
374
+
375
+ ```
376
+ Our checkout service is experiencing high error rates. Start a systematic 4-phase incident investigation:
377
+ 1. Detect and triage the active problems
378
+ 2. Assess user impact and affected services
379
+ 3. Perform cross-data source analysis (problems → spans → logs)
380
+ 4. Identify root cause with file/line-level precision
381
+ ```
382
+
383
+ **Cross-service failure analysis:**
384
+
385
+ ```
386
+ We have cascading failures across our microservices architecture.
387
+ Analyze the entity relationships and trace the failure propagation from the initial problem
388
+ through all downstream services. Show me the correlation timeline.
389
+ ```
390
+
391
+ ### **Security & Compliance Analysis**
392
+
393
+ **Latest-scan vulnerability assessment:**
394
+
395
+ ```
396
+ Perform a comprehensive security analysis using the latest scan data:
397
+ - Check for new vulnerabilities in our production environment
398
+ - Focus on critical and high-severity findings
399
+ - Provide evidence-based remediation paths
400
+ - Generate risk scores with team-specific guidance
401
+ ```
402
+
403
+ **Multi-cloud compliance monitoring:**
404
+
405
+ ```
406
+ Run a compliance assessment across our AWS, Azure, and Kubernetes environments.
407
+ Check for configuration drift and security posture changes in the last 24 hours.
408
+ ```
409
+
410
+ ### **DevOps & SRE Automation**
411
+
412
+ **Deployment health gate analysis:**
413
+
414
+ ```
415
+ Our latest deployment is showing performance degradation.
416
+ Run deployment health gate analysis with:
417
+ - Golden signals monitoring (Rate, Errors, Duration, Saturation)
418
+ - SLO/SLI validation with error budget calculations
419
+ - Generate automated rollback recommendation if needed
420
+ ```
421
+
422
+ **Infrastructure as Code remediation:**
423
+
424
+ ```
425
+ Generate Infrastructure as Code templates to remediate the current alert patterns.
426
+ Include automated scaling policies and resource optimization recommendations.
427
+ ```
428
+
429
+ ### **Deep Transaction Analysis**
430
+
431
+ **Business logic error investigation:**
432
+
433
+ ```
434
+ Our payment processing is showing intermittent failures.
435
+ Perform advanced transaction analysis:
436
+ - Extract exception details with full stack traces
437
+ - Correlate with deployment events and ArgoCD changes
438
+ - Identify the exact code location causing the issue
439
+ ```
440
+
441
+ **Performance correlation analysis:**
442
+
443
+ ```
444
+ Analyze the performance impact across our distributed system for the slow checkout flow.
445
+ Show me the complete trace analysis with business context and identify bottlenecks.
446
+ ```
447
+
448
+ ### **Traditional Use Cases (Enhanced)**
449
+
183
450
  **Find open vulnerabilities on production, setup alert:**
184
451
 
185
452
  ```
@@ -301,6 +568,20 @@ Third, create a `.env` file in this repository (you can copy from `.env.template
301
568
 
302
569
  Finally, make changes to your code and compile it with `npm run build` or just run `npm run watch` and it auto-compiles.
303
570
 
571
+ ## Releasing
572
+
573
+ When you are preparing for a release, you can use GitHub Copilot to guide you through the preparations.
574
+
575
+ 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).
576
+
577
+ You may include additional information such as the version number. If not specified, you will be asked.
578
+
579
+ This will
580
+
581
+ - prepare the [changelog](CHANGELOG.md),
582
+ - update the version number in [package.json](package.json),
583
+ - commit the changes.
584
+
304
585
  ## Notes
305
586
 
306
587
  This product is not officially supported by Dynatrace.
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
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
- const package_json_1 = require("../../package.json");
6
+ const user_agent_1 = require("../utils/user-agent");
7
7
  /**
8
8
  * Uses the provided oauth Client ID and Secret and requests a token via client-credentials flow
9
9
  * @param clientId - OAuth Client ID for Dynatrace
@@ -60,7 +60,7 @@ const createBearerTokenHttpClient = async (environmentUrl, dtPlatformToken) => {
60
60
  baseUrl: environmentUrl,
61
61
  defaultHeaders: {
62
62
  'Authorization': `Bearer ${dtPlatformToken}`,
63
- 'User-Agent': `dynatrace-mcp-server/v${package_json_1.version} (${process.platform}-${process.arch})`,
63
+ 'User-Agent': (0, user_agent_1.getUserAgent)(),
64
64
  },
65
65
  });
66
66
  };
@@ -62,7 +62,7 @@ describe('dynatrace-clients', () => {
62
62
  baseUrl: environmentUrl,
63
63
  defaultHeaders: {
64
64
  'Authorization': 'Bearer test-access-token',
65
- 'User-Agent': 'dynatrace-mcp-server/v1.0.0-test (linux-x64)',
65
+ 'User-Agent': expect.stringMatching(/^dynatrace-mcp-server\/v1\.0\.0-test \(\w+-\w+\)$/),
66
66
  },
67
67
  });
68
68
  expect(result).toBeInstanceOf(http_client_1.PlatformHttpClient);
@@ -131,7 +131,7 @@ describe('dynatrace-clients', () => {
131
131
  baseUrl: environmentUrl,
132
132
  defaultHeaders: {
133
133
  'Authorization': `Bearer ${dtPlatformToken}`,
134
- 'User-Agent': 'dynatrace-mcp-server/v1.0.0-test (linux-x64)',
134
+ 'User-Agent': expect.stringMatching(/^dynatrace-mcp-server\/v1\.0\.0-test \(\w+-\w+\)$/),
135
135
  },
136
136
  });
137
137
  expect(result).toBeInstanceOf(http_client_1.PlatformHttpClient);
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.executeDql = exports.verifyDqlStatement = void 0;
4
4
  const client_query_1 = require("@dynatrace-sdk/client-query");
5
+ const user_agent_1 = require("../utils/user-agent");
5
6
  const verifyDqlStatement = async (dtClient, dqlStatement) => {
6
7
  const queryAssistanceClient = new client_query_1.QueryAssistanceClient(dtClient);
7
8
  const response = await queryAssistanceClient.queryVerify({
@@ -22,7 +23,10 @@ exports.verifyDqlStatement = verifyDqlStatement;
22
23
  */
23
24
  const executeDql = async (dtClient, body) => {
24
25
  const queryExecutionClient = new client_query_1.QueryExecutionClient(dtClient);
25
- const response = await queryExecutionClient.queryExecute({ body });
26
+ const response = await queryExecutionClient.queryExecute({
27
+ body,
28
+ dtClientContext: (0, user_agent_1.getUserAgent)(),
29
+ });
26
30
  if (response.result) {
27
31
  // return response result immediately
28
32
  return response.result.records;
@@ -36,6 +40,7 @@ const executeDql = async (dtClient, body) => {
36
40
  await new Promise((resolve) => setTimeout(resolve, 2000));
37
41
  pollResponse = await queryExecutionClient.queryPoll({
38
42
  requestToken: response.requestToken,
43
+ dtClientContext: (0, user_agent_1.getUserAgent)(),
39
44
  });
40
45
  // done - let's return it
41
46
  if (pollResponse.result) {
@@ -1,13 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.findMonitoredEntityByName = void 0;
3
+ exports.findMonitoredEntityByName = exports.generateDqlSearchEntityCommand = void 0;
4
4
  const execute_dql_1 = require("./execute-dql");
5
+ const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
6
+ /**
7
+ * Construct a DQL statement like "fetch <entityType> | search "*<entityName>*" | fieldsAdd entity.type" for each entity type,
8
+ * and join them with " | append [ ... ]"
9
+ * @param entityName
10
+ * @returns DQL Statement for searching all entity types
11
+ */
12
+ const generateDqlSearchEntityCommand = (entityName) => {
13
+ const fetchDqlCommands = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.map((entityType, index) => {
14
+ const dql = `fetch ${entityType} | search "*${entityName}*" | fieldsAdd entity.type`;
15
+ if (index === 0) {
16
+ return dql;
17
+ }
18
+ return ` | append [ ${dql} ]\n`;
19
+ });
20
+ return fetchDqlCommands.join('');
21
+ };
22
+ exports.generateDqlSearchEntityCommand = generateDqlSearchEntityCommand;
23
+ /**
24
+ * Find a monitored entity by name via DQL
25
+ * @param dtClient
26
+ * @param entityName
27
+ * @returns A string with the entity details like id, name and type, or an error message if no entity was found
28
+ */
5
29
  const findMonitoredEntityByName = async (dtClient, entityName) => {
6
- const dql = `fetch dt.entity.application | search "*${entityName}*" | fieldsAdd entity.type
7
- | append [fetch dt.entity.service | search "*${entityName}*" | fieldsAdd entity.type]
8
- | append [fetch dt.entity.host | search "*${entityName}*" | fieldsAdd entity.type]
9
- | append [fetch dt.entity.process_group | search "*${entityName}*" | fieldsAdd entity.type]
10
- | append [fetch dt.entity.cloud_application | search "*${entityName}*" | fieldsAdd entity.type]`;
30
+ if (!entityName) {
31
+ return 'You need to provide an entity name to search for.';
32
+ }
33
+ // construct a DQL statement for searching the entityName over all entity types
34
+ const dql = (0, exports.generateDqlSearchEntityCommand)(entityName);
35
+ // Get response from API
36
+ // Note: This may be slow, as we are appending multiple entity types above
11
37
  const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
12
38
  if (dqlResponse && dqlResponse.length > 0) {
13
39
  let resp = 'The following monitored entities were found:\n';
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
4
+ const find_monitored_entity_by_name_1 = require("./find-monitored-entity-by-name");
5
+ describe('generateDqlSearchCommand', () => {
6
+ beforeEach(() => {
7
+ // Ensure we have at least some entity types for testing
8
+ expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.length).toBeGreaterThan(0);
9
+ });
10
+ it('should include all entity types from DYNATRACE_ENTITY_TYPES', () => {
11
+ const entityName = 'test';
12
+ const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityName);
13
+ console.log(result);
14
+ // Check that all entity types are included in the DQL
15
+ dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.forEach((entityType) => {
16
+ expect(result).toContain(`fetch ${entityType}`);
17
+ });
18
+ });
19
+ it('should structure the DQL correctly with first fetch and subsequent appends', () => {
20
+ const entityName = 'test';
21
+ const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityName);
22
+ // First entity type should not have append prefix
23
+ const firstEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES[0];
24
+ expect(result).toContain(`fetch ${firstEntityType} | search "*${entityName}*" | fieldsAdd entity.type`);
25
+ // Subsequent entity types should have append prefix (if there are more than 1)
26
+ if (dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.length > 1) {
27
+ const secondEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES[1];
28
+ expect(result).toContain(` | append [ fetch ${secondEntityType} | search "*${entityName}*" | fieldsAdd entity.type ]`);
29
+ }
30
+ });
31
+ });
@@ -1,12 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getMonitoredEntityDetails = void 0;
4
- const client_classic_environment_v2_1 = require("@dynatrace-sdk/client-classic-environment-v2");
4
+ const execute_dql_1 = require("./execute-dql");
5
+ const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
6
+ /**
7
+ * Get monitored entity details by entity ID via DQL
8
+ * @param dtClient
9
+ * @param entityId
10
+ * @returns Details about the monitored entity, or undefined in case we couldn't find it
11
+ */
5
12
  const getMonitoredEntityDetails = async (dtClient, entityId) => {
6
- const monitoredEntitiesClient = new client_classic_environment_v2_1.MonitoredEntitiesClient(dtClient);
7
- const entityDetails = await monitoredEntitiesClient.getEntity({
8
- entityId: entityId,
9
- });
10
- return entityDetails;
13
+ // Try to determine the entity type directly from the entity ID (e.g., PROCESS_GROUP-F84E4759809ADA84 -> dt.entity.process_group)
14
+ const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(entityId);
15
+ if (!entityType) {
16
+ console.error(`Couldn't determine entity type for ID: ${entityId}. Please raise an issue at https://github.com/dynatrace-oss/dynatrace-mcp/issues if you believe this is a bug.`);
17
+ return;
18
+ }
19
+ // construct DQL statement like `fetch dt.entity.hosts | filter id == "HOST-1234"`
20
+ const dql = `fetch ${entityType} | filter id == "${entityId}" | expand tags | fieldsAdd entity.type`;
21
+ // Get response from API
22
+ const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
23
+ // verify response and length
24
+ if (!dqlResponse || dqlResponse.length === 0) {
25
+ console.error(`No entity found for ID: ${entityId}`);
26
+ return;
27
+ }
28
+ // in case we have more than one entity -> log it
29
+ if (dqlResponse.length > 1) {
30
+ console.error(`Multiple entities (${dqlResponse.length}) found for entity ID: ${entityId}. Returning the first one.`);
31
+ }
32
+ const entity = dqlResponse[0];
33
+ // make typescript happy; entity should never be null though
34
+ if (!entity) {
35
+ console.error(`No entity found for ID: ${entityId}`);
36
+ return;
37
+ }
38
+ // return entity details
39
+ return {
40
+ entityId: String(entity.id),
41
+ displayName: String(entity['entity.name']),
42
+ type: String(entity['entity.type']),
43
+ allProperties: entity || undefined,
44
+ };
11
45
  };
12
46
  exports.getMonitoredEntityDetails = getMonitoredEntityDetails;
package/dist/index.js CHANGED
@@ -4,8 +4,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const client_platform_management_service_1 = require("@dynatrace-sdk/client-platform-management-service");
5
5
  const shared_errors_1 = require("@dynatrace-sdk/shared-errors");
6
6
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
7
8
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
8
9
  const dotenv_1 = require("dotenv");
10
+ const node_http_1 = require("node:http");
11
+ const node_crypto_1 = require("node:crypto");
12
+ const commander_1 = require("commander");
9
13
  const zod_1 = require("zod");
10
14
  const package_json_1 = require("../package.json");
11
15
  const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
@@ -13,7 +17,6 @@ const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
13
17
  const list_problems_1 = require("./capabilities/list-problems");
14
18
  const get_monitored_entity_details_1 = require("./capabilities/get-monitored-entity-details");
15
19
  const get_ownership_information_1 = require("./capabilities/get-ownership-information");
16
- const get_logs_for_entity_1 = require("./capabilities/get-logs-for-entity");
17
20
  const get_events_for_cluster_1 = require("./capabilities/get-events-for-cluster");
18
21
  const create_workflow_for_problem_notification_1 = require("./capabilities/create-workflow-for-problem-notification");
19
22
  const update_workflow_1 = require("./capabilities/update-workflow");
@@ -27,6 +30,24 @@ let scopesBase = [
27
30
  'app-engine:apps:run', // needed for environmentInformationClient
28
31
  'app-engine:functions:run', // needed for environmentInformationClient
29
32
  ];
33
+ /**
34
+ * Performs a connection test to the Dynatrace environment.
35
+ * Throws an error if the connection or authentication fails.
36
+ */
37
+ async function testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken) {
38
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
39
+ const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
40
+ // This call will fail if authentication is incorrect.
41
+ await environmentInformationClient.getEnvironmentInformation();
42
+ }
43
+ function handleClientRequestError(error) {
44
+ let additionalErrorInformation = '';
45
+ if (error.response.status === 403) {
46
+ additionalErrorInformation =
47
+ 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
48
+ }
49
+ return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
50
+ }
30
51
  const main = async () => {
31
52
  // read Environment variables
32
53
  let dynatraceEnv;
@@ -39,6 +60,34 @@ const main = async () => {
39
60
  }
40
61
  console.error(`Initializing Dynatrace MCP Server v${package_json_1.version}...`);
41
62
  const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv;
63
+ // Test connection on startup
64
+ let retryCount = 0;
65
+ const maxRetries = 5;
66
+ while (true) {
67
+ try {
68
+ console.error(`Testing connection to Dynatrace environment: ${dtEnvironment}... (Attempt ${retryCount + 1} of ${maxRetries})`);
69
+ await testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
70
+ console.error(`Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
71
+ break;
72
+ }
73
+ catch (error) {
74
+ console.error(`Error: Could not connect to the Dynatrace environment.`);
75
+ if ((0, shared_errors_1.isClientRequestError)(error)) {
76
+ console.error(handleClientRequestError(error));
77
+ }
78
+ else {
79
+ console.error(`Error: ${error.message}`);
80
+ }
81
+ retryCount++;
82
+ if (retryCount >= maxRetries) {
83
+ console.error(`Fatal: Maximum number of connection retries (${maxRetries}) exceeded. Exiting.`);
84
+ process.exit(1);
85
+ }
86
+ const delay = Math.pow(2, retryCount) * 1000; // Exponential backoff
87
+ console.error(`Retrying in ${delay / 1000} seconds...`);
88
+ await new Promise((resolve) => setTimeout(resolve, delay));
89
+ }
90
+ }
42
91
  console.error(`Starting Dynatrace MCP Server v${package_json_1.version}...`);
43
92
  const server = new mcp_js_1.McpServer({
44
93
  name: 'Dynatrace MCP Server',
@@ -60,19 +109,8 @@ const main = async () => {
60
109
  catch (error) {
61
110
  // check if it's an error originating from the Dynatrace SDK / API Gateway and provide an appropriate message to the user
62
111
  if ((0, shared_errors_1.isClientRequestError)(error)) {
63
- const e = error;
64
- let additionalErrorInformation = '';
65
- if (e.response.status == 403) {
66
- additionalErrorInformation =
67
- 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
68
- }
69
112
  return {
70
- content: [
71
- {
72
- type: 'text',
73
- text: `Client Request Error: ${e.message} with HTTP status: ${e.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(e.body)})`,
74
- },
75
- ],
113
+ content: [{ type: 'text', text: handleClientRequestError(error) }],
76
114
  isError: true,
77
115
  };
78
116
  }
@@ -86,7 +124,7 @@ const main = async () => {
86
124
  };
87
125
  server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
88
126
  };
89
- tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant)', {}, async ({}) => {
127
+ tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, async ({}) => {
90
128
  // create an oauth-client
91
129
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
92
130
  const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
@@ -183,20 +221,23 @@ const main = async () => {
183
221
  return 'No problems found';
184
222
  }
185
223
  });
186
- tool('find_entity_by_name', 'Get the entityId of a monitored entity based on the name of the entity on Dynatrace', {
187
- entityName: zod_1.z.string(),
224
+ 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', {
225
+ entityName: zod_1.z.string().describe('Name of the entity to search for, e.g., "my-service" or "my-host"'),
188
226
  }, async ({ entityName }) => {
189
- const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read', 'storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
227
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
190
228
  const entityResponse = await (0, find_monitored_entity_by_name_1.findMonitoredEntityByName)(dtClient, entityName);
191
229
  return entityResponse;
192
230
  });
193
231
  tool('get_entity_details', 'Get details of a monitored entity based on the entityId on Dynatrace', {
194
232
  entityId: zod_1.z.string().optional(),
195
233
  }, async ({ entityId }) => {
196
- const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('environment-api:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
234
+ const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
197
235
  const entityDetails = await (0, get_monitored_entity_details_1.getMonitoredEntityDetails)(dtClient, entityId);
236
+ if (!entityDetails) {
237
+ return `No entity found with entityId: ${entityId}`;
238
+ }
198
239
  let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
199
- `Properties: ${JSON.stringify(entityDetails.properties)}\n`;
240
+ `Properties: ${JSON.stringify(entityDetails.allProperties)}\n`;
200
241
  if (entityDetails.type == 'SERVICE') {
201
242
  resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`;
202
243
  }
@@ -219,13 +260,6 @@ const main = async () => {
219
260
  const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
220
261
  return `Message sent to Slack channel: ${JSON.stringify(response)}`;
221
262
  });
222
- tool('get_logs_for_entity', 'Get Logs for a monitored entity based on name of the entity on Dynatrace', {
223
- entityName: zod_1.z.string().optional(),
224
- }, async ({ entityName }) => {
225
- const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:logs:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
226
- const logs = await (0, get_logs_for_entity_1.getLogsForEntity)(dtClient, entityName);
227
- return `Logs:\n${JSON.stringify(logs?.map((logLine) => (logLine ? logLine.content : 'Empty log')))}`;
228
- });
229
263
  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.', {
230
264
  dqlStatement: zod_1.z.string(),
231
265
  }, async ({ dqlStatement }) => {
@@ -246,9 +280,14 @@ const main = async () => {
246
280
  }
247
281
  return resp;
248
282
  });
249
- tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. It\'s recommended to use "verify_dql" tool before you execute a DQL statement. A valid statement looks like this: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}. Adapt filters for certain attributes: `traceId` could be `trace_id` or `trace.id`.', {
250
- dqlStatement: zod_1.z.string(),
283
+ tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
284
+ 'You can also use "generate_dql_from_natural_language" to generate a DQL statement based on your request. ' +
285
+ 'Note: For more information about available fields for filters and aggregation, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
286
+ dqlStatement: zod_1.z
287
+ .string()
288
+ .describe('DQL Statement (Ex: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}.", "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }")'),
251
289
  }, async ({ dqlStatement }) => {
290
+ // Create a HTTP Client that has all storage:*:read scopes
252
291
  const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
253
292
  'storage:logs:read', // Read logs for reliability guardian validations
254
293
  'storage:metrics:read', // Read metrics for reliability guardian validations
@@ -397,10 +436,67 @@ const main = async () => {
397
436
  resp += JSON.stringify(ownershipInformation);
398
437
  return resp;
399
438
  });
400
- const transport = new stdio_js_1.StdioServerTransport();
401
- console.error('Connecting server to transport...');
402
- await server.connect(transport);
403
- console.error('Dynatrace MCP Server running on stdio');
439
+ // Parse command line arguments using commander
440
+ const program = new commander_1.Command();
441
+ program
442
+ .name('dynatrace-mcp-server')
443
+ .description('Dynatrace Model Context Protocol (MCP) Server')
444
+ .version(package_json_1.version)
445
+ .option('--http', 'enable HTTP server mode instead of stdio')
446
+ .option('--server', 'enable HTTP server mode (alias for --http)')
447
+ .option('-p, --port <number>', 'port for HTTP server', '3000')
448
+ .option('-H, --host <host>', 'host for HTTP server', '0.0.0.0')
449
+ .parse();
450
+ const options = program.opts();
451
+ const httpMode = options.http || options.server;
452
+ const httpPort = parseInt(options.port, 10);
453
+ const host = options.host || '0.0.0.0';
454
+ if (httpMode) {
455
+ // HTTP server mode
456
+ const httpTransport = new streamableHttp_js_1.StreamableHTTPServerTransport({
457
+ sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
458
+ });
459
+ const httpServer = (0, node_http_1.createServer)(async (req, res) => {
460
+ // Parse request body for POST requests
461
+ let body;
462
+ if (req.method === 'POST') {
463
+ const chunks = [];
464
+ for await (const chunk of req) {
465
+ chunks.push(chunk);
466
+ }
467
+ const rawBody = Buffer.concat(chunks).toString();
468
+ try {
469
+ body = JSON.parse(rawBody);
470
+ }
471
+ catch (error) {
472
+ res.writeHead(400, { 'Content-Type': 'application/json' });
473
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
474
+ return;
475
+ }
476
+ }
477
+ await httpTransport.handleRequest(req, res, body);
478
+ });
479
+ console.error('Connecting server to HTTP transport...');
480
+ await server.connect(httpTransport);
481
+ // Start HTTP Server on the specified host and port
482
+ httpServer.listen(httpPort, host, () => {
483
+ console.error(`Dynatrace MCP Server running on HTTP at http://${host}:${httpPort}`);
484
+ });
485
+ // Handle graceful shutdown
486
+ process.on('SIGINT', () => {
487
+ console.error('Shutting down HTTP server...');
488
+ httpServer.close(() => {
489
+ process.exit(0);
490
+ });
491
+ });
492
+ }
493
+ else {
494
+ // Default stdio mode
495
+ const transport = new stdio_js_1.StdioServerTransport();
496
+ console.error('Connecting server to transport...');
497
+ await server.connect(transport);
498
+ console.error('Dynatrace MCP Server running on stdio');
499
+ }
404
500
  };
405
501
  main().catch((error) => {
406
502
  console.error('Fatal error in main():', error);
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Dynatrace Entity Types
4
+ *
5
+ * Complete list of Dynatrace entity types for querying monitored entities.
6
+ * Generated from ENTITY_ID_PREFIX_TO_TYPE_MAP values to ensure consistency.
7
+ * Last Updated: 2025-08-14
8
+ * @see https://docs.dynatrace.com/docs/discover-dynatrace/references/semantic-dictionary/model/dt-entities
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.DYNATRACE_ENTITY_TYPES = void 0;
12
+ exports.getEntityTypeFromId = getEntityTypeFromId;
13
+ /**
14
+ * Entity ID prefixes mapped to their corresponding Dynatrace entity types
15
+ * When adding a new Entity, there should also be a corresponding test in dynatrace-entity-types.test.ts
16
+ * to ensure that the entity type can be correctly derived from the entity ID.
17
+ *
18
+ * Disclaimer: This mapping is based on the current Dynatrace API and may change in future versions. Use at your own risk!
19
+ *
20
+ * Recommendation: Use `verify_dql` and/or `execute_dql` to ensure that the entity type can be queried correctly.
21
+ */
22
+ const ENTITY_ID_PREFIX_TO_TYPE_MAP = {
23
+ // Core Applications and Services
24
+ APPLICATION: 'dt.entity.application', // Verified!
25
+ SERVICE: 'dt.entity.service', // Verified!
26
+ SERVICE_INSTANCE: 'dt.entity.service_instance', // Verified!
27
+ MOBILE_APPLICATION: 'dt.entity.mobile_application', // Verified! (0 rows found, manually verified that entity exists)
28
+ CUSTOM_APPLICATION: 'dt.entity.custom_application', // Verified!
29
+ // Infrastructure
30
+ HOST: 'dt.entity.host', // Verified!
31
+ HOST_GROUP: 'dt.entity.host_group', // Verified!
32
+ PROCESS_GROUP: 'dt.entity.process_group', // Verified!
33
+ PROCESS_GROUP_INSTANCE: 'dt.entity.process_group_instance', // Verified!
34
+ DISK: 'dt.entity.disk', // Verified!
35
+ NETWORK_INTERFACE: 'dt.entity.network_interface', // Verified!
36
+ // Cloud Services
37
+ CLOUD_APPLICATION: 'dt.entity.cloud_application', // Verified!
38
+ CLOUD_APPLICATION_INSTANCE: 'dt.entity.cloud_application_instance', // Verified!
39
+ CLOUD_APPLICATION_NAMESPACE: 'dt.entity.cloud_application_namespace', // Verified!
40
+ // Containers and Container Groups
41
+ CONTAINER_GROUP: 'dt.entity.container_group', // Verified!
42
+ CONTAINER_GROUP_INSTANCE: 'dt.entity.container_group_instance', // Verified!
43
+ DCG_INSTANCE: 'dt.entity.docker_container_group_instance', // Verified! (0 rows found, manually verified that entity exists, but this might be deprecated / old)
44
+ // Environment
45
+ ENVIRONMENT: 'dt.entity.environment', // Verified!
46
+ // Operating System
47
+ OS: 'dt.entity.os', // Verified!
48
+ // Synthetic Monitoring
49
+ SYNTHETIC_TEST: 'dt.entity.synthetic_test', // Verified!
50
+ SYNTHETIC_LOCATION: 'dt.entity.synthetic_location', // Verified!
51
+ // Custom Devices and Entities
52
+ CUSTOM_DEVICE: 'dt.entity.custom_device', // Verified!
53
+ CUSTOM_DEVICE_GROUP: 'dt.entity.custom_device_group', // Verified!
54
+ // Geolocation
55
+ GEOLOCATION: 'dt.entity.geolocation', // Verified!
56
+ // Database Services
57
+ RELATIONAL_DATABASE_SERVICE: 'dt.entity.relational_database_service', // Verified - might need an additional integration/config to work properly though
58
+ // AWS Services
59
+ EC2_INSTANCE: 'dt.entity.ec2_instance', // Verified!
60
+ AWS_LAMBDA_FUNCTION: 'dt.entity.aws_lambda_function', // Verified!
61
+ AWS_AVAILABILITY_ZONE: 'dt.entity.aws_availability_zone', // Verified!
62
+ AWS_APPLICATION_LOAD_BALANCER: 'dt.entity.aws_application_load_balancer', // Verified!
63
+ AWS_NETWORK_LOAD_BALANCER: 'dt.entity.aws_network_load_balancer', // Verified!
64
+ // GCP Services
65
+ GCP_ZONE: 'dt.entity.gcp_zone', // Verified!
66
+ // Virtual Machines
67
+ AZURE_VM: 'dt.entity.azure_vm', // Verified
68
+ OPENSTACK_VM: 'dt.entity.openstack_vm', // Needs manual verification - available only if OpenStack integration is configured
69
+ // Kubernetes Entities
70
+ KUBERNETES_NODE: 'dt.entity.kubernetes_node', // Verified!
71
+ KUBERNETES_CLUSTER: 'dt.entity.kubernetes_cluster', // Verified!
72
+ KUBERNETES_SERVICE: 'dt.entity.kubernetes_service', // Verified!
73
+ };
74
+ exports.DYNATRACE_ENTITY_TYPES = Object.values(ENTITY_ID_PREFIX_TO_TYPE_MAP).sort();
75
+ /**
76
+ * Maps a Dynatrace entity ID to its corresponding entity type.
77
+ *
78
+ * @param entityId - The Dynatrace entity ID (e.g., "PROCESS_GROUP-F84E4759809ADA84")
79
+ * @returns The corresponding entity type (e.g., "dt.entity.process_group") or null if no mapping exists
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * getEntityTypeFromId("PROCESS_GROUP-F84E4759809ADA84"); // Returns "dt.entity.process_group"
84
+ * getEntityTypeFromId("APPLICATION-1234567890ABCDEF"); // Returns "dt.entity.application"
85
+ * getEntityTypeFromId("KUBERNETES_SERVICE-ABCDEF1234567890"); // Returns "dt.entity.kubernetes_service"
86
+ * getEntityTypeFromId("INVALID_ID"); // Returns null
87
+ * ```
88
+ */
89
+ function getEntityTypeFromId(entityId) {
90
+ if (!entityId || typeof entityId !== 'string') {
91
+ return null;
92
+ }
93
+ // Extract the prefix (everything before the first hyphen)
94
+ const hyphenIndex = entityId.indexOf('-');
95
+ if (hyphenIndex === -1) {
96
+ return null;
97
+ }
98
+ const prefix = entityId.substring(0, hyphenIndex);
99
+ // Look up the entity type in our mapping
100
+ return ENTITY_ID_PREFIX_TO_TYPE_MAP[prefix] || null;
101
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const dynatrace_entity_types_1 = require("./dynatrace-entity-types");
4
+ describe('DYNATRACE_ENTITY_TYPES', () => {
5
+ it('should be sorted alphabetically', () => {
6
+ const sortedTypes = [...dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES].sort();
7
+ expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES).toEqual(sortedTypes);
8
+ });
9
+ it('should have unique values', () => {
10
+ const uniqueTypes = [...new Set(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES)];
11
+ expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES.length).toBe(uniqueTypes.length);
12
+ });
13
+ });
14
+ describe('getEntityTypeFromId', () => {
15
+ it('should map PROCESS_GROUP entity ID to dt.entity.process_group', () => {
16
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('PROCESS_GROUP-F84E4759809ADA84');
17
+ expect(result).toBe('dt.entity.process_group');
18
+ });
19
+ it('should map APPLICATION entity ID to dt.entity.application', () => {
20
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('APPLICATION-1234567890ABCDEF');
21
+ expect(result).toBe('dt.entity.application');
22
+ });
23
+ it('should map SERVICE entity ID to dt.entity.service', () => {
24
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('SERVICE-ABCDEF1234567890');
25
+ expect(result).toBe('dt.entity.service');
26
+ });
27
+ it('should map HOST entity ID to dt.entity.host', () => {
28
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('HOST-1234567890ABCDEF');
29
+ expect(result).toBe('dt.entity.host');
30
+ });
31
+ it('should map KUBERNETES_CLUSTER entity ID to dt.entity.kubernetes_cluster', () => {
32
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('KUBERNETES_CLUSTER-1234567890ABCDEF');
33
+ expect(result).toBe('dt.entity.kubernetes_cluster');
34
+ });
35
+ it('should map CLOUD_APPLICATION entity ID to dt.entity.cloud_application', () => {
36
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('CLOUD_APPLICATION-FEDCBA0987654321');
37
+ expect(result).toBe('dt.entity.cloud_application');
38
+ });
39
+ it('should return null for entity ID without hyphen', () => {
40
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('INVALID_ENTITY_ID');
41
+ expect(result).toBeNull();
42
+ });
43
+ it('should return null for unknown entity prefix', () => {
44
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('UNKNOWN_PREFIX-1234567890ABCDEF');
45
+ expect(result).toBeNull();
46
+ });
47
+ it('should return null for empty string', () => {
48
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('');
49
+ expect(result).toBeNull();
50
+ });
51
+ it('should return null for null input', () => {
52
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)(null);
53
+ expect(result).toBeNull();
54
+ });
55
+ it('should return null for undefined input', () => {
56
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)(undefined);
57
+ expect(result).toBeNull();
58
+ });
59
+ it('should return null for non-string input', () => {
60
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)(123);
61
+ expect(result).toBeNull();
62
+ });
63
+ it('should handle entity ID with multiple hyphens correctly', () => {
64
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)('PROCESS_GROUP-F84E-4759-809A-DA84');
65
+ expect(result).toBe('dt.entity.process_group');
66
+ });
67
+ // Tests for verified entity types found in the environment
68
+ describe('Verified Entity Types', () => {
69
+ const verifiedEntityTypesTestCases = [
70
+ {
71
+ entityId: 'ENVIRONMENT-2D0F07E264ABBB14',
72
+ expectedType: 'dt.entity.environment',
73
+ description: 'ENVIRONMENT entity ID',
74
+ },
75
+ {
76
+ entityId: 'KUBERNETES_NODE-03169E17F0A60BE7',
77
+ expectedType: 'dt.entity.kubernetes_node',
78
+ description: 'KUBERNETES_NODE entity ID',
79
+ },
80
+ {
81
+ entityId: 'SERVICE_INSTANCE-02072B90BE476A9D',
82
+ expectedType: 'dt.entity.service_instance',
83
+ description: 'SERVICE_INSTANCE entity ID',
84
+ },
85
+ {
86
+ entityId: 'PROCESS_GROUP_INSTANCE-02072B90BE476A9D',
87
+ expectedType: 'dt.entity.process_group_instance',
88
+ description: 'PROCESS_GROUP_INSTANCE entity ID',
89
+ },
90
+ {
91
+ entityId: 'EC2_INSTANCE-4F09B64F7C5EC72D',
92
+ expectedType: 'dt.entity.ec2_instance',
93
+ description: 'EC2_INSTANCE entity ID',
94
+ },
95
+ {
96
+ entityId: 'AWS_LAMBDA_FUNCTION-65CB1C926C7A1C03',
97
+ expectedType: 'dt.entity.aws_lambda_function',
98
+ description: 'AWS_LAMBDA_FUNCTION entity ID',
99
+ },
100
+ {
101
+ entityId: 'CLOUD_APPLICATION_INSTANCE-00BAA09E6EC91E39',
102
+ expectedType: 'dt.entity.cloud_application_instance',
103
+ description: 'CLOUD_APPLICATION_INSTANCE entity ID',
104
+ },
105
+ {
106
+ entityId: 'DCG_INSTANCE-02072B90BE476A9D',
107
+ expectedType: 'dt.entity.docker_container_group_instance',
108
+ description: 'DCG_INSTANCE entity ID',
109
+ },
110
+ {
111
+ entityId: 'OS-006B30874F10C837',
112
+ expectedType: 'dt.entity.os',
113
+ description: 'OS entity ID',
114
+ },
115
+ {
116
+ entityId: 'AWS_AVAILABILITY_ZONE-0BAF92AC03BF3E25',
117
+ expectedType: 'dt.entity.aws_availability_zone',
118
+ description: 'AWS_AVAILABILITY_ZONE entity ID',
119
+ },
120
+ {
121
+ entityId: 'AWS_APPLICATION_LOAD_BALANCER-52D56F3FD851FF0C',
122
+ expectedType: 'dt.entity.aws_application_load_balancer',
123
+ description: 'AWS_APPLICATION_LOAD_BALANCER entity ID',
124
+ },
125
+ {
126
+ entityId: 'AWS_NETWORK_LOAD_BALANCER-617A06EFED8F4AB4',
127
+ expectedType: 'dt.entity.aws_network_load_balancer',
128
+ description: 'AWS_NETWORK_LOAD_BALANCER entity ID',
129
+ },
130
+ {
131
+ entityId: 'SYNTHETIC_LOCATION-0000000000000010',
132
+ expectedType: 'dt.entity.synthetic_location',
133
+ description: 'SYNTHETIC_LOCATION entity ID',
134
+ },
135
+ {
136
+ entityId: 'GEOLOCATION-0000000000000000',
137
+ expectedType: 'dt.entity.geolocation',
138
+ description: 'GEOLOCATION entity ID',
139
+ },
140
+ {
141
+ entityId: 'GCP_ZONE-0C3C7E567EAAB95B',
142
+ expectedType: 'dt.entity.gcp_zone',
143
+ description: 'GCP_ZONE entity ID',
144
+ },
145
+ {
146
+ entityId: 'AZURE_VM-42EA5AB8F028E280',
147
+ expectedType: 'dt.entity.azure_vm',
148
+ description: 'AZURE_VM entity ID',
149
+ },
150
+ {
151
+ entityId: 'RELATIONAL_DATABASE_SERVICE-B4EDC0E1E1279494',
152
+ expectedType: 'dt.entity.relational_database_service',
153
+ description: 'RELATIONAL_DATABASE_SERVICE entity ID',
154
+ },
155
+ ];
156
+ it.each(verifiedEntityTypesTestCases)('should map $description to $expectedType', ({ entityId, expectedType }) => {
157
+ const result = (0, dynatrace_entity_types_1.getEntityTypeFromId)(entityId);
158
+ expect(result).toBe(expectedType);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getUserAgent = void 0;
4
+ const package_json_1 = require("../../package.json");
5
+ /**
6
+ * Generate a user agent string for Dynatrace MCP Server
7
+ * @returns User agent string in format: dynatrace-mcp-server/vX.X.X (platform-arch)
8
+ */
9
+ const getUserAgent = () => {
10
+ return `dynatrace-mcp-server/v${package_json_1.version} (${process.platform}-${process.arch})`;
11
+ };
12
+ exports.getUserAgent = getUserAgent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.5.0-rc.3",
3
+ "version": "0.5.0",
4
4
  "description": "Model Context Protocol (MCP) server for Dynatrace",
5
5
  "keywords": [
6
6
  "Dynatrace",
@@ -47,13 +47,13 @@
47
47
  "license": "MIT",
48
48
  "dependencies": {
49
49
  "@dynatrace-sdk/client-automation": "^5.3.0",
50
- "@dynatrace-sdk/client-classic-environment-v2": "^3.6.8",
51
50
  "@dynatrace-sdk/client-platform-management-service": "^1.6.3",
52
51
  "@dynatrace-sdk/client-query": "^1.18.1",
53
52
  "@dynatrace-sdk/shared-errors": "^1.0.0",
54
53
  "@modelcontextprotocol/sdk": "^1.8.0",
55
- "dotenv": "^16.4.7",
56
- "dt-app": "^0.140.1",
54
+ "commander": "^14.0.0",
55
+ "dotenv": "^17.2.1",
56
+ "dt-app": "^0.148.1",
57
57
  "zod-to-json-schema": "^3.24.5"
58
58
  },
59
59
  "devDependencies": {
@@ -1,9 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getLogsForEntity = void 0;
4
- const execute_dql_1 = require("./execute-dql");
5
- const getLogsForEntity = async (dtClient, entityId) => {
6
- const dql = `fetch logs | filter dt.source_entity == "${entityId}"`;
7
- return (0, execute_dql_1.executeDql)(dtClient, { query: dql });
8
- };
9
- exports.getLogsForEntity = getLogsForEntity;