From 5862c36bbded88439186170b37432c2e207d382e Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Fri, 12 Sep 2025 11:55:04 +0200 Subject: [PATCH 1/3] chore: Refactor connection test in main --- README.md | 13 ------------ src/index.ts | 56 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 11a11ce..ff665a6 100644 --- a/README.md +++ b/README.md @@ -288,19 +288,6 @@ npx -y @dynatrace-oss/dynatrace-mcp-server@latest --version } ``` -**Configuration for MCP clients that support HTTP transport:** - -```json -{ - "mcpServers": { - "dynatrace-http": { - "url": "http://localhost:3000", - "transport": "http" - } - } -} -``` - ### Rule File For efficient result retrieval from Dynatrace, please consider creating a rule file (e.g., [.github/copilot-instructions.md](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [.amazonq/rules/](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/context-project-rules.html)), instructing coding agents on how to get more details for your component/app/service. Here is an example for [easytrade](https://github.com/Dynatrace/easytrade), please adapt the names and filters to fit your use-cases and components: diff --git a/src/index.ts b/src/index.ts index 278276d..ff5d88a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,21 +83,18 @@ function handleClientRequestError(error: ClientRequestError): string { return `Client Request Error: ${error.message} with HTTP status: ${error.response.status}. ${additionalErrorInformation} (body: ${JSON.stringify(error.body)})`; } -const main = async () => { - // read Environment variables - let dynatraceEnv: DynatraceEnv; - try { - dynatraceEnv = getDynatraceEnv(); - } catch (err) { - console.error((err as Error).message); - process.exit(1); - } - console.error(`Initializing Dynatrace MCP Server v${getPackageJsonVersion()}...`); - const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv; - - // Test connection on startup +/** + * Try to connect to Dynatrace environment with retries and exponential backoff. + */ +async function retryTestDynatraceConnection( + dtEnvironment: string, + oauthClientId?: string, + oauthClientSecret?: string, + dtPlatformToken?: string, +) { let retryCount = 0; - const maxRetries = 5; + const maxRetries = 3; // Max retries + const delayMs = 2000; // Initial delay of 2 seconds while (true) { try { console.error( @@ -116,14 +113,41 @@ const main = async () => { retryCount++; if (retryCount >= maxRetries) { console.error(`Fatal: Maximum number of connection retries (${maxRetries}) exceeded. Exiting.`); - process.exit(1); + throw new Error( + `Failed to connect to Dynatrace environment ${dtEnvironment} after ${maxRetries} attempts. Most likely your configuration is incorrect.`, + ); } - const delay = Math.pow(2, retryCount) * 1000; // Exponential backoff + const delay = Math.pow(2, retryCount) * delayMs; // Exponential backoff console.error(`Retrying in ${delay / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delay)); } } +} + +const main = async () => { + console.error(`Initializing Dynatrace MCP Server v${getPackageJsonVersion()}...`); + + // read Environment variables + let dynatraceEnv: DynatraceEnv; + try { + dynatraceEnv = getDynatraceEnv(); + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + + // Unpack environment variables + const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv; + + // Test connection on startup + try { + await retryTestDynatraceConnection(dtEnvironment, oauthClientId, oauthClientSecret, dtPlatformToken); + } catch (err) { + console.error((err as Error).message); + process.exit(2); + } + // Ready to start the server console.error(`Starting Dynatrace MCP Server v${getPackageJsonVersion()}...`); // Initialize usage tracking From fb535f2b45d21d39681398bda4b7d86b748ab172 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Mon, 15 Sep 2025 11:22:29 +0200 Subject: [PATCH 2/3] chore: pass inner exception as a cause --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index ff5d88a..a996f05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,7 +114,8 @@ async function retryTestDynatraceConnection( if (retryCount >= maxRetries) { console.error(`Fatal: Maximum number of connection retries (${maxRetries}) exceeded. Exiting.`); throw new Error( - `Failed to connect to Dynatrace environment ${dtEnvironment} after ${maxRetries} attempts. Most likely your configuration is incorrect.`, + `Failed to connect to Dynatrace environment ${dtEnvironment} after ${maxRetries} attempts. Most likely your configuration is incorrect. Last error: ${error.message}`, + { cause: error }, ); } const delay = Math.pow(2, retryCount) * delayMs; // Exponential backoff From 9af4f05bf9f9329380a1a21aa3bda78a3ca932ef Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Mon, 15 Sep 2025 11:43:36 +0200 Subject: [PATCH 3/3] fix: HTTP Server needs to handle multiple connections (#134) --- CHANGELOG.md | 2 ++ src/index.ts | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f44935..08d8d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # @dynatrace-oss/dynatrace-mcp-server +- Fixed an issue with stateless HTTP server only taking a single connection + ## Unreleased Changes ## 0.6.0 (Release Candidate 1) diff --git a/src/index.ts b/src/index.ts index a996f05..d2a2a7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -852,15 +852,26 @@ const main = async () => { const httpPort = parseInt(options.port, 10); const host = options.host || '0.0.0.0'; + // HTTP server mode (Stateless) if (httpMode) { - // HTTP server mode - const httpTransport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - }); - const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => { // Parse request body for POST requests let body: unknown; + // Create a new Stateless HTTP Transport + const httpTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, // No Session ID needed + }); + + res.on('close', () => { + // close transport and server, but not the httpServer itself + httpTransport.close(); + server.close(); + }); + + // Connecting MCP-server to HTTP transport + await server.connect(httpTransport); + + // Handle POST Requests for this endpoint if (req.method === 'POST') { const chunks: Buffer[] = []; for await (const chunk of req) { @@ -879,9 +890,6 @@ const main = async () => { await httpTransport.handleRequest(req, res, body); }); - console.error('Connecting server to HTTP transport...'); - await server.connect(httpTransport); - // Start HTTP Server on the specified host and port httpServer.listen(httpPort, host, () => { console.error(`Dynatrace MCP Server running on HTTP at http://${host}:${httpPort}`);