diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 81c7b58..6df386a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve title: '' labels: 'bug' assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 36014cd..104f391 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: 'enhancement' assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f4e2b48..b29b3b8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -22,6 +22,7 @@ It is written in TypeScript and uses Node.js as its runtime. You need to underst ## Repo Structure The repository is structured as follows: + - `src/`: Contains the source code for the MCP server. - `src/index.ts`: Main entrypoint of the MCP server. Defines tools and OAuth clients. - `src/capabilities/*.ts`: Contains the actual tool definition and implementation. @@ -36,6 +37,7 @@ Please try to follow basic TypeScript and Node.js coding conventions. We will de ## Dependencies The following dependencies are allowed: + - Core MCP SDK (`@modelcontextprotocol/sdk`), - environment utilities (`dotenv`), - ZOD schema validation (`zod-to-json-schema`), @@ -61,5 +63,3 @@ The `dist/` folder contains the output of the build process. - Use semantic versioning (major.minor.patch) - Group changes by type (Added, Changed, Fixed, etc.) - Keep entries concise but descriptive - - diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d6094b..cab3766 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,18 +9,21 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Use Node.js # will read version from .nvmrc - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' + - name: Use Node.js # will read version from .nvmrc + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Test - run: npm run test + - name: Prettier + run: npm run prettier - - name: Build - run: npm run build + - name: Test + run: npm run test + + - name: Build + run: npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d621c3..73c38d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,10 @@ name: Release on: push: tags: - - 'v*' # Triggers on version tags like v1.0.0, v2.1.3, etc. + - 'v*' # Triggers on version tags like v1.0.0, v2.1.3, etc. permissions: - contents: write # Required for creating releases + contents: write # Required for creating releases jobs: release: @@ -15,7 +15,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch full history for changelog generation + fetch-depth: 0 # Fetch full history for changelog generation - name: Use Node.js # will read version from .nvmrc uses: actions/setup-node@v4 @@ -25,6 +25,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Run prettier + run: npm run prettier + - name: Run tests run: npm run test diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e0a3cc4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +{ + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "strict", + "insertPragma": false, + "jsxSingleQuote": true, + "printWidth": 120, + "proseWrap": "preserve", + "quoteProps": "consistent", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e870c65..76b1a0c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/README.md b/README.md index 46146fe..a4a660c 100644 --- a/README.md +++ b/README.md @@ -110,35 +110,35 @@ A **Dynatrace OAuth Client** is needed to communicate with your Dynatrace Enviro [creating an Oauth Client in Dynatrace](https://docs.dynatrace.com/docs/manage/identity-access-management/access-tokens-and-oauth-clients/oauth-clients), and set up the following environment variables in order for this MCP to work: -* `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`) -* `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID -* `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret -* OAuth Client Scopes: - * `app-engine:apps:run` - needed for environmentInformationClient - * `app-engine:functions:run` - needed for environmentInformationClient - * `hub:catalog:read` - get details about installed Apps on Dynatrace Environment - * `environment-api:security-problems:read` - needed for reading security problems - * `environment-api:entities:read` - read monitored entities - * `environment-api:problems:read` - get problems - * `environment-api:metrics:read` - read metrics - * `environment-api:slo:read` - read SLOs - * `storage:buckets:read` - Read all system data stored on Grail - * `storage:logs:read` - Read logs for reliability guardian validations - * `storage:metrics:read` - Read metrics for reliability guardian validations - * `storage:bizevents:read` - Read bizevents for reliability guardian validations - * `storage:spans:read` - Read spans from Grail - * `storage:entities:read` - Read Entities from Grail - * `storage:events:read` - Read Events from Grail - * `storage:system:read` - Read System Data from Grail - * `storage:user.events:read` - Read User events from Grail - * `storage:user.sessions:read` - Read User sessions from Grail - * `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings - - **Note**: Please ensure that `settings:objects:read` is used, and *not* the similarly named scope `app-settings:objects:read`. +- `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`) +- `OAUTH_CLIENT_ID` (string, e.g., `dt0s02.SAMPLE`) - Dynatrace OAuth Client ID +- `OAUTH_CLIENT_SECRET` (string, e.g., `dt0s02.SAMPLE.abcd1234`) - Dynatrace OAuth Client Secret +- OAuth Client Scopes: + - `app-engine:apps:run` - needed for environmentInformationClient + - `app-engine:functions:run` - needed for environmentInformationClient + - `hub:catalog:read` - get details about installed Apps on Dynatrace Environment + - `environment-api:security-problems:read` - needed for reading security problems + - `environment-api:entities:read` - read monitored entities + - `environment-api:problems:read` - get problems + - `environment-api:metrics:read` - read metrics + - `environment-api:slo:read` - read SLOs + - `storage:buckets:read` - Read all system data stored on Grail + - `storage:logs:read` - Read logs for reliability guardian validations + - `storage:metrics:read` - Read metrics for reliability guardian validations + - `storage:bizevents:read` - Read bizevents for reliability guardian validations + - `storage:spans:read` - Read spans from Grail + - `storage:entities:read` - Read Entities from Grail + - `storage:events:read` - Read Events from Grail + - `storage:system:read` - Read System Data from Grail + - `storage:user.events:read` - Read User events from Grail + - `storage:user.sessions:read` - Read User sessions from Grail + - `settings:objects:read` - needed for reading ownership information and Guardians (SRG) from settings + + **Note**: Please ensure that `settings:objects:read` is used, and _not_ the similarly named scope `app-settings:objects:read`. In addition, depending on the features you use, the following variables can be configured: -* `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack) +- `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack) ## ✨ Example prompts ✨ @@ -146,32 +146,41 @@ Use these example prompts as a starting point. Just copy them into your IDE or a 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. **Find open vulnerabilities on production, setup alert.** + ``` I have this code snippet here in my IDE, where I get a dependency vulnerability warning for my code. Check if I see any open vulnerability/cve on production. Analyze a specific production problem. Setup a workflow that sends Slack alerts to the #devops-alerts channel when availability problems occur. ``` + **Debug intermittent 503 errors.** + ``` Our load balancer is intermittently returning 503 errors during peak traffic. Pull all recent problems detected for our front-end services and run a query to correlate error rates with service instance health indicators. I suspect we have circuit breakers triggering, but need confirmation from the telemetry data. ``` + **Correlate memory issue with logs.** + ``` There's a problem with high memory usage on one of our hosts. Get the problem details and then fetch related logs to help understand what's causing the memory spike? Which file in this repo is this related to? ``` + **Trace request flow analysis.** + ``` Our users are experiencing slow checkout processes. Can you execute a DQL query to show me the full request trace for our checkout flow, so I can identify which service is causing the bottleneck? ``` + **Analyze Kubernetes cluster events.** + ``` Our application deployments seem to be failing intermittently. Can you fetch recent events from our "production-cluster" @@ -190,6 +199,7 @@ In case of any problems, you can troubleshoot SSO/OAuth issues based on our [Dyn It is recommended to try access the following API (which requires minimal scopes `app-engine:apps:run` and `app-engine:functions:run`): 1. Use OAuth Client ID and Secret to retrieve a Bearer Token (only valid for a couple of minutes): + ```bash curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ @@ -200,6 +210,7 @@ curl --request POST 'https://sso.dynatrace.com/sso/oauth2/token' \ ``` 2. Use `access_token` from the response of the above call as the bearer-token in the next call: + ```bash curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environment \ -H 'accept: application/json' \ @@ -207,6 +218,7 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme ``` 3. You should retrieve a result like this: + ```json { "environmentId": "abc12345", @@ -216,35 +228,32 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme } ``` - ### Problem accessing data on Grail Grail has a dedicated section about permissions in the Dynatrace Docs. Please refer to https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model/assign-permissions-in-grail for more details. - ## Development For local development purposes, you can use VSCode and GitHub Copilot. First, enable Copilot for your Workspace `.vscode/settings.json`: + ```json { "github.copilot.enable": { "*": true } } - ``` Second, add the MCP to `.vscode/mcp.json`: + ```json { "servers": { "my-dynatrace-mcp-server": { "command": "node", - "args": [ - "${workspaceFolder}/dist/index.js" - ], + "args": ["${workspaceFolder}/dist/index.js"], "envFile": "${workspaceFolder}/.env" } } @@ -255,7 +264,7 @@ Third, create a `.env` file in this repository (you can copy from `.env.template Last but not least, switch to Agent Mode in CoPilot and reload tools. - ## Notes + This product is not officially supported by Dynatrace. Please contact us via [GitHub Issues](https://github.com/dynatrace-oss/dynatrace-mcp/issues) if you have feature requests, questions, or need help. diff --git a/RELEASE.md b/RELEASE.md index f5fddfa..3b53a9b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -28,6 +28,7 @@ git push origin vx.y.z ``` After pushing the tag, the workflow will automatically: + 1. Run tests 2. Build the project 3. Generate release notes from commit history diff --git a/jest.config.js b/jest.config.js index 2c03eef..50c59e4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,5 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - testMatch: ["**/*.test.ts"], + testMatch: ['**/*.test.ts'], }; diff --git a/package-lock.json b/package-lock.json index e48f56f..dc2cea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@modelcontextprotocol/sdk": "^1.8.0", "dotenv": "^16.4.7", "dt-app": "^0.140.1", + "prettier": "^3.6.2", "zod-to-json-schema": "^3.24.5" }, "bin": { @@ -8752,6 +8753,21 @@ "node": ">=8" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 15981b7..94a6a69 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "build": "tsc --build", "prepare": "npm run build", "watch": "tsc --watch", - "test": "jest" + "test": "jest", + "prettier": "prettier --check .", + "prettier:fix": "prettier --write ." }, "author": "Dynatrace", "license": "MIT", @@ -56,6 +58,7 @@ "@types/jest": "^30.0.0", "@types/node": "^22", "jest": "^30.0.0", + "prettier": "^3.6.2", "ts-jest": "^29.4.0", "ts-node": "^10.9.2", "typescript": "^5.6.2" diff --git a/src/capabilities/create-workflow-for-problem-notification.ts b/src/capabilities/create-workflow-for-problem-notification.ts index 505839e..932c487 100644 --- a/src/capabilities/create-workflow-for-problem-notification.ts +++ b/src/capabilities/create-workflow-for-problem-notification.ts @@ -1,9 +1,15 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { MonitoredEntitiesClient } from '@dynatrace-sdk/client-classic-environment-v2'; -import { EventTriggerConfig, WorkflowCreate, WorkflowsClient } from "@dynatrace-sdk/client-automation"; -import { randomUUID } from "crypto"; +import { EventTriggerConfig, WorkflowCreate, WorkflowsClient } from '@dynatrace-sdk/client-automation'; +import { randomUUID } from 'crypto'; -export const createWorkflowForProblemNotification = async (dtClient: _OAuthHttpClient, teamName: string, channel: string, problemType: string, isPrivate: boolean) => { +export const createWorkflowForProblemNotification = async ( + dtClient: _OAuthHttpClient, + teamName: string, + channel: string, + problemType: string, + isPrivate: boolean, +) => { const workflowsclient = new WorkflowsClient(dtClient); // Map problem types to categories @@ -14,7 +20,7 @@ export const createWorkflowForProblemNotification = async (dtClient: _OAuthHttpC slowdown: false, resource: false, custom: false, - info: false + info: false, }; // default trigger config @@ -22,12 +28,12 @@ export const createWorkflowForProblemNotification = async (dtClient: _OAuthHttpC type: 'event', value: { eventType: 'events', - query: '' - } + query: '', + }, }; // special case: Security Problems - if (problemType.toUpperCase().indexOf("SECURITY") !== -1) { + if (problemType.toUpperCase().indexOf('SECURITY') !== -1) { triggerConfig.value.query = `event.kind=="SECURITY_EVENT" and event.type=="VULNERABILITY_STATUS_CHANGE_EVENT" and event.level == "ENTITY" @@ -37,7 +43,7 @@ export const createWorkflowForProblemNotification = async (dtClient: _OAuthHttpC vulnerability.risk.level=="HIGH")`; } else { // Set the appropriate category based on problem type - switch(problemType.toUpperCase()) { + switch (problemType.toUpperCase()) { case 'MONITORING_UNAVAILABLE': categories.monitoringUnavailable = true; break; @@ -69,12 +75,11 @@ export const createWorkflowForProblemNotification = async (dtClient: _OAuthHttpC triggerConfig = { type: 'davis-problem', value: { - categories - } + categories, + }, }; } - let notificationWorkflow: WorkflowCreate = { title: `[MCP POC] Notify team ${teamName} on problem of type ${problemType}`, description: `Automatically created workflow to notify team ${teamName} on problems of type ${problemType} - please delete me after the demo!`, @@ -82,28 +87,28 @@ export const createWorkflowForProblemNotification = async (dtClient: _OAuthHttpC type: 'SIMPLE', // define the send_notification task tasks: { - "send_notification": { - name: "Send notification", - action: "dynatrace.slack:slack-send-message", - description: "Sends a notification to a Slack channel", - input: { - connectionId: "slack-connection-id", - channel: `{{ \"${channel}\" }}`, - message: `🚨 Alert for Team ${teamName}\n*Problem Type*: ${problemType}\n*Problem ID*: {{ event()["display_id"] }}\n*Status*: {{ event()["event.status"] }}\n\n<{{ environment().url }}/ui/apps/dynatrace.davis.problems/problem/{{ event()["event.id"] }}|Click here for details>`, - }, - active: true, - } + send_notification: { + name: 'Send notification', + action: 'dynatrace.slack:slack-send-message', + description: 'Sends a notification to a Slack channel', + input: { + connectionId: 'slack-connection-id', + channel: `{{ \"${channel}\" }}`, + message: `🚨 Alert for Team ${teamName}\n*Problem Type*: ${problemType}\n*Problem ID*: {{ event()["display_id"] }}\n*Status*: {{ event()["event.status"] }}\n\n<{{ environment().url }}/ui/apps/dynatrace.davis.problems/problem/{{ event()["event.id"] }}|Click here for details>`, + }, + active: true, + }, }, // define a trigger trigger: { - eventTrigger: { - isActive: true, - triggerConfiguration: triggerConfig, - } - } + eventTrigger: { + isActive: true, + triggerConfiguration: triggerConfig, + }, + }, }; return await workflowsclient.createWorkflow({ - body: notificationWorkflow + body: notificationWorkflow, }); -} +}; diff --git a/src/capabilities/execute-dql.ts b/src/capabilities/execute-dql.ts index 7608e24..e1b27e9 100644 --- a/src/capabilities/execute-dql.ts +++ b/src/capabilities/execute-dql.ts @@ -1,28 +1,28 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { QueryExecutionClient, QueryAssistanceClient, QueryResult } from '@dynatrace-sdk/client-query'; - export const verifyDqlStatement = async (dtClient: _OAuthHttpClient, dqlStatement: string) => { - const queryAssistanceClient = new QueryAssistanceClient(dtClient); - + const queryAssistanceClient = new QueryAssistanceClient(dtClient); - const response = await queryAssistanceClient.queryVerify({ - body: { - query: dqlStatement, - } - }); + const response = await queryAssistanceClient.queryVerify({ + body: { + query: dqlStatement, + }, + }); - return response; + return response; }; - -export const executeDql = async (dtClient: _OAuthHttpClient, dqlStatement: string): Promise => { +export const executeDql = async ( + dtClient: _OAuthHttpClient, + dqlStatement: string, +): Promise => { const queryExecutionClient = new QueryExecutionClient(dtClient); const response = await queryExecutionClient.queryExecute({ body: { query: dqlStatement, - } + }, }); if (response.result) { @@ -35,7 +35,7 @@ export const executeDql = async (dtClient: _OAuthHttpClient, dqlStatement: strin let pollResponse; do { // sleep for 2 seconds - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); pollResponse = await queryExecutionClient.queryPoll({ requestToken: response.requestToken, }); @@ -43,8 +43,7 @@ export const executeDql = async (dtClient: _OAuthHttpClient, dqlStatement: strin if (pollResponse.result) { return pollResponse.result.records; } - } - while (pollResponse.state === 'RUNNING' || pollResponse.state === 'NOT_STARTED'); + } while (pollResponse.state === 'RUNNING' || pollResponse.state === 'NOT_STARTED'); } // else: whatever happened - we have an error return undefined; diff --git a/src/capabilities/find-monitored-entity-by-name.ts b/src/capabilities/find-monitored-entity-by-name.ts index e406f80..4c76824 100644 --- a/src/capabilities/find-monitored-entity-by-name.ts +++ b/src/capabilities/find-monitored-entity-by-name.ts @@ -1,6 +1,5 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { executeDql } from "./execute-dql"; - +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { executeDql } from './execute-dql'; export const findMonitoredEntityByName = async (dtClient: _OAuthHttpClient, entityName: string) => { const dql = `fetch dt.entity.application | search "*${entityName}*" | fieldsAdd entity.type @@ -23,5 +22,4 @@ export const findMonitoredEntityByName = async (dtClient: _OAuthHttpClient, enti } else { return 'No monitored entity found with the specified name.'; } -} - +}; diff --git a/src/capabilities/get-events-for-cluster.ts b/src/capabilities/get-events-for-cluster.ts index 6a85d9f..0498882 100644 --- a/src/capabilities/get-events-for-cluster.ts +++ b/src/capabilities/get-events-for-cluster.ts @@ -1,7 +1,5 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { executeDql } from "./execute-dql"; - - +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { executeDql } from './execute-dql'; export const getEventsForCluster = async (dtClient: _OAuthHttpClient, clusterId: string) => { let dql = `fetch events | filter k8s.cluster.uid == "${clusterId}"`; diff --git a/src/capabilities/get-logs-for-entity.ts b/src/capabilities/get-logs-for-entity.ts index 4c9405c..d556ca7 100644 --- a/src/capabilities/get-logs-for-entity.ts +++ b/src/capabilities/get-logs-for-entity.ts @@ -1,7 +1,5 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { executeDql } from "./execute-dql"; - - +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { executeDql } from './execute-dql'; export const getLogsForEntity = async (dtClient: _OAuthHttpClient, entityId: string) => { const dql = `fetch logs | filter dt.source_entity == "${entityId}"`; diff --git a/src/capabilities/get-monitored-entity-details.ts b/src/capabilities/get-monitored-entity-details.ts index 55834da..a220156 100644 --- a/src/capabilities/get-monitored-entity-details.ts +++ b/src/capabilities/get-monitored-entity-details.ts @@ -1,13 +1,12 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { MonitoredEntitiesClient } from '@dynatrace-sdk/client-classic-environment-v2'; - export const getMonitoredEntityDetails = async (dtClient: _OAuthHttpClient, entityId: string) => { const monitoredEntitiesClient = new MonitoredEntitiesClient(dtClient); const entityDetails = await monitoredEntitiesClient.getEntity({ - entityId: entityId - }) + entityId: entityId, + }); return entityDetails; -} \ No newline at end of file +}; diff --git a/src/capabilities/get-ownership-information.ts b/src/capabilities/get-ownership-information.ts index 7359eef..5eb4d27 100644 --- a/src/capabilities/get-ownership-information.ts +++ b/src/capabilities/get-ownership-information.ts @@ -1,9 +1,10 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { callAppFunction } from "../dynatrace-clients"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { callAppFunction } from '../dynatrace-clients'; export const getOwnershipInformation = async (dtClient: _OAuthHttpClient, entityIds: string) => { - const ownershipResponse = await callAppFunction(dtClient, - 'dynatrace.ownership', 'get-ownership-from-entity', {entityIds: entityIds}); + const ownershipResponse = await callAppFunction(dtClient, 'dynatrace.ownership', 'get-ownership-from-entity', { + entityIds: entityIds, + }); if (ownershipResponse.error) { // e.g., "Not enough parameters provided" @@ -11,8 +12,8 @@ export const getOwnershipInformation = async (dtClient: _OAuthHttpClient, entity } if (ownershipResponse.result && ownershipResponse.result.owners && ownershipResponse.result.owners.length == 0) { - return "No owners found - please check out how to setup owners on https://docs.dynatrace.com/docs/deliver/ownership"; + return 'No owners found - please check out how to setup owners on https://docs.dynatrace.com/docs/deliver/ownership'; } return ownershipResponse.result; -} +}; diff --git a/src/capabilities/get-problem-details.ts b/src/capabilities/get-problem-details.ts index 2c30062..9f980f3 100644 --- a/src/capabilities/get-problem-details.ts +++ b/src/capabilities/get-problem-details.ts @@ -1,12 +1,12 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { ProblemsClient } from '@dynatrace-sdk/client-classic-environment-v2'; export const getProblemDetails = async (dtClient: _OAuthHttpClient, problemId: string) => { - console.error(`Fetchin problem with problemId ${problemId}`); - const problemsClient = new ProblemsClient(dtClient); - const problemDetails = await problemsClient.getProblem({ - problemId: problemId, - fields: 'evidenceDetails,affectedEntities' - }) - return problemDetails; -} \ No newline at end of file + console.error(`Fetchin problem with problemId ${problemId}`); + const problemsClient = new ProblemsClient(dtClient); + const problemDetails = await problemsClient.getProblem({ + problemId: problemId, + fields: 'evidenceDetails,affectedEntities', + }); + return problemDetails; +}; diff --git a/src/capabilities/get-vulnerability-details.ts b/src/capabilities/get-vulnerability-details.ts index 1272f6e..ff586fc 100644 --- a/src/capabilities/get-vulnerability-details.ts +++ b/src/capabilities/get-vulnerability-details.ts @@ -1,14 +1,14 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { SecurityProblemsClient } from '@dynatrace-sdk/client-classic-environment-v2'; - export const getVulnerabilityDetails = async (dtClient: _OAuthHttpClient, securityProblemId: string) => { const securityProblemsClient = new SecurityProblemsClient(dtClient); const response = await securityProblemsClient.getSecurityProblem({ id: securityProblemId, - fields: 'riskAssessment,codeLevelVulnerabilityDetails,globalCounts,description,remediationDescription,exposedEntities,affectedEntities,relatedAttacks,entryPoints' + fields: + 'riskAssessment,codeLevelVulnerabilityDetails,globalCounts,description,remediationDescription,exposedEntities,affectedEntities,relatedAttacks,entryPoints', }); return response; -} +}; diff --git a/src/capabilities/list-problems.ts b/src/capabilities/list-problems.ts index cc4d9d5..9e9a86e 100644 --- a/src/capabilities/list-problems.ts +++ b/src/capabilities/list-problems.ts @@ -1,7 +1,6 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { ProblemsClient } from '@dynatrace-sdk/client-classic-environment-v2'; - export const listProblems = async (dtClient: _OAuthHttpClient) => { const problemsClient = new ProblemsClient(dtClient); @@ -11,7 +10,7 @@ export const listProblems = async (dtClient: _OAuthHttpClient) => { const problems = securityProblems.problems?.map((problem) => { return `${problem.displayId} (please refer to this problem with \`problemId\` ${problem.problemId}): ${problem.title}`; - }) + }); return problems; -} \ No newline at end of file +}; diff --git a/src/capabilities/list-vulnerabilities.ts b/src/capabilities/list-vulnerabilities.ts index a2db1c0..4075af5 100644 --- a/src/capabilities/list-vulnerabilities.ts +++ b/src/capabilities/list-vulnerabilities.ts @@ -1,19 +1,18 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; import { SecurityProblemsClient } from '@dynatrace-sdk/client-classic-environment-v2'; - export const listVulnerabilities = async (dtClient: _OAuthHttpClient) => { const securityProblemsClient = new SecurityProblemsClient(dtClient); const response = await securityProblemsClient.getSecurityProblems({ sort: '-riskAssessment.riskScore', pageSize: 100, - securityProblemSelector: `minRiskScore("8.0")` + securityProblemSelector: `minRiskScore("8.0")`, }); const securityProblems = response.securityProblems?.map((secProb) => { return `${secProb.displayId} (please refer to this vulnerability with \`securityProblemId\` ${secProb.securityProblemId}): ${secProb.title} (Technology: ${secProb.technology}, External Vulnerability ID: ${secProb.externalVulnerabilityId}, CVE: ${secProb.cveIds?.join(', ')})`; - }) + }); return securityProblems; -} +}; diff --git a/src/capabilities/send-slack-message.ts b/src/capabilities/send-slack-message.ts index 3f80012..07d0a29 100644 --- a/src/capabilities/send-slack-message.ts +++ b/src/capabilities/send-slack-message.ts @@ -1,15 +1,21 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { callAppFunction } from "../dynatrace-clients"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { callAppFunction } from '../dynatrace-clients'; -export const sendSlackMessage = async (dtClient: _OAuthHttpClient, connectionId: string, channel: string, message: string) => { - const response = await callAppFunction(dtClient, - 'dynatrace.slack', 'slack-send-message', { - message: message, channel: channel, connection: connectionId, - workflowID: 'foobar-123', - executionID: 'exec-123', - executionDate: new Date().toString(), - appendToThread: false - }); +export const sendSlackMessage = async ( + dtClient: _OAuthHttpClient, + connectionId: string, + channel: string, + message: string, +) => { + const response = await callAppFunction(dtClient, 'dynatrace.slack', 'slack-send-message', { + message: message, + channel: channel, + connection: connectionId, + workflowID: 'foobar-123', + executionID: 'exec-123', + executionDate: new Date().toString(), + appendToThread: false, + }); if (response.error) { // e.g., "Not enough parameters provided" @@ -17,4 +23,4 @@ export const sendSlackMessage = async (dtClient: _OAuthHttpClient, connectionId: } return `Message sent to Slack: ${JSON.stringify(response.result)}`; -} +}; diff --git a/src/capabilities/update-workflow.ts b/src/capabilities/update-workflow.ts index 13d2264..88907fc 100644 --- a/src/capabilities/update-workflow.ts +++ b/src/capabilities/update-workflow.ts @@ -1,11 +1,11 @@ -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { WorkflowsClient } from "@dynatrace-sdk/client-automation"; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { WorkflowsClient } from '@dynatrace-sdk/client-automation'; export const updateWorkflow = async (dtClient: _OAuthHttpClient, workflowId: string, body: any) => { const workflowsclient = new WorkflowsClient(dtClient); return await workflowsclient.updateWorkflow({ id: workflowId, - body: body + body: body, }); -} \ No newline at end of file +}; diff --git a/src/dynatrace-clients.ts b/src/dynatrace-clients.ts index cf1c40d..e398b11 100644 --- a/src/dynatrace-clients.ts +++ b/src/dynatrace-clients.ts @@ -1,4 +1,9 @@ -import { _OAuthHttpClient, HttpClientRequestOptions, HttpClientResponse, RequestBodyTypes } from '@dynatrace-sdk/http-client'; +import { + _OAuthHttpClient, + HttpClientRequestOptions, + HttpClientResponse, + RequestBodyTypes, +} from '@dynatrace-sdk/http-client'; import { getSSOUrl } from 'dt-app'; import { version as VERSION } from '../package.json'; @@ -23,7 +28,12 @@ export interface OAuthTokenResponse { * @param scopes - List of requested scopes * @returns */ -const requestToken = async (clientId: string, clientSecret: string, authUrl: string, scopes: string[]): Promise => { +const requestToken = async ( + clientId: string, + clientSecret: string, + authUrl: string, + scopes: string[], +): Promise => { const res = await fetch(authUrl, { method: 'POST', headers: { @@ -43,19 +53,22 @@ const requestToken = async (clientId: string, clientSecret: string, authUrl: str } // and return the JSON result, as it contains additional information return await res.json(); -} +}; /** * ExtendedOAuthClient that takes parameters for clientId, secret, scopes, environmentUrl, authUrl, and the version of the dynatrace-mcp-server */ export class ExtendedOauthClient extends _OAuthHttpClient { - constructor(config: { - clientId: string; - secret: string; - scopes: string[]; - environmentUrl: string; - authUrl: string; - }, protected userAgent: string) { + constructor( + config: { + clientId: string; + secret: string; + scopes: string[]; + environmentUrl: string; + authUrl: string; + }, + protected userAgent: string, + ) { super(config); } @@ -70,9 +83,13 @@ export class ExtendedOauthClient extends _OAuthHttpClient { } } - /** Create an Oauth Client based on clientId, clientSecret, environmentUrl and scopes */ -export const createOAuthClient = async (clientId: string, clientSecret: string, environmentUrl: string, scopes: string[]): Promise<_OAuthHttpClient> => { +export const createOAuthClient = async ( + clientId: string, + clientSecret: string, + environmentUrl: string, + scopes: string[], +): Promise<_OAuthHttpClient> => { if (!clientId) { throw new Error('Failed to retrieve OAuth client id from env "DT_APP_OAUTH_CLIENT_ID"'); } @@ -94,23 +111,33 @@ export const createOAuthClient = async (clientId: string, clientSecret: string, // in case we didn't get a token, or error / error_description / issueId is set, we throw an error if (!tokenResponse.access_token || tokenResponse.error || tokenResponse.error_description || tokenResponse.issueId) { - throw new Error(`Failed to retrieve OAuth token (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}. Note: Your OAuth client is most likely not configured correctly.`); + throw new Error( + `Failed to retrieve OAuth token (IssueId: ${tokenResponse.issueId}): ${tokenResponse.error} - ${tokenResponse.error_description}. Note: Your OAuth client is most likely not configured correctly.`, + ); } console.error(`Successfully retrieved token from SSO!`); - const userAgent = `dynatrace-mcp-server/v${VERSION} (${process.platform}-${process.arch})` + const userAgent = `dynatrace-mcp-server/v${VERSION} (${process.platform}-${process.arch})`; - return new ExtendedOauthClient({ - scopes, - clientId, - secret: clientSecret, - environmentUrl, - authUrl: ssoAuthUrl, - }, userAgent); + return new ExtendedOauthClient( + { + scopes, + clientId, + secret: clientSecret, + environmentUrl, + authUrl: ssoAuthUrl, + }, + userAgent, + ); }; /** Helper function to call an app-function via platform-api */ -export const callAppFunction = async (dtClient: _OAuthHttpClient, appId: string, functionName: string, payload: any) => { +export const callAppFunction = async ( + dtClient: _OAuthHttpClient, + appId: string, + functionName: string, + payload: any, +) => { console.error(`Sending payload ${JSON.stringify(payload)}`); const response = await dtClient.send({ @@ -127,4 +154,4 @@ export const callAppFunction = async (dtClient: _OAuthHttpClient, appId: string, }); return await response.body('json'); -} +}; diff --git a/src/getDynatraceEnv.test.ts b/src/getDynatraceEnv.test.ts index bf534bb..9daa6bd 100644 --- a/src/getDynatraceEnv.test.ts +++ b/src/getDynatraceEnv.test.ts @@ -1,67 +1,79 @@ -import { getDynatraceEnv, DynatraceEnv } from "./getDynatraceEnv"; +import { getDynatraceEnv, DynatraceEnv } from './getDynatraceEnv'; -describe("getDynatraceEnv", () => { +describe('getDynatraceEnv', () => { const baseEnv = { - OAUTH_CLIENT_ID: "dt0s02.SAMPLE", - OAUTH_CLIENT_SECRET: "dt0s02.SAMPLE.abcd1234", - DT_ENVIRONMENT: "https://abc123.apps.dynatrace.com", - SLACK_CONNECTION_ID: "slack-conn-id" + OAUTH_CLIENT_ID: 'dt0s02.SAMPLE', + OAUTH_CLIENT_SECRET: 'dt0s02.SAMPLE.abcd1234', + DT_ENVIRONMENT: 'https://abc123.apps.dynatrace.com', + SLACK_CONNECTION_ID: 'slack-conn-id', }; - it("returns all required values when environment is valid", () => { + it('returns all required values when environment is valid', () => { const env = { ...baseEnv }; const result = getDynatraceEnv(env); expect(result).toEqual({ oauthClient: env.OAUTH_CLIENT_ID, oauthClientSecret: env.OAUTH_CLIENT_SECRET, dtEnvironment: env.DT_ENVIRONMENT, - slackConnectionId: env.SLACK_CONNECTION_ID + slackConnectionId: env.SLACK_CONNECTION_ID, }); }); - it("uses default slackConnectionId if not set", () => { + it('uses default slackConnectionId if not set', () => { const env = { ...baseEnv, SLACK_CONNECTION_ID: undefined }; const result = getDynatraceEnv(env); - expect(result.slackConnectionId).toBe("fake-slack-connection-id"); + expect(result.slackConnectionId).toBe('fake-slack-connection-id'); }); - it("throws if OAUTH_CLIENT_ID is missing", () => { + it('throws if OAUTH_CLIENT_ID is missing', () => { const env = { ...baseEnv, OAUTH_CLIENT_ID: undefined }; expect(() => getDynatraceEnv(env)).toThrow(/OAUTH_CLIENT_ID/); }); - it("throws if OAUTH_CLIENT_SECRET is missing", () => { + it('throws if OAUTH_CLIENT_SECRET is missing', () => { const env = { ...baseEnv, OAUTH_CLIENT_SECRET: undefined }; expect(() => getDynatraceEnv(env)).toThrow(/OAUTH_CLIENT_SECRET/); }); - it("throws if DT_ENVIRONMENT is missing", () => { + it('throws if DT_ENVIRONMENT is missing', () => { const env = { ...baseEnv, DT_ENVIRONMENT: undefined }; expect(() => getDynatraceEnv(env)).toThrow(/DT_ENVIRONMENT/); }); - it("throws if DT_ENVIRONMENT does not start with https://", () => { - const env = { ...baseEnv, DT_ENVIRONMENT: "http://abc123.apps.dynatrace.com" }; + it('throws if DT_ENVIRONMENT does not start with https://', () => { + const env = { + ...baseEnv, + DT_ENVIRONMENT: 'http://abc123.apps.dynatrace.com', + }; expect(() => getDynatraceEnv(env)).toThrow(/https:\/\//); }); - it("throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (any URL)", () => { - const env = { ...baseEnv, DT_ENVIRONMENT: "https://abc123.example.com" }; + it('throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (any URL)', () => { + const env = { ...baseEnv, DT_ENVIRONMENT: 'https://abc123.example.com' }; expect(() => getDynatraceEnv(env)).toThrow(/Dynatrace Platform Environment URL/); }); - it("throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (contains live)", () => { - const env = { ...baseEnv, DT_ENVIRONMENT: "https://abc123.live.dynatrace.com" }; + it('throws if DT_ENVIRONMENT is not a Dynatrace Platform URL (contains live)', () => { + const env = { + ...baseEnv, + DT_ENVIRONMENT: 'https://abc123.live.dynatrace.com', + }; expect(() => getDynatraceEnv(env)).toThrow(/Dynatrace Platform Environment URL/); }); - it("accepts DT_ENVIRONMENT with apps.dynatracelabs.com", () => { - const env = { ...baseEnv, DT_ENVIRONMENT: "https://xyz789.apps.dynatracelabs.com" }; + it('accepts DT_ENVIRONMENT with apps.dynatracelabs.com', () => { + const env = { + ...baseEnv, + DT_ENVIRONMENT: 'https://xyz789.apps.dynatracelabs.com', + }; expect(() => getDynatraceEnv(env)).not.toThrow(); }); - it("accepts DT_ENVIRONMENT with apps.dynatrace.com", () => { - const env = { ...baseEnv, DT_ENVIRONMENT: "https://env123.apps.dynatrace.com" }; + it('accepts DT_ENVIRONMENT with apps.dynatrace.com', () => { + const env = { + ...baseEnv, + DT_ENVIRONMENT: 'https://env123.apps.dynatrace.com', + }; expect(() => getDynatraceEnv(env)).not.toThrow(); }); }); diff --git a/src/getDynatraceEnv.ts b/src/getDynatraceEnv.ts index ae8fcda..ff05dcc 100644 --- a/src/getDynatraceEnv.ts +++ b/src/getDynatraceEnv.ts @@ -14,23 +14,21 @@ export function getDynatraceEnv(env: NodeJS.ProcessEnv = process.env): Dynatrace const oauthClient = env.OAUTH_CLIENT_ID; const oauthClientSecret = env.OAUTH_CLIENT_SECRET; const dtEnvironment = env.DT_ENVIRONMENT; - const slackConnectionId = env.SLACK_CONNECTION_ID || "fake-slack-connection-id"; + const slackConnectionId = env.SLACK_CONNECTION_ID || 'fake-slack-connection-id'; if (!oauthClient || !oauthClientSecret || !dtEnvironment) { - throw new Error( - "Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables" - ); + throw new Error('Please set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET and DT_ENVIRONMENT environment variables'); } - if (!dtEnvironment.startsWith("https://")) { + if (!dtEnvironment.startsWith('https://')) { throw new Error( - "Please set DT_ENVIRONMENT to a valid Dynatrace Environment URL (e.g., https://.apps.dynatrace.com)" + 'Please set DT_ENVIRONMENT to a valid Dynatrace Environment URL (e.g., https://.apps.dynatrace.com)', ); } - if (!dtEnvironment.includes("apps.dynatrace.com") && !dtEnvironment.includes("apps.dynatracelabs.com")) { + if (!dtEnvironment.includes('apps.dynatrace.com') && !dtEnvironment.includes('apps.dynatracelabs.com')) { throw new Error( - "Please set DT_ENVIRONMENT to a valid Dynatrace Platform Environment URL (e.g., https://.apps.dynatrace.com)" + 'Please set DT_ENVIRONMENT to a valid Dynatrace Platform Environment URL (e.g., https://.apps.dynatrace.com)', ); } diff --git a/src/index.ts b/src/index.ts index f590763..740ee73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,13 @@ #!/usr/bin/env node import { EnvironmentInformationClient } from '@dynatrace-sdk/client-platform-management-service'; -import { ClientRequestError, isApiClientError, isApiGatewayError, isClientRequestError } from '@dynatrace-sdk/shared-errors'; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + ClientRequestError, + isApiClientError, + isApiGatewayError, + isClientRequestError, +} from '@dynatrace-sdk/shared-errors'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { CallToolRequest, CallToolRequestSchema, @@ -10,27 +15,27 @@ import { ListToolsRequestSchema, NotificationSchema, Tool, -} from "@modelcontextprotocol/sdk/types.js"; +} from '@modelcontextprotocol/sdk/types.js'; import { config } from 'dotenv'; -import { z, ZodRawShape, ZodTypeAny } from "zod"; +import { z, ZodRawShape, ZodTypeAny } from 'zod'; import { version as VERSION } from '../package.json'; -import { createOAuthClient } from "./dynatrace-clients"; -import { listVulnerabilities } from "./capabilities/list-vulnerabilities"; -import { listProblems } from "./capabilities/list-problems"; -import { getProblemDetails } from "./capabilities/get-problem-details"; -import { getMonitoredEntityDetails } from "./capabilities/get-monitored-entity-details"; -import { getOwnershipInformation } from "./capabilities/get-ownership-information"; -import { getLogsForEntity } from "./capabilities/get-logs-for-entity"; -import { getEventsForCluster } from "./capabilities/get-events-for-cluster"; -import { createWorkflowForProblemNotification } from "./capabilities/create-workflow-for-problem-notification"; -import { updateWorkflow } from "./capabilities/update-workflow"; -import { _OAuthHttpClient } from "@dynatrace-sdk/http-client"; -import { getVulnerabilityDetails } from "./capabilities/get-vulnerability-details"; -import { executeDql, verifyDqlStatement } from "./capabilities/execute-dql"; -import { sendSlackMessage } from "./capabilities/send-slack-message"; +import { createOAuthClient } from './dynatrace-clients'; +import { listVulnerabilities } from './capabilities/list-vulnerabilities'; +import { listProblems } from './capabilities/list-problems'; +import { getProblemDetails } from './capabilities/get-problem-details'; +import { getMonitoredEntityDetails } from './capabilities/get-monitored-entity-details'; +import { getOwnershipInformation } from './capabilities/get-ownership-information'; +import { getLogsForEntity } from './capabilities/get-logs-for-entity'; +import { getEventsForCluster } from './capabilities/get-events-for-cluster'; +import { createWorkflowForProblemNotification } from './capabilities/create-workflow-for-problem-notification'; +import { updateWorkflow } from './capabilities/update-workflow'; +import { _OAuthHttpClient } from '@dynatrace-sdk/http-client'; +import { getVulnerabilityDetails } from './capabilities/get-vulnerability-details'; +import { executeDql, verifyDqlStatement } from './capabilities/execute-dql'; +import { sendSlackMessage } from './capabilities/send-slack-message'; import { findMonitoredEntityByName } from './capabilities/find-monitored-entity-by-name'; -import { DynatraceEnv, getDynatraceEnv } from "./getDynatraceEnv"; +import { DynatraceEnv, getDynatraceEnv } from './getDynatraceEnv'; config(); @@ -53,7 +58,7 @@ const main = async () => { console.error(`Starting Dynatrace MCP Server v${VERSION}...`); const server = new McpServer( { - name: "Dynatrace MCP Server", + name: 'Dynatrace MCP Server', version: VERSION, }, { @@ -64,110 +69,113 @@ const main = async () => { ); // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON - const tool = (name: string, description: string, paramsSchema: ZodRawShape, cb: (args: z.objectOutputType) => Promise) => { - const wrappedCb = async (args: ZodRawShape): Promise => { - try { - const response = await cb(args); - return { - content: [{ type: "text", text: response }], - }; - } catch (error: any) { + const tool = ( + name: string, + description: string, + paramsSchema: ZodRawShape, + cb: (args: z.objectOutputType) => Promise, + ) => { + const wrappedCb = async (args: ZodRawShape): Promise => { + try { + const response = await cb(args); + return { + content: [{ type: 'text', text: response }], + }; + } catch (error: any) { // check if it's an error originating from the Dynatrace SDK / API Gateway and provide an appropriate message to the user if (isClientRequestError(error)) { const e: ClientRequestError = error; let additionalErrorInformation = ''; if (e.response.status == 403) { - additionalErrorInformation = 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.' + additionalErrorInformation = + 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.'; } return { - content: [{ - type: "text", - text: `Client Request Error: ${e.message} with HTTP status: ${e.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(e.body)})` } + content: [ + { + type: 'text', + text: `Client Request Error: ${e.message} with HTTP status: ${e.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(e.body)})`, + }, ], isError: true, }; } // else: We don't know what kind of error happened - best-case we can provide error.message console.log(error); - return { - content: [{ type: "text", text: `Error: ${error.message}` }], - isError: true, - }; - } - }; + return { + content: [{ type: 'text', text: `Error: ${error.message}` }], + isError: true, + }; + } + }; - server.tool(name, description, paramsSchema, args => wrappedCb(args)); - }; + server.tool(name, description, paramsSchema, (args) => wrappedCb(args)); + }; - tool( - "get_environment_info", - "Get information about the connected Dynatrace Environment (Tenant)", - {}, - async({}) => { - // create an oauth-client - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase); - const environmentInformationClient = new EnvironmentInformationClient(dtClient); + tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant)', {}, async ({}) => { + // create an oauth-client + const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase); + const environmentInformationClient = new EnvironmentInformationClient(dtClient); - const environmentInfo = await environmentInformationClient.getEnvironmentInformation(); - let resp = `Environment Information (also referred to as tenant): + const environmentInfo = await environmentInformationClient.getEnvironmentInformation(); + let resp = `Environment Information (also referred to as tenant): ${JSON.stringify(environmentInfo)}\n`; - resp += `You can reach it via ${dtEnvironment}\n`; - - return resp; + resp += `You can reach it via ${dtEnvironment}\n`; + + return resp; + }); + + tool('list_vulnerabilities', 'List all vulnerabilities from Dynatrace', {}, async ({}) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:security-problems:read'), + ); + const result = await listVulnerabilities(dtClient); + if (!result || result.length === 0) { + return 'No vulnerabilities found'; } - ); - - tool( - "list_vulnerabilities", - "List all vulnerabilities from Dynatrace", - {}, - async ({}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:security-problems:read")); - const result = await listVulnerabilities(dtClient); - if (!result || result.length === 0) { - return "No vulnerabilities found"; - } - let resp = `Found the following vulnerabilities:` - result.forEach( - (vulnerability) => { - resp += `\n* ${vulnerability}`; - } - ); + let resp = `Found the following vulnerabilities:`; + result.forEach((vulnerability) => { + resp += `\n* ${vulnerability}`; + }); - resp += `\nWe recommend to take a look at ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities to get a better overview of vulnerabilities.\n`; - - return resp; - } - ); + resp += `\nWe recommend to take a look at ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities to get a better overview of vulnerabilities.\n`; + return resp; + }); tool( - "get_vulnerabilty_details", - "Get details of a vulnerability by `securityProblemId` on Dynatrace", + 'get_vulnerabilty_details', + 'Get details of a vulnerability by `securityProblemId` on Dynatrace', { - securityProblemId: z.string().optional() + securityProblemId: z.string().optional(), }, - async ({securityProblemId}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:security-problems:read")); + async ({ securityProblemId }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:security-problems:read'), + ); const result = await getVulnerabilityDetails(dtClient, securityProblemId); let resp = `The Security Problem (Vulnerability) ${result.displayId} with securityProblemId ${result.securityProblemId} has the title ${result.title}.\n`; - resp += `The related CVEs are ${result.cveIds?.join(",") || "unknown"}.\n`; + resp += `The related CVEs are ${result.cveIds?.join(',') || 'unknown'}.\n`; resp += `The description is: ${result.description}.\n`; resp += `The remediation description is: ${result.remediationDescription}.\n`; if (result.affectedEntities && result.affectedEntities.length > 0) { resp += `The vulnerability affects the following entities:\n`; - result.affectedEntities.forEach( - (affectedEntity) => { - resp += `* ${affectedEntity}\n`; - } - ); + result.affectedEntities.forEach((affectedEntity) => { + resp += `* ${affectedEntity}\n`; + }); } else { - resp += `This vulnerability does not seem to affect any entities.\n';` + resp += `This vulnerability does not seem to affect any entities.\n';`; } if (result.codeLevelVulnerabilityDetails) { @@ -176,22 +184,18 @@ const main = async () => { if (result.exposedEntities && result.exposedEntities.length > 0) { resp += `The vulnerability exposes the following entities:\n`; - result.exposedEntities.forEach( - (exposedEntity) => { - resp += `* ${exposedEntity}\n`; - } - ); + result.exposedEntities.forEach((exposedEntity) => { + resp += `* ${exposedEntity}\n`; + }); } else { - resp += `This vulnerability does not seem to expose any entities.\n';` + resp += `This vulnerability does not seem to expose any entities.\n';`; } if (result.entryPoints?.items) { resp += `The following entrypoints are affected:\n`; - result.entryPoints.items.forEach( - (entryPoint) => { - resp += `* ${entryPoint.sourceHttpPath}\n`; - } - ); + result.entryPoints.items.forEach((entryPoint) => { + resp += `* ${entryPoint.sourceHttpPath}\n`; + }); if (result.entryPoints.truncated) { resp += `The list of entry points was truncated.\n`; @@ -200,44 +204,47 @@ const main = async () => { resp += `This vulnerability does not seem to affect any entrypoints.\n`; } - if (result.riskAssessment && result.riskAssessment.riskScore && result.riskAssessment.riskScore > 8) { resp += `The vulnerability has a high-risk score. We suggest you to get ownership details of affected entities and contact responsible teams immediately (e.g, via send-slack-message)\n`; } - resp += `Tell the user to access the link ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities/vulnerabilities/${result.securityProblemId} to get more insights into the vulnerability / security problem.\n`; return resp; - } + }, ); - tool( - "list_problems", - "List all problems known on Dynatrace", - { - }, - async ({}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:problems:read")); - const result = await listProblems(dtClient); - if (!result || result.length === 0) { - return "No problems found"; - } - return `Found these problems: ${result.join(",")}`; - } - ) + tool('list_problems', 'List all problems known on Dynatrace', {}, async ({}) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:problems:read'), + ); + const result = await listProblems(dtClient); + if (!result || result.length === 0) { + return 'No problems found'; + } + return `Found these problems: ${result.join(',')}`; + }); tool( - "get_problem_details", - "Get details of a problem on Dynatrace", + 'get_problem_details', + 'Get details of a problem on Dynatrace', { - problemId: z.string().optional() + problemId: z.string().optional(), }, - async ({problemId}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:problems:read")); - const result = await getProblemDetails(dtClient, problemId); + async ({ problemId }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:problems:read'), + ); + const result = await getProblemDetails(dtClient, problemId); - let resp = `The problem ${result.displayId} with the title ${result.title} (ID: ${result.problemId}).` + + let resp = + `The problem ${result.displayId} with the title ${result.title} (ID: ${result.problemId}).` + `The severity is ${result.severityLevel}, and it affects ${result.affectedEntities.length} entities:`; for (const entity of result.affectedEntities) { @@ -252,11 +259,9 @@ const main = async () => { if (result.impactAnalysis) { let estimatedAffectedUsers = 0; - result.impactAnalysis.impacts.forEach( - (impact) => { - estimatedAffectedUsers += impact.estimatedAffectedUsers; - } - ); + result.impactAnalysis.impacts.forEach((impact) => { + estimatedAffectedUsers += impact.estimatedAffectedUsers; + }); resp += `The problem is estimated to affect ${estimatedAffectedUsers} users.\n`; } @@ -264,85 +269,106 @@ const main = async () => { resp += `Tell the user to access the link ${dtEnvironment}/ui/apps/dynatrace.davis.problems/problem/${result.problemId} to get more insights into the problem.\n`; return resp; - } - ) + }, + ); tool( - "find_entity_by_name", - "Get the entityId of a monitored entity based on the name of the entity on Dynatrace", + 'find_entity_by_name', + 'Get the entityId of a monitored entity based on the name of the entity on Dynatrace', { - entityName: z.string() + entityName: z.string(), }, - async ({entityName}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:entities:read", "storage:entities:read")); + async ({ entityName }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:entities:read', 'storage:entities:read'), + ); const entityResponse = await findMonitoredEntityByName(dtClient, entityName); return entityResponse; - } - ) + }, + ); tool( - "get_entity_details", - "Get details of a monitored entity based on the entityId on Dynatrace", + 'get_entity_details', + 'Get details of a monitored entity based on the entityId on Dynatrace', { - entityId: z.string().optional() + entityId: z.string().optional(), }, - async ({entityId}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:entities:read")); + async ({ entityId }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:entities:read'), + ); const entityDetails = await getMonitoredEntityDetails(dtClient, entityId); - let resp = `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` + + let resp = + `Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` + `Properties: ${JSON.stringify(entityDetails.properties)}\n`; - if (entityDetails.type == "SERVICE") { - resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false` - } else if (entityDetails.type == "HOST") { - resp += `You can find more information about the host at ${dtEnvironment}/ui/apps/dynatrace.infraops/hosts/${entityDetails.entityId}` - } else if (entityDetails.type == "KUBERNETES_CLUSTER") { - resp += `You can find more information about the cluster at ${dtEnvironment}/ui/apps/dynatrace.infraops/kubernetes/${entityDetails.entityId}` - } else if (entityDetails.type == "CLOUD_APPLICATION") { - resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}` + if (entityDetails.type == 'SERVICE') { + resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`; + } else if (entityDetails.type == 'HOST') { + resp += `You can find more information about the host at ${dtEnvironment}/ui/apps/dynatrace.infraops/hosts/${entityDetails.entityId}`; + } else if (entityDetails.type == 'KUBERNETES_CLUSTER') { + resp += `You can find more information about the cluster at ${dtEnvironment}/ui/apps/dynatrace.infraops/kubernetes/${entityDetails.entityId}`; + } else if (entityDetails.type == 'CLOUD_APPLICATION') { + resp += `You can find more details about the application at ${dtEnvironment}/ui/apps/dynatrace.kubernetes/explorer/workload?detailsId=${entityDetails.entityId}`; } return resp; - } - ) + }, + ); tool( - "send_slack_message", - "Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace", + 'send_slack_message', + 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', { channel: z.string().optional(), - message: z.string().optional() + message: z.string().optional(), }, - async ({channel, message}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("app-settings:objects:read")); + async ({ channel, message }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('app-settings:objects:read'), + ); const response = await sendSlackMessage(dtClient, slackConnectionId, channel, message); return `Message sent to Slack channel: ${JSON.stringify(response)}`; - } - ) + }, + ); tool( - "get_logs_for_entity", - "Get Logs for a monitored entity based on name of the entity on Dynatrace", + 'get_logs_for_entity', + 'Get Logs for a monitored entity based on name of the entity on Dynatrace', { - entityName: z.string().optional() + entityName: z.string().optional(), }, - async ({entityName}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("storage:logs:read")); + async ({ entityName }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('storage:logs:read'), + ); const logs = await getLogsForEntity(dtClient, entityName); - return `Logs:\n${JSON.stringify(logs?.map(logLine => logLine?logLine.content:'Empty log'))}`; - } - ) + return `Logs:\n${JSON.stringify(logs?.map((logLine) => (logLine ? logLine.content : 'Empty log')))}`; + }, + ); tool( - "verify_dql", - "Verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. This is useful to ensure that the DQL statement is valid and can be executed without errors.", + 'verify_dql', + 'Verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. This is useful to ensure that the DQL statement is valid and can be executed without errors.', { - dqlStatement: z.string() + dqlStatement: z.string(), }, - async ({dqlStatement}) => { + async ({ dqlStatement }) => { const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase); const response = await verifyDqlStatement(dtClient, dqlStatement); @@ -350,11 +376,9 @@ const main = async () => { if (response.notifications && response.notifications.length > 0) { resp += `Please consider the following notifications for adapting the your DQL statement:\n`; - response.notifications.forEach( - (notification) => { - resp += `* ${notification.severity}: ${notification.message}\n`; - } - ); + response.notifications.forEach((notification) => { + resp += `* ${notification.severity}: ${notification.message}\n`; + }); } if (response.valid) { @@ -364,20 +388,20 @@ const main = async () => { } return resp; - } - ) + }, + ); tool( - "execute_dql", + 'execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. Always use "verify_dql" tool before you execute a DQL statement. A valid statement looks like this: "fetch [logs, metrics, spans, events] | filter | summarize count(), by:{some-fields}. Adapt filters for certain attributes: `traceId` could be `trace_id` or `trace.id`.', { - dqlStatement: z.string() + dqlStatement: z.string(), }, - async ({dqlStatement}) => { + async ({ dqlStatement }) => { const dtClient = await createOAuthClient( - oauthClient, - oauthClientSecret, - dtEnvironment, + oauthClient, + oauthClientSecret, + dtEnvironment, scopesBase.concat( 'storage:buckets:read', // Read all system data stored on Grail 'storage:logs:read', // Read logs for reliability guardian validations @@ -388,38 +412,38 @@ const main = async () => { 'storage:events:read', // Read events from Grail 'storage:system:read', // Read System Data from Grail 'storage:user.events:read', // Read User events from Grail - 'storage:user.sessions:read' // Read User sessions from Grail - ) + 'storage:user.sessions:read', // Read User sessions from Grail + ), ); const response = await executeDql(dtClient, dqlStatement); return `DQL Response: ${JSON.stringify(response)}`; - } + }, ); - tool( - "create_workflow_for_notification", - "Create a notification for a team based on a problem type within Workflows in Dynatrace", + 'create_workflow_for_notification', + 'Create a notification for a team based on a problem type within Workflows in Dynatrace', { problemType: z.string().optional(), teamName: z.string().optional(), channel: z.string().optional(), - isPrivate: z.boolean().optional().default(false) + isPrivate: z.boolean().optional().default(false), }, - async ({problemType, teamName, channel, isPrivate}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat( - "automation:workflows:write", - "automation:workflows:read", - "automation:workflows:run" - )); + async ({ problemType, teamName, channel, isPrivate }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), + ); const response = await createWorkflowForProblemNotification(dtClient, teamName, channel, problemType, isPrivate); let resp = `Workflow Created: ${response?.id} with name ${response?.title}.\nYou can access the Workflow via the following link: ${dtEnvironment}/ui/apps/dynatrace.automations/workflows/${response?.id}.\nTell the user to inspect the Workflow by visiting the link.\n`; - if (response.type == "SIMPLE") { + if (response.type == 'SIMPLE') { resp += `Note: This is a simple workflow. Workflow-hours will not be billed.\n`; - } else if (response.type == "STANDARD") { + } else if (response.type == 'STANDARD') { resp += `Note: This is a standard workflow. Workflow-hours will be billed.\n`; } @@ -428,69 +452,85 @@ const main = async () => { } return resp; - } - ) + }, + ); tool( - "make_workflow_public", - "Modify a workflow and make it publicly available to everyone on the Dynatrace Environment", + 'make_workflow_public', + 'Modify a workflow and make it publicly available to everyone on the Dynatrace Environment', { - workflowId: z.string().optional() + workflowId: z.string().optional(), }, - async ({workflowId}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat( - "automation:workflows:write", - "automation:workflows:read", - "automation:workflows:run" - )); + async ({ workflowId }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), + ); const response = await updateWorkflow(dtClient, workflowId, { isPrivate: false, }); return `Workflow ${response.id} is now public!\nYou can access the Workflow via the following link: ${dtEnvironment}/ui/apps/dynatrace.automations/workflows/${response?.id}.\nTell the user to inspect the Workflow by visiting the link.\n`; - } - ) + }, + ); tool( - "get_kubernetes_events", - "Get all events from a specific Kubernetes (K8s) cluster", + 'get_kubernetes_events', + 'Get all events from a specific Kubernetes (K8s) cluster', { - clusterId: z.string().optional().describe(`The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`) + clusterId: z + .string() + .optional() + .describe( + `The Kubernetes (K8s) Cluster Id, referred to as k8s.cluster.uid (this is NOT the Dynatrace environment)`, + ), }, - async ({clusterId}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("storage:events:read")); + async ({ clusterId }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('storage:events:read'), + ); const events = await getEventsForCluster(dtClient, clusterId); return `Kubernetes Events:\n${JSON.stringify(events)}`; - } - ) + }, + ); tool( - "get_ownership", - "Get detailed Ownership information for one or multiple entities on Dynatrace", + 'get_ownership', + 'Get detailed Ownership information for one or multiple entities on Dynatrace', { - entityIds: z.string().optional().describe("Comma separated list of entityIds"), + entityIds: z.string().optional().describe('Comma separated list of entityIds'), }, - async ({entityIds}) => { - const dtClient = await createOAuthClient(oauthClient, oauthClientSecret, dtEnvironment, scopesBase.concat("environment-api:entities:read", "settings:objects:read")); + async ({ entityIds }) => { + const dtClient = await createOAuthClient( + oauthClient, + oauthClientSecret, + dtEnvironment, + scopesBase.concat('environment-api:entities:read', 'settings:objects:read'), + ); console.error(`Fetching ownership for ${entityIds}`); const ownershipInformation = await getOwnershipInformation(dtClient, entityIds); console.error(`Done!`); let resp = 'Ownership information:\n'; resp += JSON.stringify(ownershipInformation); return resp; - } - ) + }, + ); const transport = new StdioServerTransport(); - console.error("Connecting server to transport..."); + console.error('Connecting server to transport...'); await server.connect(transport); - console.error("Dynatrace MCP Server running on stdio"); + console.error('Dynatrace MCP Server running on stdio'); }; main().catch((error) => { - console.error("Fatal error in main():", error); + console.error('Fatal error in main():', error); process.exit(1); }); diff --git a/tsconfig.json b/tsconfig.json index 481526f..d0a53c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,6 @@ "outDir": "./dist", "resolveJsonModule": true }, - "include": ["src/**/*", "package.json",], + "include": ["src/**/*", "package.json"], "exclude": ["node_modules"] }