11#!/usr/bin/env node
22import { 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' ;
94import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' ;
105import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' ;
116import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' ;
@@ -22,7 +17,6 @@ import {
2217} from '@modelcontextprotocol/sdk/types.js' ;
2318import { config } from 'dotenv' ;
2419import { createServer , IncomingMessage , ServerResponse } from 'node:http' ;
25- import { randomUUID } from 'node:crypto' ;
2620import { Command } from 'commander' ;
2721import { z , ZodRawShape , ZodTypeAny } from 'zod' ;
2822
@@ -48,9 +42,8 @@ import {
4842import { DynatraceEnv , getDynatraceEnv } from './getDynatraceEnv' ;
4943import { createTelemetry , Telemetry } from './utils/telemetry-openkit' ;
5044import { getEntityTypeFromId } from './utils/dynatrace-entity-types' ;
51- import { Http2ServerRequest } from 'node:http2' ;
5245import { 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
7265const 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
7468let 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-
187107const 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,51 @@ const main = async () => {
265170 ) ;
266171 } ;
267172
173+ // Try to establish a Dynatrace connection upfront, to see if everything is configured properly
174+ console . error ( `Testing connection to Dynatrace environment: ${ dtEnvironment } ...` ) ;
175+ // First, we will try a simple "fetch" to connect to dtEnvironment, without authentication
176+ // This should help to see if DNS lookup works, TCP connection can be established, and TLS handshake works
177+ try {
178+ const response = await fetch ( `${ dtEnvironment } ` ) . then ( ( response ) => response . text ( ) ) ;
179+ // check response
180+ if ( response && response . length > 0 ) {
181+ if ( response . includes ( 'Authentication required' ) ) {
182+ // all good - we reached the environment and authentication is required, which is going to be the next step
183+ } else {
184+ console . error ( `⚠️ Tried to contact ${ dtEnvironment } , got the following response: ${ response } ` ) ;
185+ // Note: We won't error out yet, but this information could already be helpful for troubleshooting
186+ }
187+ } else {
188+ throw new Error ( 'No response received' ) ;
189+ }
190+ } catch ( error : any ) {
191+ console . error ( `❌ Failed to connect to Dynatrace environment ${ dtEnvironment } :` , error . message ) ;
192+ console . error ( error ) ;
193+ process . exit ( 3 ) ;
194+ }
195+
196+ // Second, we will try with proper authentication
197+ try {
198+ const dtClient = await createAuthenticatedHttpClient ( scopesBase ) ;
199+ const environmentInformationClient = new EnvironmentInformationClient ( dtClient ) ;
200+
201+ await environmentInformationClient . getEnvironmentInformation ( ) ;
202+
203+ console . error ( `✅ Successfully connected to the Dynatrace environment at ${ dtEnvironment } .` ) ;
204+ } catch ( error : any ) {
205+ if ( isClientRequestError ( error ) ) {
206+ console . error ( `❌ Failed to connect to Dynatrace environment ${ dtEnvironment } :` , handleClientRequestError ( error ) ) ;
207+ } else {
208+ console . error ( `❌ Failed to connect to Dynatrace environment ${ dtEnvironment } :` , error . message ) ;
209+ // Logging more exhaustive error details for troubleshooting
210+ console . error ( error ) ;
211+ }
212+ process . exit ( 2 ) ;
213+ }
214+
215+ // Ready to start the server
216+ console . error ( `Starting Dynatrace MCP Server v${ getPackageJsonVersion ( ) } ...` ) ;
217+
268218 // quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
269219 const tool = (
270220 name : string ,
@@ -297,7 +247,7 @@ const main = async () => {
297247 isError : true ,
298248 } ;
299249 }
300- // else: We don't know what kind of error happened - best-case we can provide error.message
250+ // 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
301251 console . log ( error ) ;
302252 return {
303253 content : [ { type : 'text' , text : `Error: ${ error . message } ` } ] ,
0 commit comments