@dynatrace-oss/dynatrace-mcp-server 0.9.0 → 0.9.2

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.
@@ -80,7 +80,8 @@ const createOAuthClientCredentialsHttpClient = async (environmentUrl, scopes, cl
80
80
  };
81
81
  /** Create an OAuth Client using authorization code flow (interactive authentication)
82
82
  * This starts a local HTTP server to handle the OAuth redirect and requires user interaction.
83
- * Implements token caching (via .dt-mcp/token.json) to avoid repeated OAuth flows.
83
+ * Implements an in-memory token cache (not persisted to disk). After every server restart a new
84
+ * authentication flow (or token refresh) may be required.
84
85
  * Note: Always requests a complete set of scopes for maximum token reusability. Else the user will end up having to approve multiple requests.
85
86
  */
86
87
  const createOAuthAuthCodeFlowHttpClient = async (environmentUrl, scopes, clientId) => {
@@ -1,149 +1,54 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.globalTokenCache = exports.FileTokenCache = void 0;
37
- const fs = __importStar(require("fs"));
38
- const path = __importStar(require("path"));
3
+ exports.globalTokenCache = exports.InMemoryTokenCache = void 0;
39
4
  /**
40
- * File-based token cache implementation that persists tokens to disk
41
- * Stores tokens in .dt-mcp/token.json for persistence across dynatrace-mcp-server restarts
5
+ * In-memory token cache implementation (no persistence across process restarts).
6
+ * The previous implementation stored tokens on disk in `.dt-mcp/token.json` this has been
7
+ * intentionally removed to avoid writing credentials to the local filesystem. A new login /
8
+ * OAuth authorization code flow (or token retrieval) will be required after every server restart.
42
9
  */
43
- class FileTokenCache {
44
- tokenFilePath;
10
+ class InMemoryTokenCache {
45
11
  token = null;
46
- constructor() {
47
- // Create .dt-mcp directory in the current working directory
48
- const tokenDir = path.join(process.cwd(), '.dt-mcp');
49
- this.tokenFilePath = path.join(tokenDir, 'token.json');
50
- // Ensure the directory exists
51
- if (!fs.existsSync(tokenDir)) {
52
- fs.mkdirSync(tokenDir, { recursive: true });
53
- }
54
- this.loadToken();
55
- }
56
- /**
57
- * Loads the token from the file system
58
- */
59
- loadToken() {
60
- try {
61
- if (fs.existsSync(this.tokenFilePath)) {
62
- const tokenData = fs.readFileSync(this.tokenFilePath, 'utf8');
63
- this.token = JSON.parse(tokenData);
64
- console.error(`🔍 Loaded token from file: ${this.tokenFilePath}`);
65
- }
66
- else {
67
- console.error(`🔍 No token file found at: ${this.tokenFilePath}`);
68
- this.token = null;
69
- }
70
- }
71
- catch (error) {
72
- console.error(`❌ Failed to load token from file: ${error}`);
73
- this.token = null;
74
- }
75
- }
76
- /**
77
- * Saves the token to the file system
78
- */
79
- saveToken() {
80
- try {
81
- if (this.token) {
82
- fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.token, null, 2), 'utf8');
83
- console.error(`✅ Saved token to file: ${this.tokenFilePath}`);
84
- }
85
- else {
86
- // Remove the file if no token exists
87
- if (fs.existsSync(this.tokenFilePath)) {
88
- fs.unlinkSync(this.tokenFilePath);
89
- console.error(`🗑️ Removed token file: ${this.tokenFilePath}`);
90
- }
91
- }
92
- }
93
- catch (error) {
94
- console.error(`❌ Failed to save token to file: ${error}`);
95
- }
96
- }
97
12
  /**
98
13
  * Retrieves the cached token (ignores scopes since we use a global token)
99
14
  */
100
15
  getToken(scopes) {
101
- // We ignore the scopes parameter since we use a single token with all scopes
16
+ // Scopes parameter ignored single global token covers all requested scopes.
102
17
  return this.token;
103
18
  }
104
19
  /**
105
20
  * Stores the global token in the cache and persists it to file
106
21
  */
107
22
  setToken(scopes, token) {
108
- // We ignore the scopes parameter since we use a single token with all scopes
109
23
  this.token = {
110
24
  access_token: token.access_token,
111
25
  refresh_token: token.refresh_token,
112
26
  expires_at: token.expires_in ? Date.now() + token.expires_in * 1000 : undefined,
113
- scopes: [...scopes], // Store the actual scopes that were granted
27
+ scopes: [...scopes],
114
28
  };
115
- this.saveToken();
116
29
  }
117
30
  /**
118
31
  * Removes the cached token and deletes the file
119
32
  */
120
33
  clearToken(scopes) {
121
- // We ignore the scopes parameter since we use a single global token
122
34
  this.token = null;
123
- this.saveToken();
124
35
  }
125
36
  /**
126
37
  * Checks if the token exists and is still valid (not expired)
127
38
  */
128
39
  isTokenValid(scopes) {
129
40
  // We ignore the scopes parameter since we use a single token with all scopes
130
- if (!this.token) {
131
- console.error(`🔍 Token validation: No token in cache`);
41
+ if (!this.token)
132
42
  return false;
133
- }
134
- // If no expiration time is set, assume token is valid
135
- if (!this.token.expires_at) {
136
- console.error(`🔍 Token validation: Token has no expiration, assuming valid`);
137
- return true;
138
- }
43
+ if (!this.token.expires_at)
44
+ return true; // treat as non-expiring
139
45
  // Add a 30-second buffer to avoid using tokens that are about to expire
140
46
  const bufferMs = 30 * 1000; // 30 seconds
141
47
  const now = Date.now();
142
48
  const expiresAt = this.token.expires_at;
143
- const isValid = now + bufferMs < expiresAt;
144
- return isValid;
49
+ return now + bufferMs < expiresAt;
145
50
  }
146
51
  }
147
- exports.FileTokenCache = FileTokenCache;
148
- // Global token cache instance - uses file-based persistence
149
- exports.globalTokenCache = new FileTokenCache();
52
+ exports.InMemoryTokenCache = InMemoryTokenCache;
53
+ // Global token cache instance - In-memory only
54
+ exports.globalTokenCache = new InMemoryTokenCache();
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ const getDynatraceEnv_1 = require("./getDynatraceEnv");
27
27
  const telemetry_openkit_1 = require("./utils/telemetry-openkit");
28
28
  const dynatrace_entity_types_1 = require("./utils/dynatrace-entity-types");
29
29
  const grail_budget_tracker_1 = require("./utils/grail-budget-tracker");
30
+ const dynatrace_connection_utils_1 = require("./utils/dynatrace-connection-utils");
30
31
  // Load environment variables from .env file if available, and suppress warnings/logging to stdio
31
32
  // as it breaks MCP communication when using stdio transport
32
33
  const dotEnvOutput = (0, dotenv_1.config)({ quiet: true });
@@ -42,6 +43,7 @@ else {
42
43
  console.error(`.env file loaded successfully - loaded ${dotEnvOutput.parsed ? Object.keys(dotEnvOutput.parsed).length : 0} environment variables: ${Object.keys(dotEnvOutput.parsed || {}).join(', ')}`);
43
44
  }
44
45
  const DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID = 'dt0s08.dt-app-local'; // ToDo: Register our own oauth client
46
+ // Base Scopes for MCP Server tools
45
47
  let scopesBase = [
46
48
  'app-engine:apps:run', // needed for environmentInformationClient
47
49
  'app-engine:functions:run', // needed for environmentInformationClient
@@ -53,7 +55,8 @@ const allRequiredScopes = scopesBase.concat([
53
55
  'storage:events:read', // Read events from Grail
54
56
  'storage:buckets:read', // Read all system data stored on Grail
55
57
  'storage:security.events:read', // Read Security events from Grail
56
- 'storage:entities:read', // Read Entities from Grail
58
+ 'storage:entities:read', // Read classic Entities
59
+ 'storage:smartscape:read', // Read Smartscape Entities from Grail
57
60
  'storage:logs:read', // Read logs for reliability guardian validations
58
61
  'storage:metrics:read', // Read metrics for reliability guardian validations
59
62
  'storage:bizevents:read', // Read bizevents for reliability guardian validations
@@ -74,57 +77,6 @@ const allRequiredScopes = scopesBase.concat([
74
77
  // Communication scopes
75
78
  'email:emails:send', // Send emails
76
79
  ]);
77
- /**
78
- * Performs a connection test to the Dynatrace environment.
79
- * Throws an error if the connection or authentication fails.
80
- */
81
- async function testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken) {
82
- const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, oauthClientId && !oauthClientSecret ? allRequiredScopes : scopesBase, oauthClientId, oauthClientSecret, dtPlatformToken);
83
- const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
84
- // This call will fail if authentication is incorrect.
85
- await environmentInformationClient.getEnvironmentInformation();
86
- }
87
- function handleClientRequestError(error) {
88
- let additionalErrorInformation = '';
89
- if (error.response.status === 403) {
90
- additionalErrorInformation =
91
- 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
92
- }
93
- return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
94
- }
95
- /**
96
- * Try to connect to Dynatrace environment with retries and exponential backoff.
97
- */
98
- async function retryTestDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken) {
99
- let retryCount = 0;
100
- const maxRetries = 3; // Max retries
101
- const delayMs = 2000; // Initial delay of 2 seconds
102
- while (true) {
103
- try {
104
- console.error(`Testing connection to Dynatrace environment: ${dtEnvironment}... (Attempt ${retryCount + 1} of ${maxRetries})`);
105
- await testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
106
- console.error(`Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
107
- break;
108
- }
109
- catch (error) {
110
- console.error(`Error: Could not connect to the Dynatrace environment at ${dtEnvironment}.`);
111
- if ((0, shared_errors_1.isClientRequestError)(error)) {
112
- console.error(handleClientRequestError(error));
113
- }
114
- else {
115
- console.error(`Error: ${error.message}`);
116
- }
117
- retryCount++;
118
- if (retryCount >= maxRetries) {
119
- console.error(`Fatal: Maximum number of connection retries (${maxRetries}) exceeded. Exiting.`);
120
- throw new Error(`Failed to connect to Dynatrace environment ${dtEnvironment} after ${maxRetries} attempts. Most likely your configuration is incorrect. Last error: ${error.message}`, { cause: error });
121
- }
122
- const delay = Math.pow(2, retryCount) * delayMs; // Exponential backoff
123
- console.error(`Retrying in ${delay / 1000} seconds...`);
124
- await new Promise((resolve) => setTimeout(resolve, delay));
125
- }
126
- }
127
- }
128
80
  const main = async () => {
129
81
  console.error(`Initializing Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
130
82
  // read Environment variables
@@ -144,20 +96,6 @@ const main = async () => {
144
96
  console.error('No OAuth credentials or platform token provided - switching to OAuth authorization code flow.');
145
97
  oauthClientId = DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID; // Default OAuth client ID for auth code flow
146
98
  }
147
- // Test connection on startup
148
- try {
149
- // Depending on the authentication type, there are multiple pitfalls
150
- // * For Platform Tokens, we can just try to access "get environment info" and we will know whether it works
151
- // * For Oauth Client Credentials flow, we can also try to request an access token upfront with limited scopes, and verify whether everything works
152
- // * for Oauth Auth Code flow, we can only verify whether the client ID is valid and the OAuth verifier call works, but we can't verify whether the user will be able to authenticate successfully
153
- await retryTestDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
154
- }
155
- catch (err) {
156
- console.error(err.message);
157
- process.exit(2);
158
- }
159
- // Ready to start the server
160
- console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
161
99
  // Initialize usage tracking
162
100
  const telemetry = (0, telemetry_openkit_1.createTelemetry)();
163
101
  await telemetry.trackMcpServerStart();
@@ -189,6 +127,51 @@ const main = async () => {
189
127
  return await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, oauthClientId && !oauthClientSecret ? allRequiredScopes : scopes, // Always use all scopes for maximum reusability
190
128
  oauthClientId, oauthClientSecret, dtPlatformToken);
191
129
  };
130
+ // Try to establish a Dynatrace connection upfront, to see if everything is configured properly
131
+ console.error(`Testing connection to Dynatrace environment: ${dtEnvironment}...`);
132
+ // First, we will try a simple "fetch" to connect to dtEnvironment, without authentication
133
+ // This should help to see if DNS lookup works, TCP connection can be established, and TLS handshake works
134
+ try {
135
+ const response = await fetch(`${dtEnvironment}`).then((response) => response.text());
136
+ // check response
137
+ if (response && response.length > 0) {
138
+ if (response.includes('Authentication required')) {
139
+ // all good - we reached the environment and authentication is required, which is going to be the next step
140
+ }
141
+ else {
142
+ console.error(`⚠️ Tried to contact ${dtEnvironment}, got the following response: ${response}`);
143
+ // Note: We won't error out yet, but this information could already be helpful for troubleshooting
144
+ }
145
+ }
146
+ else {
147
+ throw new Error('No response received');
148
+ }
149
+ }
150
+ catch (error) {
151
+ console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, error.message);
152
+ console.error(error);
153
+ process.exit(3);
154
+ }
155
+ // Second, we will try with proper authentication
156
+ try {
157
+ const dtClient = await createAuthenticatedHttpClient(scopesBase);
158
+ const environmentInformationClient = new client_platform_management_service_1.EnvironmentInformationClient(dtClient);
159
+ await environmentInformationClient.getEnvironmentInformation();
160
+ console.error(`✅ Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
161
+ }
162
+ catch (error) {
163
+ if ((0, shared_errors_1.isClientRequestError)(error)) {
164
+ console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, (0, dynatrace_connection_utils_1.handleClientRequestError)(error));
165
+ }
166
+ else {
167
+ console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, error.message);
168
+ // Logging more exhaustive error details for troubleshooting
169
+ console.error(error);
170
+ }
171
+ process.exit(2);
172
+ }
173
+ // Ready to start the server
174
+ console.error(`Starting Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
192
175
  // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
193
176
  const tool = (name, description, paramsSchema, annotations, cb) => {
194
177
  const wrappedCb = async (args) => {
@@ -210,11 +193,11 @@ const main = async () => {
210
193
  // check if it's an error originating from the Dynatrace SDK / API Gateway and provide an appropriate message to the user
211
194
  if ((0, shared_errors_1.isClientRequestError)(error)) {
212
195
  return {
213
- content: [{ type: 'text', text: handleClientRequestError(error) }],
196
+ content: [{ type: 'text', text: (0, dynatrace_connection_utils_1.handleClientRequestError)(error) }],
214
197
  isError: true,
215
198
  };
216
199
  }
217
- // else: We don't know what kind of error happened - best-case we can provide error.message
200
+ // else: We don't know what kind of error happened - best case we can log the error and provide error.message as a tool response
218
201
  console.log(error);
219
202
  return {
220
203
  content: [{ type: 'text', text: `Error: ${error.message}` }],
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleClientRequestError = handleClientRequestError;
4
+ function handleClientRequestError(error) {
5
+ let additionalErrorInformation = '';
6
+ if (error.response.status === 403) {
7
+ additionalErrorInformation =
8
+ 'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
9
+ }
10
+ return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynatrace-oss/dynatrace-mcp-server",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "mcpName": "io.github.dynatrace-oss/Dynatrace-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Dynatrace",
6
6
  "keywords": [