Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
13 changes: 0 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
81 changes: 57 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -116,14 +113,42 @@ 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. Last error: ${error.message}`,
{ cause: error },
);
}
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
Expand Down Expand Up @@ -827,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) {
Expand All @@ -854,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}`);
Expand Down