Skip to content

Commit d250904

Browse files
patch: Slightly improve error handling when connecting the first time
1 parent 1644f30 commit d250904

File tree

3 files changed

+40
-99
lines changed

3 files changed

+40
-99
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased Changes
44

5+
- Improved error handling when initializing connection first time
6+
57
## 0.9.1
68

79
- Replaced file-based token cache with an in-memory cache to avoid writing credentials to disk. Tokens now reset on server restart.

src/index.ts

Lines changed: 27 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
#!/usr/bin/env node
22
import { EnvironmentInformationClient } from '@dynatrace-sdk/client-platform-management-service';
3-
import {
4-
ClientRequestError,
5-
isApiClientError,
6-
isApiGatewayError,
7-
isClientRequestError,
8-
} from '@dynatrace-sdk/shared-errors';
3+
import { isClientRequestError } from '@dynatrace-sdk/shared-errors';
94
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
105
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
116
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -22,7 +17,6 @@ import {
2217
} from '@modelcontextprotocol/sdk/types.js';
2318
import { config } from 'dotenv';
2419
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
25-
import { randomUUID } from 'node:crypto';
2620
import { Command } from 'commander';
2721
import { z, ZodRawShape, ZodTypeAny } from 'zod';
2822

@@ -48,9 +42,8 @@ import {
4842
import { DynatraceEnv, getDynatraceEnv } from './getDynatraceEnv';
4943
import { createTelemetry, Telemetry } from './utils/telemetry-openkit';
5044
import { getEntityTypeFromId } from './utils/dynatrace-entity-types';
51-
import { Http2ServerRequest } from 'node:http2';
5245
import { resetGrailBudgetTracker, getGrailBudgetTracker } from './utils/grail-budget-tracker';
53-
import { read } from 'node:fs';
46+
import { handleClientRequestError } from './utils/dynatrace-connection-utils';
5447

5548
// Load environment variables from .env file if available, and suppress warnings/logging to stdio
5649
// as it breaks MCP communication when using stdio transport
@@ -71,6 +64,7 @@ if (dotEnvOutput.error) {
7164

7265
const DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID = 'dt0s08.dt-app-local'; // ToDo: Register our own oauth client
7366

67+
// Base Scopes for MCP Server tools
7468
let scopesBase = [
7569
'app-engine:apps:run', // needed for environmentInformationClient
7670
'app-engine:functions:run', // needed for environmentInformationClient
@@ -110,80 +104,6 @@ const allRequiredScopes = scopesBase.concat([
110104
'email:emails:send', // Send emails
111105
]);
112106

113-
/**
114-
* Performs a connection test to the Dynatrace environment.
115-
* Throws an error if the connection or authentication fails.
116-
*/
117-
async function testDynatraceConnection(
118-
dtEnvironment: string,
119-
oauthClientId?: string,
120-
oauthClientSecret?: string,
121-
dtPlatformToken?: string,
122-
) {
123-
const dtClient = await createDtHttpClient(
124-
dtEnvironment,
125-
oauthClientId && !oauthClientSecret ? allRequiredScopes : scopesBase,
126-
oauthClientId,
127-
oauthClientSecret,
128-
dtPlatformToken,
129-
);
130-
const environmentInformationClient = new EnvironmentInformationClient(dtClient);
131-
// This call will fail if authentication is incorrect.
132-
await environmentInformationClient.getEnvironmentInformation();
133-
}
134-
135-
function handleClientRequestError(error: ClientRequestError): string {
136-
let additionalErrorInformation = '';
137-
if (error.response.status === 403) {
138-
additionalErrorInformation =
139-
'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
140-
}
141-
142-
return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
143-
}
144-
145-
/**
146-
* Try to connect to Dynatrace environment with retries and exponential backoff.
147-
*/
148-
async function retryTestDynatraceConnection(
149-
dtEnvironment: string,
150-
oauthClientId?: string,
151-
oauthClientSecret?: string,
152-
dtPlatformToken?: string,
153-
) {
154-
let retryCount = 0;
155-
const maxRetries = 3; // Max retries
156-
const delayMs = 2000; // Initial delay of 2 seconds
157-
while (true) {
158-
try {
159-
console.error(
160-
`Testing connection to Dynatrace environment: ${dtEnvironment}... (Attempt ${retryCount + 1} of ${maxRetries})`,
161-
);
162-
await testDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
163-
console.error(`Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
164-
break;
165-
} catch (error: any) {
166-
console.error(`Error: Could not connect to the Dynatrace environment at ${dtEnvironment}.`);
167-
if (isClientRequestError(error)) {
168-
console.error(handleClientRequestError(error));
169-
} else {
170-
console.error(`Error: ${error.message}`);
171-
}
172-
retryCount++;
173-
if (retryCount >= maxRetries) {
174-
console.error(`Fatal: Maximum number of connection retries (${maxRetries}) exceeded. Exiting.`);
175-
throw new Error(
176-
`Failed to connect to Dynatrace environment ${dtEnvironment} after ${maxRetries} attempts. Most likely your configuration is incorrect. Last error: ${error.message}`,
177-
{ cause: error },
178-
);
179-
}
180-
const delay = Math.pow(2, retryCount) * delayMs; // Exponential backoff
181-
console.error(`Retrying in ${delay / 1000} seconds...`);
182-
await new Promise((resolve) => setTimeout(resolve, delay));
183-
}
184-
}
185-
}
186-
187107
const main = async () => {
188108
console.error(`Initializing Dynatrace MCP Server v${getPackageJsonVersion()}...`);
189109

@@ -207,21 +127,6 @@ const main = async () => {
207127
oauthClientId = DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID; // Default OAuth client ID for auth code flow
208128
}
209129

210-
// Test connection on startup
211-
try {
212-
// Depending on the authentication type, there are multiple pitfalls
213-
// * For Platform Tokens, we can just try to access "get environment info" and we will know whether it works
214-
// * For Oauth Client Credentials flow, we can also try to request an access token upfront with limited scopes, and verify whether everything works
215-
// * 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
216-
await retryTestDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken);
217-
} catch (err) {
218-
console.error((err as Error).message);
219-
process.exit(2);
220-
}
221-
222-
// Ready to start the server
223-
console.error(`Starting Dynatrace MCP Server v${getPackageJsonVersion()}...`);
224-
225130
// Initialize usage tracking
226131
const telemetry = createTelemetry();
227132
await telemetry.trackMcpServerStart();
@@ -265,6 +170,29 @@ const main = async () => {
265170
);
266171
};
267172

173+
// Try to establish a Dynatrace connection upfront, to see if everything is configured properly
174+
try {
175+
console.error(`Testing connection to Dynatrace environment: ${dtEnvironment}...`);
176+
const dtClient = await createAuthenticatedHttpClient(scopesBase);
177+
const environmentInformationClient = new EnvironmentInformationClient(dtClient);
178+
179+
await environmentInformationClient.getEnvironmentInformation();
180+
181+
console.error(`✅ Successfully connected to the Dynatrace environment at ${dtEnvironment}.`);
182+
} catch (error: any) {
183+
if (isClientRequestError(error)) {
184+
console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, handleClientRequestError(error));
185+
} else {
186+
console.error(`❌ Failed to connect to Dynatrace environment ${dtEnvironment}:`, error.message);
187+
// Logging more exhaustive error details for troubleshooting
188+
console.error(error);
189+
}
190+
process.exit(2);
191+
}
192+
193+
// Ready to start the server
194+
console.error(`Starting Dynatrace MCP Server v${getPackageJsonVersion()}...`);
195+
268196
// quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
269197
const tool = (
270198
name: string,
@@ -297,7 +225,7 @@ const main = async () => {
297225
isError: true,
298226
};
299227
}
300-
// else: We don't know what kind of error happened - best-case we can provide error.message
228+
// 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
301229
console.log(error);
302230
return {
303231
content: [{ type: 'text', text: `Error: ${error.message}` }],
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ClientRequestError } from '@dynatrace-sdk/shared-errors';
2+
3+
export function handleClientRequestError(error: ClientRequestError): string {
4+
let additionalErrorInformation = '';
5+
if (error.response.status === 403) {
6+
additionalErrorInformation =
7+
'Note: Your user or service-user is most likely lacking the necessary permissions/scopes for this API Call.';
8+
}
9+
10+
return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`;
11+
}

0 commit comments

Comments
 (0)